From c3de6b0a9af0239ff2d551b37ac19ef07708fb8b Mon Sep 17 00:00:00 2001 From: Nefrace Date: Sun, 8 Jan 2023 12:50:21 +0300 Subject: [PATCH] Finished project. Enemies, PvP, Music, menug --- actor.go | 8 +++ bullet.go | 51 ++++++++++++++++ enemy.go | 100 +++++++++++++++++++++++++++++++ gamestate.go | 163 +++++++++++++++++++++++++++++++++++++++++++++++++++ logo.png | Bin 0 -> 895 bytes main.go | 63 ++------------------ menu.go | 90 ++++++++++++++++++++++++++++ music.go | 142 ++++++++++++++++++++++++++++++++++++++++++++ player.go | 118 +++++++++++++++++++++++++++++-------- ropes.go | 13 ++-- 10 files changed, 660 insertions(+), 88 deletions(-) create mode 100644 actor.go create mode 100644 bullet.go create mode 100644 enemy.go create mode 100644 gamestate.go create mode 100644 logo.png create mode 100644 menu.go create mode 100644 music.go diff --git a/actor.go b/actor.go new file mode 100644 index 0000000..cc3f012 --- /dev/null +++ b/actor.go @@ -0,0 +1,8 @@ +package main + +type Actor interface { + Update() bool + Draw() + TakeHit(Actor) + GetPosition() Vector +} diff --git a/bullet.go b/bullet.go new file mode 100644 index 0000000..7f980fd --- /dev/null +++ b/bullet.go @@ -0,0 +1,51 @@ +package main + +import "cart/w4" + +type Bullet struct { + Position Vector + SpeedX int8 + SpeedY int8 + Owner Actor + Dead bool +} + +func (b *Bullet) GetPosition() Vector { + return b.Position +} + +func (b *Bullet) Update() bool { + if b.Dead { + return true + } + b.Position.Move(float64(b.SpeedX), float64(b.SpeedY)) + if b.Position.X < 0 || b.Position.X > 320 || b.Position.Y < -160 || b.Position.Y > 320 { + return true + } + for _, actor := range actors { + if actor == Actor(b.Owner) || actor == b { + continue + } + if _, ok := actor.(*Bullet); ok { + continue + } + diff := b.Position.Sub(actor.GetPosition()) + if diff.LenSquared() < 60 { + actor.TakeHit(b) + // b.TakeHit(b) + b.Dead = true + return true + } + } + return false +} + +func (b *Bullet) Draw() { + *w4.DRAW_COLORS = 0x41 + w4.Oval(int(b.Position.X)-3-camX, int(b.Position.Y)-3-camY, 6, 6) +} + +func (b *Bullet) TakeHit(from Actor) { + w4.Tone(800, 10<<8, 20, w4.TONE_MODE2) + b.Dead = true +} diff --git a/enemy.go b/enemy.go new file mode 100644 index 0000000..b00a7c0 --- /dev/null +++ b/enemy.go @@ -0,0 +1,100 @@ +package main + +import ( + "cart/w4" + "math" +) + +const enemyWidth = 8 +const enemyHeight = 8 +const enemyFlags = 1 // BLIT_2BPP +var enemy = [16]byte{0x80, 0x80, 0xaa, 0x80, 0xa6, 0xa4, 0xa9, 0x9a, 0xaa, 0xaa, 0xaa, 0x9a, 0xaa, 0xa8, 0x20, 0x20} + +type Enemy struct { + Position Vector + Speed Vector + Dead bool + ShootTimer uint8 +} + +func (e *Enemy) GetPosition() Vector { + return e.Position +} + +func (e *Enemy) Update() bool { + if e.Position.Y > 320 { + return true + } + if e.Dead { + e.Speed.Y = math.Min(e.Speed.Y+0.2, 4) + } + e.Position.MoveVec(e.Speed) + if e.Position.X < 0 && e.Speed.X < 0 { + e.Position.X = 0 + e.Speed.X *= -1 + } + if e.Position.X > 320 && e.Speed.X > 0 { + e.Position.X = 320 + e.Speed.X *= -1 + } + if e.ShootTimer > 0 { + e.ShootTimer-- + } else if e.Position.X > 0 && e.Position.X < 320 { + e.ShootTimer = 180 + distance := math.MaxFloat64 + direction := Vector{} + var player *Player + for _, p := range players { + if p.Health == 0 { + continue + } + diff := p.Position.Sub(e.Position) + dis := diff.LenSquared() + if distance > dis { + distance = dis + player = p + direction = diff.Normalized() + } + + } + if player != nil { + sx := int8(direction.X*5) / 3 + sy := int8(direction.Y*5) / 3 + b := &Bullet{ + Position: e.Position, + Owner: e, + SpeedX: sx, + SpeedY: sy, + } + w4.Tone(400|300<<16, 3, 5, w4.TONE_PULSE2) + actors = append(actors, b) + } + } + return false +} + +func (e *Enemy) Draw() { + *w4.DRAW_COLORS = 0x44 + var f uint = w4.BLIT_2BPP + if e.Speed.X < 0 { + f |= w4.BLIT_FLIP_X + } + *w4.DRAW_COLORS = 0x430 + w4.Blit(&enemy[0], int(e.Position.X)-camX-4, int(e.Position.Y)-camY-4, 8, 8, f) +} + +func (e *Enemy) TakeHit(from Actor) { + if e.Dead { + return + } + if bullet, ok := from.(*Bullet); ok { + if _, ok := bullet.Owner.(*Player); !ok { + return + } + } + enemies-- + kills++ + e.Dead = true + e.Speed.Y = -3 + w4.Tone(300|200<<16, 5, 20, w4.TONE_PULSE2) +} diff --git a/gamestate.go b/gamestate.go new file mode 100644 index 0000000..1076385 --- /dev/null +++ b/gamestate.go @@ -0,0 +1,163 @@ +package main + +import ( + "cart/w4" + "math/rand" + "strconv" + "unsafe" +) + +var gravity = 0.2 +var points []*Point = []*Point{} +var sticks []*Stick = []*Stick{} +var actors []Actor = []Actor{} +var enemies uint = 0 +var players []*Player +var playersAlive = 0 +var kills int = 0 +var lightIndex uint64 = 0 +var camX int = 0 +var camY int = 0 +var deathTimer = 180 +var music *Music + +func gameStart() { + kills = 0 + deathTimer = 180 + rand.Seed(int64(frame)) + music = &Music{ + KickProb: [8]uint8{100, 10, 25, 15, 50, 10, 20, 30}, + SnareProb: [8]uint8{0, 0, 70, 20, 20, 10, 70, 30}, + LeadProb: [8]uint8{90, 70, 60, 60, 90, 60, 70, 60}, + MainNoteIndex: 1, + } + if musicEnabled { + music.Start() + } + points = []*Point{} + sticks = []*Stick{} + actors = []Actor{} + players = []*Player{} + + for i := 0.0; i < 8; i++ { + var y1, y2 float64 + if int(i)%2 == 0 { + y1 = rand.Float64()*40 - 20 + y2 = rand.Float64() * 40 + } else { + y1 = rand.Float64() * 40 + y2 = rand.Float64()*40 - 20 + } + + p, s := CreateRope( + Vector{0, y1 + i*30}, + Vector{320, y2 + i*30}, + 14, + ) + points = append(points, p...) + sticks = append(sticks, s...) + } + playersAlive = playersCount + for i, ready := range readyPlayers { + if !ready { + continue + } + player := &Player{ + Index: uint8(i), + Health: 20, + ShootTimer: 10, + Position: Vector{80, 80}, + Speed: Vector{}, + Gamepad: gamepads[i], + StickGrabbed: sticks[rand.Intn(len(sticks)-1)], + } + actors = append(actors, player) + players = append(players, player) + } + +} + +func gameUpdate() { + if (!pvp && playersAlive == 0) || (pvp && playersAlive <= 1) { + if deathTimer--; deathTimer == 0 { + if !pvp && kills > maxKills { + w4.DiskW(unsafe.Pointer(&kills), 4) + } + menuStart() + stateUpdate = menuUpdate + } + } + music.Update() + frame += 1 + Simulate(points, sticks) + if frame%120 == 0 && enemies < 10 && !pvp { + spX := rand.Float64()*2 - 1 + posY := rand.Float64()*200 + 50 + posX := float64(-160) + if spX < 0 { + posX = 480 + } + + e := &Enemy{ + Position: Vector{posX, posY}, + Speed: Vector{spX, 0}, + ShootTimer: uint8(rand.Uint32() % 120), + } + enemies++ + actors = append(actors, e) + } + actorsToRemove := []int{} + for i, actor := range actors { + if actor.Update() { + actorsToRemove = append(actorsToRemove, i) + } + } + for i := len(actorsToRemove) - 1; i > 0; i-- { + actor := actorsToRemove[i] + actors = append(actors[:actor], actors[actor+1:]...) + } + target := players[0] + isDead := false + if *w4.NETPLAY&0b100 != 0 { + playerId := *w4.NETPLAY & 0b11 + target = players[playerId] + if target.Health == 0 { + isDead = true + if plr, ok := target.KilledBy.(*Player); ok { + target = plr + } + } + } + camX, camY = int(target.Position.X)-80, int(target.Position.Y)-80 + for _, s := range sticks { + s.Draw() + } + for _, p := range points { + p.Draw() + } + for _, actor := range actors { + actor.Draw() + } + + *w4.DRAW_COLORS = 0x23 + w4.Rect(-160-camX, -100-camY, 160, 500) + w4.Rect(320-camX, -100-camY, 160, 500) + *w4.DRAW_COLORS = 0x4 + if !pvp { + w4.Text(strconv.Itoa(kills)+" kills", 1, 1) + } + // w4.Text(strconv.Itoa(playersAlive), 0, 0) + if !isDead { + if !pvp || (pvp && playersAlive > 1) { + w4.Text("health", 57, 151) + hbar := float32(target.Health) / 20 * 80 + w4.Rect(int(80-hbar), 150, uint(hbar*2), 10) + *w4.DRAW_COLORS = 0x1 + w4.Text("health", 57, 151) + } else { + w4.Text("winner", 57, 151) + } + } else { + w4.Text(" dead ", 57, 151) + } +} diff --git a/logo.png b/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..5a60e8f195b293d54e55ae55955f73d530952b5a GIT binary patch literal 895 zcmeAS@N?(olHy`uVBq!ia0y~yU~~YoKX5PuNw?3wkwA*EILO_JVcj{Imp~4GfKP}k zkp6S;6{CiVTBPZJ-`shoh6O+&){-Erp4F;g0gHsKHe27w6aS&kj|gL1B7ob_@=nUB*_^*F=jVl1H!UX>@QQ@d48UO z=$OltAzZ1@U#*-Od-21qs;r;s-t7FAU1k5K{bu11cyKq4tsqM#??{Z^kKm4ehQ80ajnF{7%Mz2F zdmvhWL;8!4v-+)d*Z+JeZvgh#4YdPLXaBgxTX!#m_g~oMO5-zYOOGEjtz*jPf&~6T z-zOK3x*u4p%h}N 1 { + pvp = true + gameStart() + stateUpdate = gameUpdate + } + case 2: + musicEnabled = !musicEnabled + if musicEnabled { + options[2] = "Music: ON" + } else { + options[2] = "Music: OFF" + } + } + } + for i, gp := range gamepads { + if *gp&w4.BUTTON_2 != 0 { + if !readyPlayers[i] { + readyPlayers[i] = true + playersCount++ + } + } + } + w4.Text(strconv.Itoa(maxKills)+" kills", 2, 40) + w4.Text("CHRISTMAS\nLIGHTS\nMASSACRE", 70, 2) + w4.Text("Press Z to get ready", 0, 150) + for i, opt := range options { + *w4.DRAW_COLORS = 0x3 + if i == selected { + *w4.DRAW_COLORS = 0x4 + } + w4.Text(opt, 2, 60+i*9) + *w4.DRAW_COLORS = 0x2 + } + for i, v := range readyPlayers { + if v { + w4.Text("Player "+strconv.Itoa(i+1)+" is ready", 8, 100+i*8) + } + } +} diff --git a/music.go b/music.go new file mode 100644 index 0000000..f3728bc --- /dev/null +++ b/music.go @@ -0,0 +1,142 @@ +package main + +import ( + "cart/w4" + "math/rand" +) + +const ( + C int = iota + Cs + D + Ds + E + F + Fs + G + Gs + A + As + B +) + +var frequencies = []uint{262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494} + +type Music struct { + KickProb [8]uint8 + KickLine [8]bool + SnareProb [8]uint8 + SnareLine [8]bool + LeadProb [8]uint8 + LeadLine [8]uint + MainNoteIndex int + Frame uint64 + CurrentPart uint8 + CurrentLine uint8 + Active bool + MutedFrames uint +} + +func (m *Music) Start() { + m.GenLines() + m.Frame = 0 + m.CurrentLine = 0 + m.CurrentPart = 0 + m.Active = true +} +func (m *Music) Pause() { + m.Active = false +} +func (m *Music) Resume() { + m.Active = true + m.MutedFrames = 0 +} + +func (m *Music) MuteFor(frames uint) { + m.MutedFrames = frames +} + +var pentaScale = []int{0, 3, 5, 7, 10, 12} + +func (m *Music) PentaNote(n int) uint { + pentaIndex := n % 6 + octave := 1 + n/6 + // w4.Trace("Octave: " + strconv.Itoa(octave)) + noteIndex := (m.MainNoteIndex + pentaScale[pentaIndex]) % 12 + return frequencies[noteIndex] * uint(octave) +} + +func (m *Music) Update() { + if !m.Active { + return + } + if m.MutedFrames > 0 { + m.MutedFrames-- + } + if m.Frame%15 == 0 { + if m.MutedFrames == 0 { + if m.KickLine[m.CurrentLine] { + w4.Tone(200|1<<16, 5, 60, w4.TONE_TRIANGLE) + } + if m.SnareLine[m.CurrentLine] { + w4.Tone(200|50<<16, 10<<8, 10, w4.TONE_NOISE) + } + if m.LeadLine[m.CurrentLine] != 0 { + w4.Tone(m.LeadLine[m.CurrentLine], 10|15<<8, 5, w4.TONE_MODE2|w4.TONE_PULSE2) + } + } + m.CurrentLine++ + if m.CurrentLine == 8 { + m.CurrentLine = 0 + m.CurrentPart++ + if m.CurrentPart == 4 { + m.CurrentPart = 0 + m.GenLines() + } + } + } + m.Frame++ +} + +func (m *Music) GenLines() { + m.KickLine = m.GenDrumLine(m.KickProb) + m.SnareLine = m.GenDrumLine(m.SnareProb) + m.LeadLine = m.GenLeadLine(m.LeadProb) + m.MainNoteIndex = rand.Intn(5) +} + +func (m *Music) GenDrumLine(prob [8]uint8) [8]bool { + line := [8]bool{} + for i, p := range prob { + line[i] = rand.Intn(101) < int(p) + } + return line +} + +func (m *Music) GenLeadLine(prob [8]uint8) [8]uint { + line := [8]uint{} + pattern := [8]int{} + offset := rand.Intn(6) + switch rand.Intn(4) { + case 0: + pattern = [8]int{0, 1, 2, 3, 0, 1, 2, 3} + case 1: + pattern = [8]int{3, 2, 1, 0, 3, 2, 1, 0} + case 2: + pattern = [8]int{7, 5, 6, 4, 5, 3, 4, 2} + case 3: + pattern = [8]int{0, 2, 1, 3, 2, 4, 3, 5} + case 4: + pattern = [8]int{0, 2, 4, 1, 3, 5, 2, 4} + case 5: + pattern = [8]int{4, 2, 5, 3, 1, 4, 2, 0} + } + for i, p := range prob { + if rand.Intn(101) < int(p) { + line[i] = m.PentaNote(pattern[i]+offset) / 2 + continue + } + line[i] = 0 + } + return line +} diff --git a/player.go b/player.go index 1dbdc8a..eb3a8aa 100644 --- a/player.go +++ b/player.go @@ -7,39 +7,64 @@ import ( ) type Player struct { - Position Vector - Speed Vector - //PointGrabbed *Point + Index uint8 + Health uint8 + KilledBy Actor + Position Vector + Speed Vector StickGrabbed *Stick StickOffset float64 - GrabTimeout uint + GrabTimer uint8 + ShootTimer uint8 + ShootDirX int8 + ShootDirY int8 Gamepad *uint8 GamepadLast uint8 } -func (p *Player) Update() { +func (p *Player) GetPosition() Vector { + return p.Position +} + +func (p *Player) Update() bool { + var dirX int8 = int8(*p.Gamepad&w4.BUTTON_RIGHT>>5) - int8(*p.Gamepad&w4.BUTTON_LEFT>>4) + var dirY int8 = int8(*p.Gamepad&w4.BUTTON_DOWN>>7) - int8(*p.Gamepad&w4.BUTTON_UP>>6) + if !(dirX == 0 && dirY == 0) && p.Health > 0 { + p.ShootDirX = dirX + p.ShootDirY = dirY + } lastGamepad := *p.Gamepad & (*p.Gamepad ^ p.GamepadLast) p.GamepadLast = *p.Gamepad - if p.GrabTimeout > 0 { - p.GrabTimeout-- + if p.GrabTimer > 0 { + p.GrabTimer-- } + if p.ShootTimer > 0 { + p.ShootTimer-- + } else { + if *p.Gamepad&w4.BUTTON_1 != 0 && !(p.ShootDirX == 0 && p.ShootDirY == 0) { + bullet := &Bullet{ + Owner: p, + Position: p.Position, + SpeedX: p.ShootDirX * 3, + SpeedY: p.ShootDirY * 3, + } + // w4.Trace(strconv.Itoa(int(p.ShootDirX))) + // w4.Trace(strconv.Itoa(int(p.ShootDirY))) + // w4.Trace(p.Position.String()) + w4.Tone(150|50<<16, 10, 100, w4.TONE_MODE1) + p.ShootTimer = 15 + actors = append(actors, bullet) + } + } + isJumping := *p.Gamepad&w4.BUTTON_DOWN == 0 p.Speed.Y = math.Min(4, p.Speed.Y+gravity) if p.StickGrabbed != nil { - p.Speed.Y = 0 p.Speed.X = 0 - // p.Position = p.PointGrabbed.Position - if *p.Gamepad&w4.BUTTON_LEFT != 0 { - p.Speed.X -= 1 - } - if *p.Gamepad&w4.BUTTON_RIGHT != 0 { - p.Speed.X += 1 - } - if *p.Gamepad&w4.BUTTON_UP != 0 { - p.Speed.Y -= 1 - } - if *p.Gamepad&w4.BUTTON_DOWN != 0 { - p.Speed.Y += 1 + p.Speed.Y = 0 + if *p.Gamepad&w4.BUTTON_1 == 0 { + p.Speed.X = float64(dirX) + p.Speed.Y = float64(dirY) } p.MoveOnRope(p.Speed) if p.StickOffset < 0 { @@ -62,9 +87,9 @@ func (p *Player) Update() { } p.Position = p.StickGrabbed.GetPosition(p.StickOffset) if lastGamepad&w4.BUTTON_2 != 0 { - p.GrabTimeout = 10 + p.GrabTimer = 10 if isJumping { - p.GrabTimeout = 10 + p.GrabTimer = 10 p.Speed = p.Speed.MulScalar(2) if p.Speed.Y <= 0 { p.Speed.Y -= 1 * 2 @@ -75,9 +100,17 @@ func (p *Player) Update() { } p.StickGrabbed = nil } + if p.ShootTimer == 15 { + impulse := Vector{-float64(dirX), -float64(dirY)} + if p.StickGrabbed != nil { + p.StickGrabbed.PointA.Position.MoveVec(impulse) + p.StickGrabbed.PointB.Position.MoveVec(impulse) + } + + } } else { p.Position.Move(p.Speed.X, p.Speed.Y) - if *p.Gamepad&w4.BUTTON_DOWN == 0 && p.GrabTimeout == 0 { + if *p.Gamepad&w4.BUTTON_DOWN == 0 && p.GrabTimer == 0 && p.Health > 0 { distance := math.MaxFloat64 var selectedPoint *Point for _, point := range points { @@ -98,7 +131,6 @@ func (p *Player) Update() { } } if stickDistance < 4 { - w4.Trace(strconv.FormatFloat(stickDistance, 'f', 3, 64)) p.StickGrabbed = selectedStick p.StickOffset = selectedStick.GetOffset(p.Position) p.StickGrabbed.PointA.Position.MoveVec(p.Speed) @@ -107,6 +139,11 @@ func (p *Player) Update() { } } p.Position.X = math.Min(math.Max(0, p.Position.X), 320) + if p.Position.Y > 320 && p.Health > 0 { + p.Health = 0 + p.Death() + } + return false } func (p *Player) MoveOnRope(motion Vector) { @@ -117,5 +154,36 @@ func (p *Player) MoveOnRope(motion Vector) { func (p *Player) Draw() { *w4.DRAW_COLORS = 0x34 - w4.Rect(int(p.Position.X)-4-camX, int(p.Position.Y)-4-camY, 8, 8) + drawX := int(p.Position.X) - 4 - camX + drawY := int(p.Position.Y) - 4 - camY + w4.Rect(drawX, drawY, 8, 8) + + eyePosX := drawX + 4 + int(p.ShootDirX) + eyePosY := drawY + 4 - 2 + int(p.ShootDirY) + w4.Rect(eyePosX-2, eyePosY, 1, 2) + w4.Rect(eyePosX+1, eyePosY, 1, 2) + if pvp { + *w4.DRAW_COLORS = 0x4 + w4.Text("P"+strconv.Itoa(int(p.Index+1)), drawX-3, drawY-10) + } +} + +func (p *Player) TakeHit(from Actor) { + if p.Health > 0 { + if p.Health--; p.Health == 0 { + if bullet, ok := from.(*Bullet); ok { + p.StickGrabbed = nil + p.KilledBy = bullet.Owner + p.Death() + } + } + w4.Tone(250|200<<16, 15, 60, w4.TONE_PULSE1) + } +} + +func (p *Player) Death() { + p.Speed.Y = -3 + playersAlive-- + music.MuteFor(30) + w4.Tone(600|200<<16, 15|15<<8, 30, w4.TONE_PULSE2) } diff --git a/ropes.go b/ropes.go index 74575b5..b05ef19 100644 --- a/ropes.go +++ b/ropes.go @@ -9,7 +9,7 @@ type Point struct { Position Vector PreviousPosition Vector IsLocked bool - TimeOffset uint64 + TimeOffset uint16 Sticks []*Stick } @@ -24,14 +24,15 @@ func (p *Point) GetMotion() Vector { func (p *Point) Draw() { *w4.DRAW_COLORS = 0x31 - fr := (frame + p.TimeOffset) % 60 + fr := (frame + uint64(p.TimeOffset)) % 60 if fr > 20 { - *w4.DRAW_COLORS = 0x32 - } - if fr > 40 { *w4.DRAW_COLORS = 0x34 } + if fr > 40 { + *w4.DRAW_COLORS = 0x32 + } w4.Oval(int(p.Position.X)-2-camX, int(p.Position.Y)-2-camY, 4, 4) + } type Stick struct { @@ -129,7 +130,7 @@ func CreateRope(start Vector, end Vector, divisions int) ([]*Point, []*Stick) { Position: pos, PreviousPosition: pos, IsLocked: (i == 0 || i == divisions), - TimeOffset: lightIndex * 153, + TimeOffset: uint16(lightIndex * 147), } lightIndex++ if i != 0 {