Compare commits

..

28 Commits

Author SHA1 Message Date
63eafe036a Added a rain layer for atmosphere. 2024-11-23 01:13:55 -05:00
253c708d45 Added weapon drops and collection. New laser item asset. 2024-11-22 17:57:16 -05:00
b3a8ef8c0f Housekeeping. 2024-11-21 16:35:36 -05:00
257318926d Added weapon system. Animated laser. 2024-11-21 16:26:25 -05:00
75d464b5e2 Guideline for laser mechanic removed. 2024-11-21 13:33:37 -05:00
30b13dbc6c Playing around with a laser weapon. 2024-11-21 13:20:17 -05:00
1498865026 Fixed health bar positioning. Added fireball sfx. 2024-11-17 13:35:29 -05:00
e049e8c3d0 Fireballs and shadows. 2024-11-16 19:03:07 -05:00
1d65d0046e Cloud layer. 2024-11-16 12:31:22 -05:00
fd46346346 Minor tweaks to flygoblin. 2024-11-15 18:40:08 -05:00
a4a532edec Refactoring in explosion sounds. 2024-11-15 16:46:20 -05:00
cbc4ba5eb3 Refactor to use better interfaces and event callbacks. 2024-11-15 16:11:45 -05:00
4ced75d66c Pause sounds, tweak on gunshot. 2024-11-13 10:42:35 -05:00
478ba994d6 Added new enemy character. WIP. 2024-11-13 07:44:56 -05:00
8a1194eca3 Pause/unpause sound added. Minor audio tweaks. Cleanup. 2024-11-11 17:25:33 -05:00
7b08eadd27 New title backdrop. 2024-11-11 15:43:24 -05:00
6aae03ed18 Added copyright. Background generation cleanup. Audio tweaks. 2024-11-11 15:00:44 -05:00
f6ab64ca6e Shadow cleanup on target explosion. 2024-11-11 14:32:59 -05:00
658ae73c9b Added music and sfx. 2024-11-11 14:07:02 -05:00
56d1f62020 Splash screen update, gamepad cleanup on main game. 2024-11-11 10:48:44 -05:00
e10bf47427 Adding splash screen. 2024-11-11 10:25:02 -05:00
6f794b7bb2 Brought in screen manager, minor refactor. 2024-11-11 09:54:30 -05:00
9130155999 Moving to standardgamepad buttons. 2024-11-08 16:02:41 -05:00
b4287ad61e Cleanup. 2024-11-08 07:29:34 -05:00
8bf6e56398 Added weapon sprite into the mix. 2024-11-07 17:39:39 -05:00
0648ed3658 Decoupled death animation from draw thread. 2024-11-07 15:02:21 -05:00
1897114236 Decoupling death animation from draw thread. 2024-11-07 15:01:10 -05:00
87e40226c7 Cleanup, touchups, new death animation on hero. 2024-11-07 10:41:30 -05:00
74 changed files with 4326 additions and 670 deletions

15
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "survive",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "main.go"
}
]
}

BIN
assets/altar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

63
assets/audiobank.go Normal file
View File

@@ -0,0 +1,63 @@
package assets
import (
"bytes"
_ "embed"
"log"
"github.com/hajimehoshi/ebiten/v2/audio/wav"
)
type SndAssetName string
const (
MainLoop SndAssetName = "MainLoop"
Explode SndAssetName = "Explode"
sampleRate = 44100
)
var (
SoundBank map[SndAssetName]*wav.Stream
//go:embed loop.wav
mainloop_snd []byte
//go:embed explode.wav
explode_snd []byte
//go:embed explode.wav
Splode []byte
//go:embed hit.wav
TargetHit []byte
//go:embed herodeath.wav
HeroDeath []byte
//go:embed shot.wav
Shot []byte
//go:embed magic.wav
Magic []byte
//go:embed survive.wav
Survive []byte
//go:embed pausein.wav
PauseIn []byte
//go:embed pauseout.wav
PauseOut []byte
//go:embed flare.wav
Flare []byte
)
func LoadSounds() {
SoundBank = make(map[SndAssetName]*wav.Stream)
SoundBank[MainLoop] = LoadSoundFatal(sampleRate, mainloop_snd)
SoundBank[Explode] = LoadSoundFatal(sampleRate, explode_snd)
}
func LoadSoundFatal(rate int, obj []byte) *wav.Stream {
stream, err := wav.DecodeWithSampleRate(rate, bytes.NewReader(obj))
if err != nil {
log.Fatal("dead, jim")
}
return stream
}

BIN
assets/cloud.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
assets/explode.wav Normal file

Binary file not shown.

BIN
assets/flare.wav Normal file

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Before

Width:  |  Height:  |  Size: 682 B

After

Width:  |  Height:  |  Size: 682 B

BIN
assets/grasstile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
assets/hero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
assets/herodeath.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
assets/herodeath.wav Normal file

Binary file not shown.

BIN
assets/hit.wav Normal file

Binary file not shown.

BIN
assets/hot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

107
assets/imagebank.go Normal file
View File

@@ -0,0 +1,107 @@
package assets
import (
"bytes"
"image"
"log"
_ "embed"
"github.com/hajimehoshi/ebiten/v2"
)
type ImgAssetName string
const (
Title ImgAssetName = "Title"
FlyEyeNormal ImgAssetName = "FlyEyeNormal"
FlyEyeDamaged ImgAssetName = "FlyEyeDamaged"
FlyEyeDying ImgAssetName = "FlyEyeDying"
FlyEyeShadow ImgAssetName = "FlyEyeShadow"
HeroNormal ImgAssetName = "HeroNormal"
HeroDying ImgAssetName = "HeroDying"
TileSet ImgAssetName = "TileSet"
Altar ImgAssetName = "Altar"
Weapon ImgAssetName = "Weapon"
WormDamaged ImgAssetName = "WormDamaged"
Worm ImgAssetName = "WormDefault"
Cloud ImgAssetName = "Cloud"
Fireball ImgAssetName = "Fireball"
Splash ImgAssetName = "Splash"
LaserBeam ImgAssetName = "LaserBeam"
ItemLaser ImgAssetName = "ItemLaser"
RainSplash ImgAssetName = "RainSplash"
)
var (
ImageBank map[ImgAssetName]*ebiten.Image
//go:embed title.png
title_img []byte
//go:embed fly-eye.png
flyeye_img []byte
//go:embed fly-eye2.png
flyeye_img2 []byte
//go:embed fly-eye3.png
flyeye_img3 []byte
//go:embed shadow.png
shadow_img []byte
//go:embed hero.png
hero_img []byte
//go:embed herodeath.png
herodeath_img []byte
//go:embed grasstile.png
tileset_img []byte
//go:embed altar.png
altar_img []byte
//go:embed weapon.png
weapon_img []byte
//go:embed worm.png
worm_img []byte
//go:embed wormdefault.png
wormdefault_img []byte
//go:embed cloud.png
cloud_img []byte
//go:embed hot.png
fireball_img []byte
//go:embed splash.png
splash_img []byte
//go:embed laserbeam.png
laserbeam_img []byte
//go:embed item-laser.png
itemlaser_img []byte
//go:embed rain-splash.png
rainsplash_img []byte
)
func LoadImages() {
ImageBank = make(map[ImgAssetName]*ebiten.Image)
ImageBank[Title] = LoadImagesFatal(title_img)
ImageBank[FlyEyeNormal] = LoadImagesFatal(flyeye_img)
ImageBank[FlyEyeDamaged] = LoadImagesFatal(flyeye_img2)
ImageBank[FlyEyeDying] = LoadImagesFatal(flyeye_img3)
ImageBank[FlyEyeShadow] = LoadImagesFatal(shadow_img)
ImageBank[HeroNormal] = LoadImagesFatal(hero_img)
ImageBank[HeroDying] = LoadImagesFatal(herodeath_img)
ImageBank[TileSet] = LoadImagesFatal(tileset_img)
ImageBank[Altar] = LoadImagesFatal(altar_img)
ImageBank[Weapon] = LoadImagesFatal(weapon_img)
ImageBank[WormDamaged] = LoadImagesFatal(worm_img)
ImageBank[Worm] = LoadImagesFatal(wormdefault_img)
ImageBank[Cloud] = LoadImagesFatal(cloud_img)
ImageBank[Fireball] = LoadImagesFatal(fireball_img)
ImageBank[Splash] = LoadImagesFatal(splash_img)
ImageBank[LaserBeam] = LoadImagesFatal(laserbeam_img)
ImageBank[ItemLaser] = LoadImagesFatal(itemlaser_img)
ImageBank[RainSplash] = LoadImagesFatal(rainsplash_img)
}
func LoadImagesFatal(b []byte) *ebiten.Image {
img, _, err := image.Decode(bytes.NewReader(b))
if err != nil {
log.Fatal(err)
}
return ebiten.NewImageFromImage(img)
}

BIN
assets/item-laser.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 B

BIN
assets/laserbeam.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 786 B

BIN
assets/loop.wav Normal file

Binary file not shown.

BIN
assets/magic.wav Normal file

Binary file not shown.

BIN
assets/pausein.wav Normal file

Binary file not shown.

BIN
assets/pauseout.wav Normal file

Binary file not shown.

BIN
assets/rain-splash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

BIN
assets/shadow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 B

BIN
assets/shot.wav Normal file

Binary file not shown.

BIN
assets/splash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 615 B

BIN
assets/survive.wav Normal file

Binary file not shown.

BIN
assets/title.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

BIN
assets/weapon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 741 B

BIN
assets/worm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

BIN
assets/wormdefault.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

130
elements/boss.go Normal file
View File

@@ -0,0 +1,130 @@
package elements
import (
"image"
"image/color"
"mover/assets"
"mover/gamedata"
"github.com/hajimehoshi/ebiten/v2"
)
type Boss struct {
Sprite *ebiten.Image
Maks *ebiten.Image
MaskDest *ebiten.Image
Spawned bool
Pos gamedata.Coordinates
Right bool
Health int
Touched bool
damage bool
cycle int
Action MoverAction
hitcount int
damageduration int
SplodeInitiated bool
}
func NewBoss() *Boss {
b := &Boss{
Sprite: ebiten.NewImage(96, 96),
Spawned: false,
Action: MoverActionDefault,
hitcount: 0,
Maks: ebiten.NewImage(96, 96),
MaskDest: ebiten.NewImage(96, 96),
Health: 100,
Touched: false,
}
b.Maks.Fill(color.White)
return b
}
func (b *Boss) Update() {
if b.damage {
b.damageduration++
if b.damageduration > 30 {
b.damage = false
b.damageduration = 0
}
}
if b.Action == MoverActionDead {
b.Spawned = false
}
b.cycle++
}
func (b *Boss) Draw() {
b.Sprite.Clear()
b.MaskDest.Clear()
/*
//b.Sprite.Fill(color.RGBA{R: 0xFF, G: 0xFF, B: 0x00, A: 0xFF})
vector.DrawFilledCircle(b.Sprite, 48, 48, 48, color.RGBA{R: 0xFF, G: 0xFF, B: 0x00, A: 0xFF}, true)
*/
idx := (b.cycle / 8) % 4
x0 := 96 * idx
x1 := x0 + 96
y0 := 0
y1 := 96
op := &ebiten.DrawImageOptions{}
if b.Right {
op.GeoM.Scale(-1, 1)
op.GeoM.Translate(MOVER_WIDTH*2, 0)
}
switch b.Action {
case MoverActionDefault:
b.Sprite.DrawImage(assets.ImageBank[assets.Worm].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), op)
case MoverActionDamaged:
if (b.cycle/5)%2 == 0 && b.damage {
b.MaskDest.DrawImage(assets.ImageBank[assets.WormDamaged].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), op)
op := &ebiten.DrawImageOptions{}
op.GeoM.Reset()
op.Blend = ebiten.BlendSourceAtop
b.MaskDest.DrawImage(b.Maks, op)
b.Sprite.DrawImage(b.MaskDest, nil)
} else {
b.Sprite.DrawImage(assets.ImageBank[assets.WormDamaged].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), op)
}
case MoverActionExploding:
op.GeoM.Scale(2, 2)
//op.GeoM.Translate(-48, -48)
b.Sprite.DrawImage(assets.ImageBank[assets.FlyEyeDying].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), op)
if idx == 3 {
b.Action++
}
}
}
func (b *Boss) SetHit() {
b.hitcount++
b.damage = true
b.Health--
if b.Health <= 0 {
b.Action = MoverActionExploding
b.cycle = 0
}
}
func (b *Boss) Reset() {
b.hitcount = 0
b.damage = false
b.damageduration = 0
b.Action = MoverActionDefault
b.Spawned = false
b.Health = 100
}
func (b *Boss) ToggleColor() {
if b.Action == MoverActionDefault {
b.Action = MoverActionDamaged
} else if b.Action == MoverActionDamaged {
b.Action = MoverActionDefault
}
}

62
elements/cloud.go Normal file
View File

@@ -0,0 +1,62 @@
package elements
import (
"math"
"mover/assets"
"mover/gamedata"
"github.com/hajimehoshi/ebiten/v2"
)
type Cloud struct {
Sprite *ebiten.Image
position gamedata.Coordinates
angle float64
velocity float64
Alpha float64
}
func NewCloud(a gamedata.Area, angle, velocity float64) *Cloud {
c := &Cloud{
Sprite: ebiten.NewImage(a.Width, a.Height),
angle: angle, //rand.Float64() * (math.Pi * 2),
velocity: velocity,
}
return c
}
func (c *Cloud) Update() error {
c.position.X += c.velocity * math.Cos(c.angle)
c.position.Y += c.velocity * math.Sin(c.angle)
return nil
}
func (c *Cloud) Draw() {
c.Sprite.Clear()
//c.Sprite.Fill(color.RGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff})
//c.Sprite.Fill(color.White)
cloudsprite := assets.ImageBank[assets.Cloud]
spritex := cloudsprite.Bounds().Dx()
spritey := cloudsprite.Bounds().Dy()
cloudx := c.Sprite.Bounds().Dx()
cloudy := c.Sprite.Bounds().Dy()
scalex := float64(cloudx) / float64(spritex)
scaley := float64(cloudy) / float64(spritey)
op := &ebiten.DrawImageOptions{}
op.GeoM.Scale(scalex, scaley)
c.Sprite.DrawImage(assets.ImageBank[assets.Cloud], op)
}
func (c *Cloud) SetPosition(p gamedata.Coordinates) {
c.position = p
}
func (c *Cloud) GetPosition() gamedata.Coordinates {
return c.position
}

28
elements/enemies.go Normal file
View File

@@ -0,0 +1,28 @@
package elements
import (
"mover/gamedata"
"github.com/hajimehoshi/ebiten/v2"
)
type Enemies interface {
Update() error
Draw()
GetPosition() gamedata.Coordinates
SetPosition(gamedata.Coordinates)
SetTarget(gamedata.Coordinates)
GetSprite() *ebiten.Image
GetEnemyState() gamedata.EnemyState
SetHit()
SetToggle()
IsToggled() bool
SetTouched()
ClearTouched()
IsTouched() bool
ExplosionInitiated() bool
SetExplosionInitiated()
Health() int
MaxHealth() int
GetAngle() float64
}

View File

@@ -1,8 +1,10 @@
package main
package elements
import "mover/gamedata"
type Explosion struct {
Radius float64
Origin Coordinates
Origin gamedata.Coordinates
cycle int
Active bool
}
@@ -27,7 +29,7 @@ func (e *Explosion) Update() {
}
func (e *Explosion) SetOrigin(origin Coordinates) {
func (e *Explosion) SetOrigin(origin gamedata.Coordinates) {
e.Origin = origin
}

118
elements/fireball.go Normal file
View File

@@ -0,0 +1,118 @@
package elements
import (
"image"
"math"
"mover/assets"
"mover/gamedata"
"github.com/hajimehoshi/ebiten/v2"
)
type FireBall struct {
Sprite *ebiten.Image
position gamedata.Coordinates
angle float64
velocity float64
cycle int
}
func NewFireBall(angle, velocity float64) *FireBall {
fb := &FireBall{
Sprite: ebiten.NewImage(50, 50),
cycle: 0,
angle: angle,
velocity: velocity,
}
return fb
}
func (fb *FireBall) Update() error {
fb.position.X += fb.velocity * math.Cos(fb.angle)
fb.position.Y += fb.velocity * math.Sin(fb.angle)
fb.cycle++
return nil
}
func (fb *FireBall) Draw() {
fb.Sprite.Clear()
//fb.Sprite.Fill(color.RGBA{R: 0xff, G: 0x00, B: 0x00, A: 0xff})
idx := fb.cycle / 8 % 5
x0 := idx * 50
x1 := x0 + 50
y0 := 0
y1 := 50
fb.Sprite.DrawImage(assets.ImageBank[assets.Fireball].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), nil)
}
func (fb *FireBall) GetPosition() gamedata.Coordinates {
return fb.position
}
func (fb *FireBall) SetPosition(pos gamedata.Coordinates) {
fb.position = pos
}
func (fb *FireBall) SetTarget(gamedata.Coordinates) {
}
func (fb *FireBall) GetAngle() float64 {
return fb.angle
}
func (fb *FireBall) GetVelocity() float64 {
return fb.velocity
}
func (fb *FireBall) GetSprite() *ebiten.Image {
return fb.Sprite
}
func (fb *FireBall) ClearTouched() {
}
func (fb *FireBall) ExplosionInitiated() bool {
return false
}
func (fb *FireBall) GetEnemyState() gamedata.EnemyState {
return gamedata.EnemyStateDefault
}
func (fb *FireBall) SetHit() {
}
func (fb *FireBall) SetToggle() {
}
func (fb *FireBall) IsToggled() bool {
return false
}
func (fb *FireBall) SetTouched() {
}
func (fb *FireBall) IsTouched() bool {
return false
}
func (fb *FireBall) SetExplosionInitiated() {
}
func (fb *FireBall) Health() int {
return 0
}
func (fb *FireBall) MaxHealth() int {
return 1
}

180
elements/flyeye.go Normal file
View File

@@ -0,0 +1,180 @@
package elements
import (
"image"
"image/color"
"math"
"mover/assets"
"mover/gamedata"
"github.com/hajimehoshi/ebiten/v2"
)
type FlyEye struct {
Sprite *ebiten.Image
Maks *ebiten.Image
MaksDest *ebiten.Image
position gamedata.Coordinates
target gamedata.Coordinates
state gamedata.EnemyState
cycle int
dyingcount int
health int
hit bool
touched bool
toggle bool
sploding bool
}
func NewFlyEye() *FlyEye {
f := &FlyEye{
Sprite: ebiten.NewImage(46, 46),
Maks: ebiten.NewImage(48, 48),
MaksDest: ebiten.NewImage(48, 48),
health: 0,
cycle: 0,
dyingcount: 0,
hit: false,
touched: false,
toggle: false,
sploding: false,
}
f.Maks.Fill(color.White)
return f
}
func (f *FlyEye) Update() error {
//close loop on target
if f.state <= gamedata.EnemyStateHit {
dx := f.target.X - f.position.X
dy := f.target.Y - f.position.Y
if math.Abs(dx) > 3 || math.Abs(dy) > 3 {
angle := math.Atan2(dy, dx)
f.position.X += math.Cos(angle) * 3
f.position.Y += math.Sin(angle) * 3
}
}
if f.state == gamedata.EnemyStateDying {
f.dyingcount++
}
f.cycle++
return nil
}
func (f *FlyEye) Draw() {
f.Sprite.Clear()
f.MaksDest.Clear()
idx := (f.cycle / 8) % 4
y0 := 0
y1 := 48
x0 := 48 * idx
x1 := x0 + 48
switch f.state {
case gamedata.EnemyStateDefault:
if !f.toggle {
f.Sprite.DrawImage(assets.ImageBank[assets.FlyEyeNormal].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), nil)
} else {
f.Sprite.DrawImage(assets.ImageBank[assets.FlyEyeDamaged].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), nil)
}
case gamedata.EnemyStateDying:
//after some condition, set to exploding
if (f.cycle/5)%2 == 0 {
f.MaksDest.DrawImage(assets.ImageBank[assets.FlyEyeDamaged].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), nil)
op := &ebiten.DrawImageOptions{}
op.GeoM.Reset()
op.Blend = ebiten.BlendSourceAtop
f.MaksDest.DrawImage(f.Maks, op)
f.Sprite.DrawImage(f.MaksDest, nil)
} else {
f.Sprite.DrawImage(assets.ImageBank[assets.FlyEyeDamaged].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), nil)
}
if f.dyingcount >= 31 {
f.cycle = 0
f.state = gamedata.EnemyStateExploding
}
case gamedata.EnemyStateExploding:
f.Sprite.DrawImage(assets.ImageBank[assets.FlyEyeDying].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), nil)
if idx == 3 {
f.state = gamedata.EnemyStateDead
}
}
}
func (f *FlyEye) GetPosition() gamedata.Coordinates {
return f.position
}
func (f *FlyEye) SetTarget(p gamedata.Coordinates) {
f.target = p
}
func (f *FlyEye) GetSprite() *ebiten.Image {
return f.Sprite
}
func (f *FlyEye) SetHit() {
f.hit = true
f.state = gamedata.EnemyStateDying
f.cycle = 0
f.health--
}
func (f *FlyEye) IsTouched() bool {
return f.touched
}
func (f *FlyEye) SetTouched() {
f.touched = true
}
func (f *FlyEye) ClearTouched() {
f.touched = false
}
func (f *FlyEye) SetToggle() {
f.toggle = !f.toggle
}
func (f *FlyEye) IsToggled() bool {
return f.toggle
}
func (f *FlyEye) GetEnemyState() gamedata.EnemyState {
return f.state
}
func (f *FlyEye) SetPosition(p gamedata.Coordinates) {
f.position = p
}
func (f *FlyEye) ExplosionInitiated() bool {
return f.sploding
}
func (f *FlyEye) SetExplosionInitiated() {
f.sploding = true
}
func (f *FlyEye) Health() int {
//health bars reserved for special enemies, flyeye is a one
//hitter so returning zero ensure no health bar is rendered
return f.health
}
func (f *FlyEye) MaxHealth() int {
return 1
}
func (f *FlyEye) GetAngle() float64 {
return 0
}

218
elements/flygoblin.go Normal file
View File

@@ -0,0 +1,218 @@
package elements
import (
"image"
"image/color"
"math/rand/v2"
"mover/assets"
"mover/gamedata"
"github.com/hajimehoshi/ebiten/v2"
)
const (
FG_MAXHEALTH = 100
)
type FlyGoblin struct {
Sprite *ebiten.Image
Maks *ebiten.Image
MaksDest *ebiten.Image
position gamedata.Coordinates
target gamedata.Coordinates
state gamedata.EnemyState
cycle int
health int
damageduration int
right bool
touched bool
toggle bool
sploding bool
damage bool
called bool
deathcallback func()
fireballcallback func()
}
func NewFlyGoblin() *FlyGoblin {
fg := &FlyGoblin{
Sprite: ebiten.NewImage(96, 96),
Maks: ebiten.NewImage(96, 96),
MaksDest: ebiten.NewImage(96, 96),
health: FG_MAXHEALTH,
damageduration: 0,
called: false,
}
fg.Maks.Fill(color.White)
return fg
}
func (f *FlyGoblin) Update() error {
if f.damage {
f.damageduration++
if f.damageduration > 30 {
f.damage = false
f.damageduration = 0
}
}
if f.state < gamedata.EnemyStateDying {
dx := f.target.X - f.position.X
dy := f.target.Y - f.position.Y
f.right = dx/48 > 0
f.position.X += dx / 48
f.position.Y += dy / 48
//10% chance to summon fireball
fb := rand.Float64() >= 0.9
if (f.cycle/8)%4 == 0 && fb {
if f.fireballcallback != nil {
f.fireballcallback()
}
}
}
if f.state == gamedata.EnemyStateDead && !f.called {
f.called = true
if f.deathcallback != nil {
f.deathcallback()
}
}
f.cycle++
return nil
}
func (f *FlyGoblin) Draw() {
f.Sprite.Clear()
f.MaksDest.Clear()
idx := (f.cycle / 8) % 4
x0 := 96 * idx
x1 := x0 + 96
y0 := 0
y1 := 96
op := &ebiten.DrawImageOptions{}
if f.right {
op.GeoM.Scale(-1, 1)
op.GeoM.Translate(MOVER_WIDTH*2, 0)
}
switch f.state {
case gamedata.EnemyStateDefault:
if !f.toggle {
f.Sprite.DrawImage(assets.ImageBank[assets.Worm].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), op)
} else {
f.Sprite.DrawImage(assets.ImageBank[assets.WormDamaged].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), op)
}
case gamedata.EnemyStateHit:
if (f.cycle/5)%2 == 0 && f.damage {
f.MaksDest.DrawImage(assets.ImageBank[assets.WormDamaged].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), op)
op := &ebiten.DrawImageOptions{}
op.GeoM.Reset()
op.Blend = ebiten.BlendSourceAtop
f.MaksDest.DrawImage(f.Maks, op)
f.Sprite.DrawImage(f.MaksDest, nil)
} else {
f.Sprite.DrawImage(assets.ImageBank[assets.WormDamaged].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), op)
}
case gamedata.EnemyStateExploding:
op.GeoM.Scale(2, 2)
//op.GeoM.Translate(-48, -48)
f.Sprite.DrawImage(assets.ImageBank[assets.FlyEyeDying].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), op)
if idx == 3 {
f.state = gamedata.EnemyStateDead
}
}
}
func (f *FlyGoblin) GetPosition() gamedata.Coordinates {
return f.position
}
func (f *FlyGoblin) SetPosition(pos gamedata.Coordinates) {
f.position = pos
}
func (f *FlyGoblin) SetTarget(target gamedata.Coordinates) {
f.target = target
}
func (f *FlyGoblin) GetSprite() *ebiten.Image {
return f.Sprite
}
func (f *FlyGoblin) GetEnemyState() gamedata.EnemyState {
return f.state
}
func (f *FlyGoblin) SetHit() {
f.health--
f.damage = true
f.state = gamedata.EnemyStateHit
if f.health <= 0 {
f.state = gamedata.EnemyStateExploding
f.cycle = 0
}
}
func (f *FlyGoblin) SetToggle() {
f.toggle = !f.toggle
if !f.toggle {
f.state = gamedata.EnemyStateDefault
}
}
func (f *FlyGoblin) IsToggled() bool {
return f.toggle
}
func (f *FlyGoblin) SetTouched() {
f.touched = true
}
func (f *FlyGoblin) ClearTouched() {
f.touched = false
}
func (f *FlyGoblin) IsTouched() bool {
return f.touched
}
func (f *FlyGoblin) ExplosionInitiated() bool {
return f.sploding
}
func (f *FlyGoblin) SetExplosionInitiated() {
f.sploding = true
}
func (f *FlyGoblin) Health() int {
return f.health
}
func (f *FlyGoblin) MaxHealth() int {
return FG_MAXHEALTH
}
func (f *FlyGoblin) SetDeathEvent(somefunc func()) {
f.deathcallback = somefunc
}
func (f *FlyGoblin) SetFireballCallback(somefunc func()) {
f.fireballcallback = somefunc
}
func (f *FlyGoblin) GetAngle() float64 {
return 0
}

View File

@@ -1,9 +1,9 @@
package main
package elements
import (
"bytes"
"image"
"log"
"mover/assets"
"mover/gamedata"
_ "embed"
"image/color"
@@ -12,11 +12,9 @@ import (
"github.com/hajimehoshi/ebiten/v2"
)
var (
heroImage *ebiten.Image
//go:embed hero.png
hero_img []byte
const (
MOVER_WIDTH = 48
MOVER_HEIGHT = 48
)
const (
@@ -30,41 +28,37 @@ const (
type HeroAction uint
func init() {
img, _, err := image.Decode(bytes.NewReader(hero_img))
if err != nil {
log.Fatal(err)
}
heroImage = ebiten.NewImageFromImage(img)
}
type Hero struct {
Sprite *ebiten.Image
Maks *ebiten.Image
MaksDest *ebiten.Image
Angle float64
Pos Coordinates
Origin Coordinates
Pos gamedata.Coordinates
Origin gamedata.Coordinates
Lastpos gamedata.Coordinates
Action HeroAction
cycles int
Upgrade bool
rotating bool
Toggled bool
Hit bool
Touched bool
Left bool
dyingcount int
}
func NewHero() *Hero {
m := &Hero{
Sprite: ebiten.NewImage(48, 48),
Maks: ebiten.NewImage(48, 48),
MaksDest: ebiten.NewImage(48, 48),
Sprite: ebiten.NewImage(MOVER_WIDTH, MOVER_HEIGHT),
Maks: ebiten.NewImage(MOVER_WIDTH, MOVER_HEIGHT),
MaksDest: ebiten.NewImage(MOVER_WIDTH, MOVER_HEIGHT),
Action: HeroActionDefault,
cycles: 4,
Angle: 0,
rotating: false,
Toggled: false,
dyingcount: 0,
Upgrade: true,
}
m.Maks.Fill(color.White)
@@ -79,7 +73,7 @@ func (m *Hero) SetAngle(a float64) {
m.Angle = a
}
func (m *Hero) SetOrigin(coords Coordinates) {
func (m *Hero) SetOrigin(coords gamedata.Coordinates) {
m.Origin = coords
m.Pos = coords
}
@@ -95,19 +89,42 @@ func (m *Hero) Draw() {
x0 := 48 * idx
x1 := x0 + 48
op := &ebiten.DrawImageOptions{}
if m.Left {
op.GeoM.Scale(-1, 1)
op.GeoM.Translate(MOVER_WIDTH, 0)
}
switch m.Action {
case HeroActionDefault:
m.Sprite.DrawImage(heroImage.SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), nil)
m.Sprite.DrawImage(assets.ImageBank[assets.HeroNormal].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), op)
case HeroActionDying:
m.Sprite.DrawImage(assets.ImageBank[assets.HeroDying].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), op)
if m.dyingcount >= 31 {
m.cycles = 0
m.Action++
}
case HeroActionExploding:
m.Sprite.DrawImage(assets.ImageBank[assets.HeroDying].SubImage(image.Rect(48*3, 0, 48*4, 48)).(*ebiten.Image), op)
default:
}
}
func (m *Hero) Update() {
if m.Action == HeroActionDying {
m.dyingcount++
}
m.cycles++
}
// one hit death for the hero
func (m *Hero) SetHit() {
m.Action++ // = (m.Action + 1) % HeroActionMax
m.Action = HeroActionDying
m.dyingcount = 0
m.Angle = 0
m.cycles = 0
}
func (m *Hero) ToggleColor() {

72
elements/laser.go Normal file
View File

@@ -0,0 +1,72 @@
package elements
import (
"image"
"mover/assets"
"mover/gamedata"
"github.com/hajimehoshi/ebiten/v2"
)
type Laser struct {
Sprite *ebiten.Image
position gamedata.Coordinates
angle float64
cycle int
firing bool
numcycles int
}
func NewLaser(pos gamedata.Coordinates, angle float64) *Laser {
l := &Laser{
Sprite: ebiten.NewImage(200, 20),
angle: angle,
cycle: 0,
position: pos,
firing: false,
numcycles: 5,
}
return l
}
func (l *Laser) Update() error {
l.cycle++
return nil
}
func (l *Laser) Draw() {
l.Sprite.Clear()
//l.Sprite.Fill(color.White)
idx := (l.cycle / 4) % l.numcycles
x0 := 0
y0 := 20 * idx
x1 := 200
y1 := y0 + 20
l.Sprite.DrawImage(assets.ImageBank[assets.LaserBeam].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), nil)
}
func (l *Laser) GetPosition() gamedata.Coordinates {
return l.position
}
func (l *Laser) SetPosition(pos gamedata.Coordinates) {
l.position = pos
}
func (l *Laser) GetAngle() float64 {
return l.angle
}
func (l *Laser) SetAngle(a float64) {
l.angle = a
}
func (l *Laser) SetFiring(b bool) {
l.firing = b
}
func (l *Laser) IsFiring() bool {
return l.firing
}

147
elements/mover.go Normal file
View File

@@ -0,0 +1,147 @@
package elements
import (
"image"
"mover/assets"
"mover/gamedata"
_ "embed"
"image/color"
_ "image/png"
"github.com/hajimehoshi/ebiten/v2"
)
const (
MoverActionDefault = iota
MoverActionDamaged
MoverActionDying
MoverActionExploding
MoverActionDead
MoverActionMax
)
type MoverAction uint
type Mover struct {
Sprite *ebiten.Image
Maks *ebiten.Image
MaksDest *ebiten.Image
Angle float64
Pos gamedata.Coordinates
Origin gamedata.Coordinates
Action MoverAction
cycles int
rotating bool
Toggled bool
Hit bool
Touched bool
SplodeInitiated bool
dyingcount int
}
func NewMover() *Mover {
m := &Mover{
Sprite: ebiten.NewImage(MOVER_WIDTH, MOVER_HEIGHT),
Maks: ebiten.NewImage(MOVER_WIDTH, MOVER_HEIGHT),
MaksDest: ebiten.NewImage(MOVER_WIDTH, MOVER_HEIGHT),
Action: MoverActionDefault,
cycles: 4,
Angle: 0,
rotating: false,
Toggled: false,
dyingcount: 0,
SplodeInitiated: false,
}
m.Maks.Fill(color.White)
return m
}
func (m *Mover) ToggleRotate() {
m.rotating = !m.rotating
}
func (m *Mover) SetAngle(a float64) {
m.Angle = a
}
func (m *Mover) SetOrigin(coords gamedata.Coordinates) {
m.Origin = coords
m.Pos = coords
}
func (m *Mover) Draw() {
m.Sprite.Clear()
m.MaksDest.Clear()
idx := (m.cycles / 8) % 4
y0 := 0
y1 := 48
x0 := 48 * idx
x1 := x0 + 48
switch m.Action {
case MoverActionDefault:
m.Sprite.DrawImage(assets.ImageBank[assets.FlyEyeNormal].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), nil)
case MoverActionDamaged:
m.Sprite.DrawImage(assets.ImageBank[assets.FlyEyeDamaged].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), nil)
case MoverActionDying:
if (m.cycles/5)%2 == 0 {
m.MaksDest.DrawImage(assets.ImageBank[assets.FlyEyeDamaged].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), nil)
op := &ebiten.DrawImageOptions{}
op.GeoM.Reset()
op.Blend = ebiten.BlendSourceAtop
m.MaksDest.DrawImage(m.Maks, op)
m.Sprite.DrawImage(m.MaksDest, nil)
} else {
m.Sprite.DrawImage(assets.ImageBank[assets.FlyEyeDamaged].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), nil)
}
if m.dyingcount >= 31 {
m.cycles = 0
m.SetHit()
}
case MoverActionExploding:
m.Sprite.DrawImage(assets.ImageBank[assets.FlyEyeDying].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), nil)
if idx == 3 {
m.SetHit()
}
default:
}
}
func (m *Mover) Update() error {
/*
dx := 0. //40 * math.Cos(float64(m.cycles)/16)
dy := 0. //40 * math.Sin(float64(m.cycles)/16)
m.Pos = Coordinates{X: m.Origin.X + dx, Y: m.Origin.Y + dy}
*/
/*
if m.rotating {
m.Angle = float64(m.cycles) / (math.Pi * 2)
}
*/
if m.Action == MoverActionDying {
m.dyingcount++
}
m.cycles++
return nil
}
func (m *Mover) SetHit() {
m.Action++ // = (m.Action + 1) % MoverActionMax
}
func (m *Mover) ToggleColor() {
//m.Toggled = !m.Toggled
if m.Action == MoverActionDefault {
m.Action = MoverActionDamaged
} else if m.Action == MoverActionDamaged {
m.Action = MoverActionDefault
}
}

View File

@@ -1,14 +1,17 @@
package main
package elements
import "math"
import (
"math"
"mover/gamedata"
)
type Projectile struct {
Pos Coordinates
Pos gamedata.Coordinates
Velocity float64
a float64
}
func NewProjectile(origin Coordinates, angle, velocity float64) *Projectile {
func NewProjectile(origin gamedata.Coordinates, angle, velocity float64) *Projectile {
return &Projectile{
Velocity: velocity,
a: angle,

46
elements/raindrop.go Normal file
View File

@@ -0,0 +1,46 @@
package elements
import (
"image/color"
"math/rand/v2"
"mover/gamedata"
"github.com/hajimehoshi/ebiten/v2"
)
type RainDrop struct {
Sprite *ebiten.Image
position gamedata.Coordinates
cycle int
}
func NewRainDrop() *RainDrop {
rd := &RainDrop{
Sprite: ebiten.NewImage(2, 10),
cycle: rand.IntN(30),
}
return rd
}
func (rd *RainDrop) Update() error {
rd.position.Y += 5
rd.cycle++
return nil
}
func (rd *RainDrop) Draw() {
rd.Sprite.Clear()
rd.Sprite.Fill(color.White)
}
func (rd *RainDrop) GetPosition() gamedata.Coordinates {
return rd.position
}
func (rd *RainDrop) SetPosition(pos gamedata.Coordinates) {
rd.position = pos
}
func (rd *RainDrop) Expired() bool {
return rd.cycle > 30
}

58
elements/rainsplash.go Normal file
View File

@@ -0,0 +1,58 @@
package elements
import (
"image"
"math/rand/v2"
"mover/assets"
"mover/gamedata"
"github.com/hajimehoshi/ebiten/v2"
)
type RainSplash struct {
Sprite *ebiten.Image
position gamedata.Coordinates
cycle int
counter int
}
func NewRainSplash() *RainSplash {
rd := &RainSplash{
Sprite: ebiten.NewImage(10, 4),
cycle: rand.IntN(4),
counter: 0,
}
return rd
}
func (rd *RainSplash) Update() error {
rd.counter++
rd.cycle++
return nil
}
func (rd *RainSplash) Draw() {
rd.Sprite.Clear()
//rd.Sprite.Fill(color.White)
idx := (rd.cycle / 8) % 4
x0 := idx * 10
y0 := 0
x1 := x0 + 10
y1 := 4
rd.Sprite.DrawImage(assets.ImageBank[assets.RainSplash].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), nil)
}
func (rd *RainSplash) GetPosition() gamedata.Coordinates {
return rd.position
}
func (rd *RainSplash) SetPosition(pos gamedata.Coordinates) {
rd.position = pos
}
func (rd *RainSplash) Expired() bool {
return rd.counter > 30
}

112
elements/splash.go Normal file
View File

@@ -0,0 +1,112 @@
package elements
import (
"math"
"mover/assets"
"mover/gamedata"
"github.com/hajimehoshi/ebiten/v2"
)
const (
SPLASH_DIM = 128
SPLASH_ELEMS = 10
SPLASH_PRIMARY_SIZE = 46
)
type Splash struct {
Sprite *ebiten.Image
position gamedata.Coordinates
cycle int
opacity float32
}
func NewSplash() *Splash {
sp := &Splash{
Sprite: ebiten.NewImage(SPLASH_DIM, SPLASH_DIM),
cycle: 0,
opacity: 1,
}
return sp
}
func (sp *Splash) Update() error {
sp.cycle++
sp.opacity = sp.opacity - float32(sp.cycle)/(60*60)
return nil
}
func (sp *Splash) Draw() {
sp.Sprite.Clear()
/*
for i := SPLASH_ELEMS; i > 0; i-- {
percent := float64(i) / SPLASH_ELEMS
dx := 1 / percent * math.Cos(float64(sp.cycle)/(math.Pi*2))
dy := -float64(i - SPLASH_ELEMS) //math.Sin(float64(sp.cycle) / (math.Pi * 2))
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(-SPLASH_DIM/2, -SPLASH_DIM/2)
op.GeoM.Scale(percent, percent)
//op.GeoM.Rotate(-(float64(sp.cycle - i*30)) / (math.Pi * 2))
op.GeoM.Translate(SPLASH_DIM/2+dx, SPLASH_DIM/2+dy)
//op.ColorScale.ScaleAlpha(float32(percent))
sp.Sprite.DrawImage(assets.ImageBank[assets.Splash], op)
}*/
for i := 0; i < SPLASH_ELEMS; i++ {
percent := float64(SPLASH_ELEMS-i) / SPLASH_ELEMS
dy := -float64(i)*4 - float64(sp.cycle)/60
dx := 2 / percent * math.Cos(float64(sp.cycle-i*10)/(math.Pi*2))
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(-48/2, -48/2)
op.GeoM.Scale(percent, percent)
op.GeoM.Rotate(-float64(sp.cycle) / (math.Pi * 4))
op.GeoM.Translate(SPLASH_DIM/2, SPLASH_DIM/2)
op.GeoM.Translate(dx, dy)
op.ColorScale.ScaleAlpha(sp.opacity)
sp.Sprite.DrawImage(assets.ImageBank[assets.Splash], op)
}
for i := 0; i < 5; i++ {
percent := float64(5-i) / 5
a := 9.8
time := float64(sp.cycle) / 8
v0 := 10.
dy := 1/2.*a*math.Pow(time-float64(i), 2) - v0*time
dx := -float64(sp.cycle)
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(-48/2, -48/2)
op.GeoM.Scale(percent, percent)
op.GeoM.Rotate(-float64(sp.cycle) / (math.Pi * 4))
op.GeoM.Translate(SPLASH_DIM/2, SPLASH_DIM/2)
op.GeoM.Translate(dx, dy)
sp.Sprite.DrawImage(assets.ImageBank[assets.Splash], op)
op.GeoM.Translate(-2*dx, 0)
sp.Sprite.DrawImage(assets.ImageBank[assets.Splash], op)
}
}
func (sp *Splash) GetPosition() gamedata.Coordinates {
return sp.position
}
func (sp *Splash) SetPosition(pos gamedata.Coordinates) {
sp.position = pos
}
func (sp *Splash) GetAlpha() float32 {
return sp.opacity
}

66
elements/weapondrop.go Normal file
View File

@@ -0,0 +1,66 @@
package elements
import (
"math"
"mover/assets"
"mover/gamedata"
"github.com/hajimehoshi/ebiten/v2"
)
type WeaponDrop struct {
Sprite *ebiten.Image
position gamedata.Coordinates
weapontype gamedata.WeaponType
cycle int
collected bool
}
func NewWeaponDrop(wt gamedata.WeaponType) *WeaponDrop {
wp := &WeaponDrop{
Sprite: ebiten.NewImage(32, 32),
weapontype: wt,
cycle: 0,
collected: false,
}
return wp
}
func (wp *WeaponDrop) SetPosition(pos gamedata.Coordinates) {
wp.position = pos
}
func (wp *WeaponDrop) GetPosition() gamedata.Coordinates {
return wp.position
}
func (wp *WeaponDrop) GetWeaponType() gamedata.WeaponType {
return wp.weapontype
}
func (wp *WeaponDrop) Update() error {
wp.cycle++
return nil
}
func (wp *WeaponDrop) Draw() {
wp.Sprite.Clear()
dy := 2 * math.Sin(float64(wp.cycle)/(math.Pi*2))
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(0, dy)
switch wp.weapontype {
case gamedata.WeaponTypeLaser:
wp.Sprite.DrawImage(assets.ImageBank[assets.ItemLaser], op)
}
}
func (wp *WeaponDrop) SetCollected(c bool) {
wp.collected = c
}
func (wp *WeaponDrop) IsCollected() bool {
return wp.collected
}

View File

@@ -16,6 +16,7 @@ const (
FontSizeStandard = 16
FontSizeLarge = 24
FontSizeBig = 60
FontSizeArcadeSmall = 8
FontSizeArcade = 12
FontSizeArcadeBig = 40
FontSizeArcadeHuge = 80
@@ -29,6 +30,7 @@ type FontStruct struct {
Arcade font.Face
ArcadeLarge font.Face
ArcadeHuge font.Face
ArcadeSmall font.Face
}
var (
@@ -82,4 +84,5 @@ func init() {
SurviveFont.Arcade = GetFaceFatal(fnt3, FontDPI, FontSizeArcade)
SurviveFont.ArcadeLarge = GetFaceFatal(fnt3, FontDPI, FontSizeArcadeBig)
SurviveFont.ArcadeHuge = GetFaceFatal(fnt3, FontDPI, FontSizeArcadeHuge)
SurviveFont.ArcadeSmall = GetFaceFatal(fnt3, FontDPI, FontSizeArcadeSmall)
}

449
game.go
View File

@@ -1,449 +0,0 @@
package main
import (
"bytes"
"fmt"
"image"
"image/color"
"log"
"math"
"math/rand/v2"
"mover/fonts"
_ "embed"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil"
"github.com/hajimehoshi/ebiten/v2/text"
"github.com/hajimehoshi/ebiten/v2/vector"
)
const (
MOVER_WIDTH = 48
MOVER_HEIGHT = 48
)
var (
tilesetImage *ebiten.Image
//go:embed grasstile.png
tileset_img []byte
)
type Game struct {
background *ebiten.Image
collisionMask *ebiten.Image
projectileMask *ebiten.Image
heroCollisionMask *ebiten.Image
heroCollisionCpy *ebiten.Image
Pos Coordinates
Paused bool
initialized bool
gameover bool
reset bool
runtime float64
hero *Hero
projectiles map[int]*Projectile
explosion *Explosion
score int
counter int
timer int
targets []*Mover
gamepadIDsBuf []ebiten.GamepadID
gamepadIDs map[ebiten.GamepadID]struct{}
//axes map[ebiten.GamepadID][]string
//pressedButtons map[ebiten.GamepadID][]string
}
func init() {
img, _, err := image.Decode(bytes.NewReader(tileset_img))
if err != nil {
log.Fatal(err)
}
tilesetImage = ebiten.NewImageFromImage(img)
}
func (g *Game) Initialize() {
origin := Coordinates{X: 640 / 2, Y: 480 / 2}
g.ConstructBackground()
g.hero = NewHero()
g.hero.SetOrigin(origin)
g.hero.ToggleRotate()
g.gameover = false
g.collisionMask = ebiten.NewImage(screenWidth, screenHeight)
g.projectileMask = ebiten.NewImage(screenWidth, screenHeight)
g.heroCollisionMask = ebiten.NewImage(MOVER_WIDTH, MOVER_HEIGHT)
g.heroCollisionCpy = ebiten.NewImage(MOVER_WIDTH, MOVER_HEIGHT)
g.explosion = NewExplosion()
g.explosion.SetOrigin(origin)
g.score = 0
g.reset = false
for j := 0; j < len(g.targets); j++ {
g.targets[j] = nil
}
g.targets = g.targets[:0]
g.score = 0
g.counter = 0
g.timer = 0
g.runtime = 0.
}
func (g *Game) Update() error {
if g.gamepadIDs == nil {
g.gamepadIDs = map[ebiten.GamepadID]struct{}{}
}
g.gamepadIDsBuf = inpututil.AppendJustConnectedGamepadIDs(g.gamepadIDsBuf[:0])
for _, id := range g.gamepadIDsBuf {
log.Printf("gamepad connected: id: %d, SDL ID: %s", id, ebiten.GamepadSDLID(id))
g.gamepadIDs[id] = struct{}{}
}
for id := range g.gamepadIDs {
if inpututil.IsGamepadJustDisconnected(id) {
log.Printf("gamepad disconnected: id: %d", id)
delete(g.gamepadIDs, id)
}
}
if !g.initialized || g.reset {
g.Initialize()
g.projectiles = make(map[int]*Projectile)
g.initialized = true
g.reset = false
} else {
g.StepGame()
}
g.timer++
return nil
}
func (g *Game) Draw(screen *ebiten.Image) {
screen.Clear()
screen.DrawImage(g.background, nil)
g.hero.Draw()
op := &ebiten.DrawImageOptions{}
if !g.gameover {
g.runtime = float64(g.counter) / 60.
}
s := fmt.Sprintf("%02.3f", g.runtime)
if !g.gameover {
text.Draw(screen, "TIME: "+s, fonts.SurviveFont.Arcade, 640/2-250, 25, color.White)
text.Draw(screen, fmt.Sprintf("SCORE: %d", g.score*10), fonts.SurviveFont.Arcade, 640/2+100, 25, color.White)
} else {
if (g.counter/30)%2 == 0 {
text.Draw(screen, "TIME: "+s, fonts.SurviveFont.Arcade, 640/2-250, 25, color.White)
text.Draw(screen, fmt.Sprintf("SCORE: %d", g.score*10), fonts.SurviveFont.Arcade, 640/2+100, 25, color.White)
}
}
op.GeoM.Translate(-MOVER_WIDTH/2, -MOVER_HEIGHT/2)
op.GeoM.Rotate(g.hero.Angle)
op.GeoM.Translate(g.hero.Pos.X, g.hero.Pos.Y)
screen.DrawImage(g.hero.Sprite, op)
for _, target := range g.targets {
target.Draw()
op.GeoM.Reset()
op.GeoM.Translate(-MOVER_WIDTH/2, -MOVER_HEIGHT/2)
op.GeoM.Rotate(target.Angle)
op.GeoM.Translate(target.Pos.X, target.Pos.Y)
screen.DrawImage(target.Sprite, op)
}
g.projectileMask.Clear()
//ebitenutil.DrawCircle()
for _, p := range g.projectiles {
//vector.DrawFilledCircle(screen, float32(p.Pos.X), float32(p.Pos.Y), 3, color.White, true)
vector.DrawFilledCircle(g.projectileMask, float32(p.Pos.X), float32(p.Pos.Y), 3, color.White, true)
}
screen.DrawImage(g.projectileMask, nil)
vector.StrokeCircle(screen, float32(g.explosion.Origin.X), float32(g.explosion.Origin.Y), float32(g.explosion.Radius), 3, color.White, true)
/*for _, gamepad ebiten.StandardGamepadAxisValue(id, ebiten.StandardGamepadAxisRightStickHorizontal),
ebiten.StandardGamepadAxisValue(id, ebiten.StandardGamepadAxisRightStickVertical))*/
}
func (g *Game) Layout(width, height int) (int, int) {
return screenWidth, screenHeight
}
func (g *Game) CleanupTargets() {
// remove dead targets by 1) iterating over all targets
i := 0
for _, target := range g.targets {
//moving valid targets to the front of the slice
if target.Action < MoverActionDead {
g.targets[i] = target
i++
}
}
//then culling the last elements of the slice
if len(g.targets)-i > 0 {
// fmt.Printf("Removing %d elements\n", len(g.targets)-i)
g.score += len(g.targets) - i
}
for j := i; j < len(g.targets); j++ {
g.targets[j] = nil
}
g.targets = g.targets[:i]
}
func (g *Game) StepGame() {
g.HandleInput()
if !g.Paused {
g.UpdateHeroPosition()
g.hero.Update()
g.explosion.Update()
g.UpdateTargets()
g.UpdateProjectiles()
if !g.gameover {
//append new projectiles
g.AppendProjectiles()
//add new target with increasing frequency
g.AddNewTargets()
//handle pulsewave updates
g.HandlePulseWaveUpdate()
}
g.CleanupTargets()
g.counter++
}
}
func (g *Game) AddNewTargets() {
f := 40000 / (g.counter + 1)
if g.counter%f == 0 {
g.targets = append(g.targets, NewMover())
g.targets[len(g.targets)-1].SetOrigin(Coordinates{X: rand.Float64() * 640, Y: rand.Float64() * 480})
}
}
func (g *Game) HandlePulseWaveUpdate() {
if g.explosion.Active {
if g.explosion.Radius > math.Sqrt(640*640+480*480) {
g.explosion.ToggleActivate()
g.explosion.Reset()
g.ResetTargetTouches()
}
//check collisions
for _, target := range g.targets {
dx := target.Pos.X - g.hero.Pos.X
dy := target.Pos.Y - g.hero.Pos.Y
r := math.Sqrt(dx*dx + dy*dy)
if r >= g.explosion.Radius-5 && r <= g.explosion.Radius+5 && target.Action <= MoverActionDamaged && !target.Touched {
target.ToggleColor()
target.Touched = true
//target.SetHit()
}
}
}
}
func (g *Game) UpdateProjectiles() {
for k, p := range g.projectiles {
//for i := 0; i < len(g.projectiles); i++ {
// g.projectiles[i].Update()
p.Update()
//if g.projectiles[i].Pos.X < 5 || g.projectiles[i].Pos.X > 635 || g.projectiles[i].Pos.Y < 5 || g.projectiles[i].Pos.Y > 475 {
if p.Pos.X < 5 || p.Pos.X > 635 || p.Pos.Y < 5 || p.Pos.Y > 475 {
p.Velocity = 0
delete(g.projectiles, k)
}
//compute projectile collisions
for _, target := range g.targets {
//first, boundary check
if p.Pos.X >= target.Pos.X-MOVER_WIDTH/2 && p.Pos.X <= target.Pos.X+MOVER_WIDTH/2 && p.Pos.Y >= target.Pos.Y-MOVER_HEIGHT/2 && p.Pos.Y <= target.Pos.Y+MOVER_HEIGHT/2 && target.Action == MoverActionDamaged {
//fmt.Println("potential collision")
//the following computes total collisions in the image using a projectile mask that is a duplicate of what is on screen
//there's definitely room for optimization here
g.collisionMask.Clear()
g.collisionMask.DrawImage(g.projectileMask, nil)
op := &ebiten.DrawImageOptions{}
op.GeoM.Reset()
op.Blend = ebiten.BlendSourceIn
op.GeoM.Translate(target.Pos.X-MOVER_WIDTH/2, target.Pos.Y-MOVER_HEIGHT/2)
g.collisionMask.DrawImage(target.Sprite, op)
//var pixels []byte = make([]byte, MOVER_WIDTH*MOVER_HEIGHT*4)
var pixels []byte = make([]byte, screenWidth*screenHeight*4)
g.collisionMask.ReadPixels(pixels)
for i := 0; i < len(pixels); i = i + 4 {
if pixels[i+3] != 0 {
//fmt.Println("pixel collision")
delete(g.projectiles, k)
//target.ToggleColor()
target.SetHit()
//target.SetOrigin(Coordinates{X: rand.Float64() * 640, Y: rand.Float64() * 480})
target.Hit = true
break
}
}
}
}
}
}
func (g *Game) UpdateTargets() {
for _, target := range g.targets {
if !target.Hit && g.hero.Action < MoverActionDying {
dx := g.hero.Pos.X - target.Pos.X
dy := g.hero.Pos.Y - target.Pos.Y
angle := math.Atan2(dy, dx)
maxspeed := 3.
target.Pos.X += maxspeed * math.Cos(angle)
target.Pos.Y += maxspeed * math.Sin(angle)
}
//compute collision with hero
if g.hero.Pos.X >= target.Pos.X-MOVER_WIDTH/2 && g.hero.Pos.X <= target.Pos.X+MOVER_WIDTH/2 && g.hero.Pos.Y >= target.Pos.Y-MOVER_HEIGHT/2 && g.hero.Pos.Y <= target.Pos.Y+MOVER_HEIGHT/2 && target.Action < MoverActionDying {
g.heroCollisionMask.Clear()
g.heroCollisionMask.DrawImage(g.hero.Sprite, nil)
op := &ebiten.DrawImageOptions{}
op.GeoM.Reset()
op.Blend = ebiten.BlendSourceIn
op.GeoM.Translate((g.hero.Pos.X-target.Pos.X)-MOVER_WIDTH/2, (g.hero.Pos.Y-target.Pos.Y)-MOVER_HEIGHT/2)
g.heroCollisionMask.DrawImage(target.Sprite, op)
//var pixels []byte = make([]byte, MOVER_WIDTH*MOVER_HEIGHT*4)
var pixels []byte = make([]byte, MOVER_HEIGHT*MOVER_HEIGHT*4)
g.heroCollisionMask.ReadPixels(pixels)
for i := 0; i < len(pixels); i = i + 4 {
if pixels[i+3] != 0 {
fmt.Println("pixel death")
g.hero.Action = MoverActionDying
g.gameover = true
break
}
}
}
target.Update()
}
}
func (g *Game) ResetTargetTouches() {
for _, t := range g.targets {
t.Touched = false
}
}
func (g *Game) AppendProjectiles() {
if g.counter%14 == 0 && ebiten.IsGamepadButtonPressed(0, ebiten.GamepadButton7) {
g.projectiles[g.counter] = NewProjectile(Coordinates{X: g.hero.Pos.X, Y: g.hero.Pos.Y}, g.hero.Angle, 5.)
g.projectiles[g.counter+1] = NewProjectile(Coordinates{X: g.hero.Pos.X, Y: g.hero.Pos.Y}, g.hero.Angle+math.Pi, 5.)
}
}
func (g *Game) HandleInput() {
if len(g.gamepadIDs) > 0 {
if ebiten.IsGamepadButtonPressed(0, ebiten.GamepadButton11) {
if !g.explosion.Active {
g.explosion.SetOrigin(g.hero.Pos)
g.explosion.Reset()
g.explosion.ToggleActivate()
}
}
if inpututil.IsGamepadButtonJustPressed(0, ebiten.GamepadButton9) {
if g.gameover {
g.reset = true
} else {
g.Paused = !g.Paused
}
}
//account for controller sensitivity
xaxis := ebiten.StandardGamepadAxisValue(0, ebiten.StandardGamepadAxisRightStickHorizontal)
yaxis := ebiten.StandardGamepadAxisValue(0, ebiten.StandardGamepadAxisRightStickVertical)
if yaxis <= 0.09 && yaxis >= -0.09 {
yaxis = 0
}
if xaxis <= 0.09 && xaxis >= -0.09 {
xaxis = 0
}
inputangle := math.Atan2(yaxis, xaxis)
g.hero.SetAngle(inputangle)
}
}
func (g *Game) UpdateHeroPosition() {
//handle gamepad input
inpx := ebiten.GamepadAxisValue(0, 0)
inpy := ebiten.GamepadAxisValue(0, 1)
if inpx >= 0.15 || inpx <= -0.15 {
g.hero.Pos.X += ebiten.GamepadAxisValue(0, 0) * 5
}
if inpy >= 0.15 || inpy <= -0.15 {
g.hero.Pos.Y += ebiten.GamepadAxisValue(0, 1) * 5
}
}
func (g *Game) ConstructBackground() {
g.background = ebiten.NewImage(screenWidth, screenHeight)
for i := 0; i < 640/16; i++ {
for j := 0; j < 480/16; j++ {
//select random tile in x and y from tileset
idx_x := rand.IntN(256 / 16)
idx_y := rand.IntN(256 / 16)
x0 := 16 * idx_x
y0 := 16 * idx_y
x1 := x0 + 16
y1 := y0 + 16
//translate for grid element we're painting
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(float64(i)*16, float64(j)*16)
g.background.DrawImage(tilesetImage.SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), op)
}
}
}

View File

@@ -1,4 +1,4 @@
package main
package gamedata
type Coordinates struct {
X float64

12
gamedata/enemystates.go Normal file
View File

@@ -0,0 +1,12 @@
package gamedata
type EnemyState int
const (
EnemyStateDefault = iota
EnemyStateHit
EnemyStateDying
EnemyStateExploding
EnemyStateDead
EnemyStateMax
)

12
gamedata/gameevents.go Normal file
View File

@@ -0,0 +1,12 @@
package gamedata
type GameEvent int
const (
GameEventPlayerDeath = iota
GameEventCharge
GameEventNewShot
GameEventTargetHit
GameEventExplosion
GameEventFireball
)

16
gamedata/gameinfo.go Normal file
View File

@@ -0,0 +1,16 @@
package gamedata
type Area struct {
Width int
Height int
}
func (a *Area) Area() int {
return a.Height * a.Width
}
type GameInfo struct {
Name string
Version string
Dimensions Area
}

13
gamedata/gameinputs.go Normal file
View File

@@ -0,0 +1,13 @@
package gamedata
type GameInputs struct {
XAxis float64
YAxis float64
ShotAngle float64
Shot bool
Start bool
Charge bool
Quit bool
Reset bool
CycleWeapon bool
}

9
gamedata/weapontype.go Normal file
View File

@@ -0,0 +1,9 @@
package gamedata
type WeaponType int
const (
WeaponTypeGun = iota
WeaponTypeLaser
WeaponTypeMax
)

83
gameelement/background.go Normal file
View File

@@ -0,0 +1,83 @@
package gameelement
import (
"image"
"math/rand/v2"
"mover/assets"
"mover/gamedata"
"github.com/hajimehoshi/ebiten/v2"
)
type Background struct {
Sprite *ebiten.Image
initialized bool
}
func NewBackground(a gamedata.Area) *Background {
b := &Background{
Sprite: ebiten.NewImage(a.Width, a.Height),
initialized: false,
}
return b
}
func (b *Background) SetInputs(gamedata.GameInputs) {
}
func (b *Background) Update() error {
if !b.initialized {
b.Initialize()
} else {
}
return nil
}
func (b *Background) Draw(drawimg *ebiten.Image) {
//all the stuff before
op := &ebiten.DrawImageOptions{}
drawimg.DrawImage(b.Sprite, op)
}
func (b *Background) Initialize() {
b.ConstructBackground()
b.initialized = true
}
func (b *Background) ConstructBackground() {
BLOCK_SIZE := 32
for i := 0; i < b.Sprite.Bounds().Dx()/BLOCK_SIZE; i++ {
for j := 0; j < b.Sprite.Bounds().Dy()/BLOCK_SIZE; j++ {
//select random tile in x and y from tileset
idx_y := rand.IntN(256 / BLOCK_SIZE)
idx_x := rand.IntN(256 / BLOCK_SIZE)
x0 := BLOCK_SIZE * idx_x
y0 := BLOCK_SIZE * idx_y
x1 := x0 + BLOCK_SIZE
y1 := y0 + BLOCK_SIZE
//translate for grid element we're painting
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(float64(i*BLOCK_SIZE), float64(j*BLOCK_SIZE))
b.Sprite.DrawImage(assets.ImageBank[assets.TileSet].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), op)
}
}
ax := float64(rand.IntN(b.Sprite.Bounds().Dx()/BLOCK_SIZE) * BLOCK_SIZE)
ay := float64(rand.IntN(b.Sprite.Bounds().Dy()/BLOCK_SIZE) * BLOCK_SIZE)
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(ax, ay)
b.Sprite.DrawImage(assets.ImageBank[assets.Altar], op)
}
func (b *Background) RegisterEvents(e gamedata.GameEvent, f func()) {
}

988
gameelement/canvas.go Normal file
View File

@@ -0,0 +1,988 @@
package gameelement
import (
"fmt"
"image"
"image/color"
"math"
"math/rand/v2"
"mover/assets"
"mover/elements"
"mover/fonts"
"mover/gamedata"
"mover/weapons"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/text"
"github.com/hajimehoshi/ebiten/v2/vector"
)
type Canvas struct {
Sprite *ebiten.Image
collisionMask *ebiten.Image
projectileMask *ebiten.Image
laserMask *ebiten.Image
heroCollisionMask *ebiten.Image
heroCollisionCpy *ebiten.Image
eventmap map[gamedata.GameEvent]func()
initialized bool
goblinspawned bool
goblindead bool
lastInputs gamedata.GameInputs
runtime float64
counter int
score int
splashes []*elements.Splash
wpdrops []*elements.WeaponDrop
hero *elements.Hero
charge *elements.Explosion
goblin *elements.FlyGoblin
enemies []elements.Enemies
projectiles []*elements.Projectile
laser *elements.Laser
gameover bool
lasercoords []gamedata.Coordinates
holster *weapons.Holster
}
func NewCanvas(a gamedata.Area) *Canvas {
c := &Canvas{
Sprite: ebiten.NewImage(a.Width, a.Height),
projectileMask: ebiten.NewImage(a.Width, a.Height),
collisionMask: ebiten.NewImage(a.Width, a.Height),
laserMask: ebiten.NewImage(a.Width, a.Height),
heroCollisionMask: ebiten.NewImage(48, 48),
heroCollisionCpy: ebiten.NewImage(48, 48),
hero: elements.NewHero(),
charge: elements.NewExplosion(),
laser: elements.NewLaser(gamedata.Coordinates{X: 320, Y: 240}, 0),
initialized: false,
gameover: false,
goblinspawned: false,
goblindead: false,
score: 0,
runtime: 0.,
counter: 0,
holster: weapons.NewHolster(),
}
c.laserMask.Clear()
c.eventmap = make(map[gamedata.GameEvent]func())
c.lasercoords = make([]gamedata.Coordinates, 4)
return c
}
func (c *Canvas) SetInputs(gi gamedata.GameInputs) {
c.lastInputs = gi
}
func (c *Canvas) Update() error {
if !c.initialized {
c.Initialize()
} else {
c.UpdateHero()
c.UpdateWeaponDrops()
c.UpdateWeapons()
c.UpdateProjectiles()
c.UpdateCharge()
c.UpdateEnemies()
c.SpawnEnemies()
c.CleanupTargets()
c.UpdateSplashes()
c.CleanSplashes()
c.CleanupDrops()
c.counter++
}
return nil
}
func (c *Canvas) Draw(drawimg *ebiten.Image) {
c.Sprite.Clear()
c.projectileMask.Clear()
//c.laserMask.Clear()
//vector.DrawFilledCircle(c.Sprite, float32(c.hero.Pos.X), float32(c.hero.Pos.Y), 100, color.White, true)
//render heor
c.hero.Draw()
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(c.hero.Pos.X-48/2, c.hero.Pos.Y-48/2)
c.Sprite.DrawImage(c.hero.Sprite, op)
//render weapon
if !c.gameover {
op.GeoM.Reset()
op.GeoM.Translate(0, -16)
op.GeoM.Rotate(c.lastInputs.ShotAngle)
op.GeoM.Translate(c.hero.Pos.X, c.hero.Pos.Y)
c.Sprite.DrawImage(assets.ImageBank[assets.Weapon], op)
}
//draw enemy shadows
for _, es := range c.enemies {
if es.GetEnemyState() < gamedata.EnemyStateExploding {
dx := float64(assets.ImageBank[assets.FlyEyeShadow].Bounds().Dx()) / 2
dy := float64(assets.ImageBank[assets.FlyEyeShadow].Bounds().Dy()) / 2
sx := float64(es.GetSprite().Bounds().Dx()) / 48
sy := float64(es.GetSprite().Bounds().Dy()) / 48
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(-dx, -dy)
op.GeoM.Scale(sx, sy)
op.GeoM.Translate(es.GetPosition().X, es.GetPosition().Y+float64(es.GetSprite().Bounds().Dx())/2)
c.Sprite.DrawImage(assets.ImageBank[assets.FlyEyeShadow], op)
}
}
//draw weapon drops
for _, drop := range c.wpdrops {
drop.Draw()
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(drop.GetPosition().X-float64(drop.Sprite.Bounds().Dx())/2, drop.GetPosition().Y-float64(drop.Sprite.Bounds().Dy())/2)
c.Sprite.DrawImage(drop.Sprite, op)
}
//draw enemies
for _, e := range c.enemies {
e.Draw()
xshift := float64(e.GetSprite().Bounds().Dx() / 2)
yshift := float64(e.GetSprite().Bounds().Dy() / 2)
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(-xshift, -yshift)
op.GeoM.Rotate(e.GetAngle())
op.GeoM.Translate(e.GetPosition().X, e.GetPosition().Y)
c.Sprite.DrawImage(e.GetSprite(), op)
//do we need a health bar for this enemy?
if e.Health() > 0 {
hbWidth := float64(e.MaxHealth())*2 + 4
p1 := float64(e.GetSprite().Bounds().Dx())
p0 := e.GetPosition().X - p1/2
x0 := p0 - (hbWidth-p1)/2
y0 := e.GetPosition().Y - 2/3.*float64(e.GetSprite().Bounds().Dy())
vector.DrawFilledRect(c.Sprite, float32(x0), float32(y0), float32(e.MaxHealth())*2+4, 12, color.Black, true)
vector.DrawFilledRect(c.Sprite, float32(x0+2), float32(y0+2), float32(e.Health())*2, 8, color.RGBA{R: 0xff, G: 0x00, B: 0x00, A: 0xff}, true)
}
}
//draw projectiles
for _, p := range c.projectiles {
vector.DrawFilledCircle(c.projectileMask, float32(p.Pos.X), float32(p.Pos.Y), 3, color.White, true)
}
c.Sprite.DrawImage(c.projectileMask, nil)
//draw laser(s)
if c.laser.IsFiring() {
c.laser.Draw()
c.Sprite.DrawImage(c.laserMask, nil)
}
//c.Sprite.DrawImage(c.laser.Sprite, op)
vector.StrokeCircle(c.Sprite, float32(c.charge.Origin.X), float32(c.charge.Origin.Y), float32(c.charge.Radius), 3, color.White, true)
//TEMPORARY let's see how far off the beam we are
//vector.StrokeLine(c.Sprite, float32(c.lasercoords[2].X), float32(c.lasercoords[2].Y), float32(c.lasercoords[3].X), float32(c.lasercoords[3].Y), 2, color.White, true)
//let's render our laser 'splashes'
for _, sp := range c.splashes {
sp.Draw()
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(sp.GetPosition().X-128/2, sp.GetPosition().Y-128/2)
c.Sprite.DrawImage(sp.Sprite, op)
}
if !c.gameover {
c.runtime = float64(c.counter) / 60.
}
s := fmt.Sprintf("%02.3f", c.runtime)
if !c.gameover {
text.Draw(c.Sprite, "TIME: "+s, fonts.SurviveFont.Arcade, 640/2-250, 25, color.White)
text.Draw(c.Sprite, fmt.Sprintf("SCORE: %d", c.score*10), fonts.SurviveFont.Arcade, 640/2+100, 25, color.White)
} else {
if (c.counter/30)%2 == 0 {
text.Draw(c.Sprite, "TIME: "+s, fonts.SurviveFont.Arcade, 640/2-250, 25, color.White)
text.Draw(c.Sprite, fmt.Sprintf("SCORE: %d", c.score*10), fonts.SurviveFont.Arcade, 640/2+100, 25, color.White)
}
text.Draw(c.Sprite, "PRESS START TO TRY AGAIN", fonts.SurviveFont.Arcade, 640/2-150, 480/2, color.White)
}
op.GeoM.Reset()
drawimg.DrawImage(c.Sprite, op)
}
func (c *Canvas) Initialize() {
c.InitializeHero()
c.CleanSplashes()
c.ResetWeaponDrops()
c.enemies = c.enemies[:0]
c.gameover = false
c.initialized = true
c.score = 0
c.counter = 0
c.runtime = 0.
c.goblinspawned = false
c.goblindead = false
//temporary
c.hero.Action = elements.HeroActionDefault
c.holster.SetActiveWeapon(gamedata.WeaponTypeGun)
c.laser.SetFiring(false)
}
func (c *Canvas) UpdateHero() {
c.hero.Update()
if !c.gameover {
c.UpdateHeroPosition()
c.ComputeHeroCollisions()
}
}
func (c *Canvas) UpdateHeroPosition() {
if c.lastInputs.XAxis >= 0.15 || c.lastInputs.XAxis <= -0.15 {
c.hero.Left = c.lastInputs.XAxis < 0
c.hero.Pos.X += c.lastInputs.XAxis * 5
}
if c.lastInputs.YAxis >= 0.15 || c.lastInputs.YAxis <= -0.15 {
c.hero.Pos.Y += c.lastInputs.YAxis * 5
}
}
func (c *Canvas) ComputeHeroCollisions() {
for _, e := range c.enemies {
//compute collision with hero
if c.hero.Pos.X >= e.GetPosition().X-float64(e.GetSprite().Bounds().Dx())/2 && c.hero.Pos.X <= e.GetPosition().X+float64(e.GetSprite().Bounds().Dx())/2 &&
c.hero.Pos.Y >= e.GetPosition().Y-float64(e.GetSprite().Bounds().Dy())/2 && c.hero.Pos.Y <= e.GetPosition().Y+float64(e.GetSprite().Bounds().Dy())/2 &&
e.GetEnemyState() < gamedata.EnemyStateDying {
// target.Action < elements.MoverActionDying && g.hero.Action < elements.HeroActionDying {
c.heroCollisionMask.Clear()
c.heroCollisionMask.DrawImage(c.hero.Sprite, nil)
op := &ebiten.DrawImageOptions{}
op.GeoM.Reset()
op.Blend = ebiten.BlendSourceIn
op.GeoM.Translate((c.hero.Pos.X-e.GetPosition().X)-float64(e.GetSprite().Bounds().Dx())/2, (c.hero.Pos.Y-e.GetPosition().Y)-float64(e.GetSprite().Bounds().Dy())/2)
c.heroCollisionMask.DrawImage(e.GetSprite(), op)
if c.HasCollided(c.heroCollisionMask, 48*48*4) {
c.hero.SetHit()
c.gameover = true
if c.eventmap[gamedata.GameEventPlayerDeath] != nil {
c.eventmap[gamedata.GameEventPlayerDeath]()
}
}
}
}
}
func (c *Canvas) AddProjectiles() {
//add new projectiles
if c.lastInputs.Shot && c.counter%14 == 0 {
loc := gamedata.Coordinates{
X: c.hero.Pos.X,
Y: c.hero.Pos.Y,
}
angle := c.lastInputs.ShotAngle
velocity := 5.
c.projectiles = append(c.projectiles, elements.NewProjectile(loc, angle, velocity))
if c.hero.Upgrade {
c.projectiles = append(c.projectiles, elements.NewProjectile(loc, angle+math.Pi, velocity))
}
if c.eventmap[gamedata.GameEventNewShot] != nil {
c.eventmap[gamedata.GameEventNewShot]()
}
}
}
func (c *Canvas) InitializeHero() {
//recenter the hero
pos := gamedata.Coordinates{
X: float64(c.Sprite.Bounds().Dx() / 2),
Y: float64(c.Sprite.Bounds().Dy() / 2),
}
c.hero.SetOrigin(pos)
}
func (c *Canvas) UpdateProjectiles() {
i := 0
for _, p := range c.projectiles {
p.Update()
projectilevalid := true
if p.Pos.X < -640/2 || p.Pos.X > 1.5*640 || p.Pos.Y < -480/2 || p.Pos.Y > 1.5*480 {
projectilevalid = false
}
for _, e := range c.enemies {
if p.Pos.X >= e.GetPosition().X-float64(e.GetSprite().Bounds().Dx())/2 && p.Pos.X <= e.GetPosition().X+float64(e.GetSprite().Bounds().Dx())/2 &&
p.Pos.Y >= e.GetPosition().Y-float64(e.GetSprite().Bounds().Dy())/2 && p.Pos.Y <= e.GetPosition().Y+float64(e.GetSprite().Bounds().Dy())/2 &&
e.IsToggled() && e.GetEnemyState() < gamedata.EnemyStateDying {
c.collisionMask.Clear()
c.collisionMask.DrawImage(c.projectileMask, nil)
op := &ebiten.DrawImageOptions{}
op.GeoM.Reset()
op.Blend = ebiten.BlendSourceIn
op.GeoM.Translate(e.GetPosition().X-float64(e.GetSprite().Bounds().Dx())/2, e.GetPosition().Y-float64(e.GetSprite().Bounds().Dy())/2)
c.collisionMask.DrawImage(e.GetSprite(), op)
if c.HasCollided(c.collisionMask, 640*480*4) {
projectilevalid = false
e.SetHit()
if c.eventmap[gamedata.GameEventTargetHit] != nil {
c.eventmap[gamedata.GameEventTargetHit]()
}
}
}
}
if projectilevalid {
c.projectiles[i] = p
i++
}
}
for j := i; j < len(c.projectiles); j++ {
c.projectiles[j] = nil
}
c.projectiles = c.projectiles[:i]
}
func (c *Canvas) UpdateLaser() {
c.laser.Update()
c.laser.SetFiring(c.lastInputs.Shot)
if c.lastInputs.Shot {
c.laser.SetPosition(c.hero.Pos)
c.laser.SetAngle(c.lastInputs.ShotAngle)
c.laserMask.Clear()
lpos := c.laser.GetPosition()
op := &ebiten.DrawImageOptions{}
op.GeoM.Reset()
//op.GeoM.Translate(-float64(c.laser.Sprite.Bounds().Dx())/2, -float64(c.laser.Sprite.Bounds().Dy())/2)
op.GeoM.Translate(0, -float64(c.laser.Sprite.Bounds().Dy())/2)
op.GeoM.Rotate(c.laser.GetAngle())
op.GeoM.Translate(lpos.X, lpos.Y)
c.laserMask.DrawImage(c.laser.Sprite, op)
//c.LaserAttempt1()
//c.LaserAttempt2()
//c.LaserAttempt3()
c.LaserAttempt4()
}
}
func (c *Canvas) UpdateCharge() {
if c.lastInputs.Charge && !c.charge.Active && !c.gameover {
c.charge.SetOrigin(c.hero.Pos)
c.charge.Reset()
c.charge.ToggleActivate()
if c.eventmap[gamedata.GameEventCharge] != nil {
c.eventmap[gamedata.GameEventCharge]()
}
}
c.charge.Update()
if c.charge.Active {
if c.charge.Radius > math.Sqrt(640*640+480*480) {
c.charge.ToggleActivate()
c.charge.Reset()
c.ResetTargetTouches()
}
for _, e := range c.enemies {
dx := e.GetPosition().X - c.hero.Pos.X
dy := e.GetPosition().Y - c.hero.Pos.Y
r := math.Sqrt(dx*dx + dy*dy)
if r >= c.charge.Radius-5 && r <= c.charge.Radius+5 &&
!e.IsTouched() && e.GetEnemyState() <= gamedata.EnemyStateHit {
e.SetToggle()
e.SetTouched()
}
}
}
}
func (c *Canvas) ResetTargetTouches() {
for _, e := range c.enemies {
e.ClearTouched()
}
}
func (c *Canvas) UpdateEnemies() {
//update existing enemies
for _, e := range c.enemies {
if !c.gameover {
e.SetTarget(c.hero.Pos)
if e.GetEnemyState() == gamedata.EnemyStateExploding && !e.ExplosionInitiated() {
if c.eventmap[gamedata.GameEventExplosion] != nil {
c.eventmap[gamedata.GameEventExplosion]()
}
e.SetExplosionInitiated()
}
} else {
e.SetTarget(e.GetPosition())
}
e.Update()
}
}
func (c *Canvas) SpawnEnemies() {
if !c.gameover {
if !c.goblinspawned || c.goblindead {
c.SpawnFlyEyes()
}
if !c.goblinspawned && c.counter > 2400 && !c.goblindead {
c.SpawnGoblin()
}
}
}
func (c *Canvas) SpawnFlyEyes() {
//spawn new enemies
f := 40000 / (c.counter + 1)
if c.counter%f == 0 {
newenemy := elements.NewFlyEye()
x0 := rand.Float64() * 640
y0 := rand.Float64() * 480
quadrant := rand.IntN(3)
switch quadrant {
case 0:
newenemy.SetPosition(gamedata.Coordinates{X: x0, Y: -48})
case 1:
newenemy.SetPosition(gamedata.Coordinates{X: x0, Y: 480 + 48})
case 2:
newenemy.SetPosition(gamedata.Coordinates{X: -48, Y: y0})
case 3:
newenemy.SetPosition(gamedata.Coordinates{X: 640 + x0, Y: y0})
}
newenemy.SetTarget(c.hero.Pos)
c.enemies = append(c.enemies, newenemy)
}
}
func (c *Canvas) SpawnGoblin() {
newfg := elements.NewFlyGoblin()
newfg.SetDeathEvent(c.GoblinDeathEvent)
newfg.SetFireballCallback(c.GoblinFireballEvent)
x0 := rand.Float64() * 640
y0 := rand.Float64() * 480
quadrant := rand.IntN(3)
switch quadrant {
case 0:
newfg.SetPosition(gamedata.Coordinates{X: x0, Y: -96})
case 1:
newfg.SetPosition(gamedata.Coordinates{X: x0, Y: 480 + 48})
case 2:
newfg.SetPosition(gamedata.Coordinates{X: -96, Y: y0})
case 3:
newfg.SetPosition(gamedata.Coordinates{X: 640 + x0, Y: y0})
}
c.goblin = newfg
c.enemies = append(c.enemies, newfg)
c.goblinspawned = true
}
func (c *Canvas) HasCollided(mask *ebiten.Image, size int) bool {
var result bool = false
var pixels []byte = make([]byte, size)
mask.ReadPixels(pixels)
for i := 0; i < len(pixels); i = i + 4 {
if pixels[i+3] != 0 {
result = true
break
}
}
return result
}
func (c *Canvas) RegisterEvents(e gamedata.GameEvent, f func()) {
c.eventmap[e] = f
}
func (c *Canvas) CleanupTargets() {
// remove dead targets by iterating over all targets
i := 0
for _, e := range c.enemies {
//compute odds for dropping an item on dead enemies
if e.GetEnemyState() == elements.MoverActionDead {
if rand.Float64() > 0.98 {
drop := elements.NewWeaponDrop(gamedata.WeaponTypeLaser)
drop.SetPosition(e.GetPosition())
c.wpdrops = append(c.wpdrops, drop)
}
}
//moving valid targets to the front of the slice
if e.GetEnemyState() < elements.MoverActionDead &&
!(e.GetPosition().X < -640*2 || e.GetPosition().X > 640*2 ||
e.GetPosition().Y > 480*2 || e.GetPosition().Y < -480*2) {
c.enemies[i] = e
i++
}
}
//then culling the last elements of the slice, and conveniently we can update
//our base score with the number of elements removed (bonuses calculated elsewhere)
if len(c.enemies)-i > 0 {
c.score += len(c.enemies) - i
}
for j := i; j < len(c.enemies); j++ {
c.enemies[j] = nil
}
c.enemies = c.enemies[:i]
}
func (c *Canvas) GoblinDeathEvent() {
c.goblindead = true
c.goblinspawned = false
c.score += 10
}
func (c *Canvas) GoblinFireballEvent() {
if !c.gameover {
velocity := 8.
dx := c.hero.Pos.X - c.goblin.GetPosition().X
dy := c.hero.Pos.Y - c.goblin.GetPosition().Y
angle := math.Atan2(dy, dx)
//add some randomness to the angle
arand := rand.Float64() * math.Pi / 3
newfb := elements.NewFireBall(angle+arand, velocity)
newfb.SetPosition(c.goblin.GetPosition())
c.enemies = append(c.enemies, newfb)
if c.eventmap[gamedata.GameEventFireball] != nil {
c.eventmap[gamedata.GameEventFireball]()
}
}
}
func IsPixelColliding(img1, img2 *ebiten.Image, offset1, offset2 image.Point) bool {
// Get the pixel data from both images
bounds1 := img1.Bounds()
bounds2 := img2.Bounds()
// Create slices to hold the pixel data
pixels1 := make([]byte, 4*bounds1.Dx()*bounds1.Dy()) // RGBA (4 bytes per pixel)
pixels2 := make([]byte, 4*bounds2.Dx()*bounds2.Dy())
// Read pixel data from the images
img1.ReadPixels(pixels1)
img2.ReadPixels(pixels2)
// Determine the overlapping rectangle
rect1 := bounds1.Add(offset1)
rect2 := bounds2.Add(offset2)
intersection := rect1.Intersect(rect2)
if intersection.Empty() {
return false // No overlap
}
// Check pixel data in the overlapping region
for y := intersection.Min.Y; y < intersection.Max.Y; y++ {
for x := intersection.Min.X; x < intersection.Max.X; x++ {
// Calculate the indices in the pixel slices
idx1 := ((y-offset1.Y)*bounds1.Dx() + (x - offset1.X)) * 4
idx2 := ((y-offset2.Y)*bounds2.Dx() + (x - offset2.X)) * 4
// Extract alpha values (transparency)
alpha1 := pixels1[idx1+3]
alpha2 := pixels2[idx2+3]
// If both pixels are non-transparent, there's a collision
if alpha1 > 0 && alpha2 > 0 {
return true
}
}
}
return false // No collision detected
}
// RotatePoint rotates a point (x, y) around an origin (ox, oy) by a given angle (in radians).
func RotatePoint(x, y, ox, oy, angle float64) (float64, float64) {
sin, cos := math.Sin(angle), math.Cos(angle)
dx, dy := x-ox, y-oy
return ox + dx*cos - dy*sin, oy + dx*sin + dy*cos
}
// IsPixelCollidingWithRotation checks for pixel-perfect collision between two rotated images.
func IsPixelCollidingWithRotation(img1, img2 *ebiten.Image, center1, center2 image.Point, angle1, angle2 float64) bool {
// Get pixel data
bounds1 := img1.Bounds()
bounds2 := img2.Bounds()
pixels1 := make([]byte, 4*bounds1.Dx()*bounds1.Dy())
pixels2 := make([]byte, 4*bounds2.Dx()*bounds2.Dy())
img1.ReadPixels(pixels1)
img2.ReadPixels(pixels2)
// Loop through all pixels in the bounding boxes of the first image
for y1 := bounds1.Min.Y; y1 < bounds1.Max.Y; y1++ {
for x1 := bounds1.Min.X; x1 < bounds1.Max.X; x1++ {
// Get alpha for the pixel in img1
idx1 := (y1*bounds1.Dx() + x1) * 4
alpha1 := pixels1[idx1+3]
if alpha1 == 0 {
continue // Skip transparent pixels
}
// Rotate this pixel to its global position
globalX, globalY := RotatePoint(float64(x1), float64(y1), float64(bounds1.Dx()/2), float64(bounds1.Dy()/2), angle1)
globalX += float64(center1.X)
globalY += float64(center1.Y)
// Transform global position to img2's local space
localX, localY := RotatePoint(globalX-float64(center2.X), globalY-float64(center2.Y), 0, 0, -angle2)
// Check if the transformed position is within img2's bounds
lx, ly := int(localX)+bounds2.Dx()/2, int(localY)+bounds2.Dy()/2
if lx < 0 || ly < 0 || lx >= bounds2.Dx() || ly >= bounds2.Dy() {
continue
}
// Get alpha for the pixel in img2
idx2 := (ly*bounds2.Dx() + lx) * 4
alpha2 := pixels2[idx2+3]
if alpha2 > 0 {
return true // Collision detected
}
}
}
return false // No collision
}
func (c *Canvas) LaserAttempt1() {
//for _, e := range c.enemies {
/*
rgba1 := c.laserMask.SubImage(c.laserMask.Bounds()).(*image.RGBA)
rgba2 := e.GetSprite().SubImage(e.GetSprite().Bounds()).(*image.RGBA)
*/
// Check collision
/*
if IsPixelCollidingWithRotation(c.laser.Sprite,
e.GetSprite(),
image.Pt(int(c.laser.GetPosition().X), int(c.laser.GetPosition().Y)),
image.Pt(int(e.GetPosition().X), int(e.GetPosition().Y)),
c.laser.GetAngle(),
0,
) {
println("Pixel-perfect collision detected!")
}
*/
/*
c.collisionMask.Clear()
c.collisionMask.DrawImage(c.laserMask, nil)
op := &ebiten.DrawImageOptions{}
op.GeoM.Reset()
op.Blend = ebiten.BlendDestinationIn
op.GeoM.Translate(e.GetPosition().X-float64(e.GetSprite().Bounds().Dx())/2, e.GetPosition().Y-float64(e.GetSprite().Bounds().Dy())/2)
c.collisionMask.DrawImage(e.GetSprite(), op)
*/
/*
if c.HasCollided(c.collisionMask, 640*480*4) {
if c.eventmap[gamedata.GameEventTargetHit] != nil {
c.eventmap[gamedata.GameEventTargetHit]()
}
fmt.Println("enemy sliced")
}
*/
//}
}
// try to find if the enemy is along the laser line first, then apply pixel collision
func (c *Canvas) LaserAttempt2() {
for _, e := range c.enemies {
a := c.lastInputs.ShotAngle
x0 := c.hero.Pos.X
y0 := c.hero.Pos.Y
thresh := 25.
x := e.GetPosition().X
y := math.Tan(a)*(x-x0) + y0
var laserd bool = false
if !math.IsNaN(math.Tan(a)) {
if math.Abs(e.GetPosition().Y-y) <= thresh {
laserd = true
} else {
if math.Abs(e.GetPosition().X-x0) <= thresh {
laserd = true
}
}
}
if laserd {
//check for pixel collision
if IsPixelColliding(c.laserMask, e.GetSprite(),
image.Pt(0, 0),
image.Pt(int(e.GetPosition().X), int(e.GetPosition().Y))) {
e.SetHit()
if c.eventmap[gamedata.GameEventTargetHit] != nil {
c.eventmap[gamedata.GameEventTargetHit]()
}
fmt.Println("laser'd")
}
}
}
}
// straight up just pixel collision check, expensive though
func (c *Canvas) LaserAttempt3() {
for _, e := range c.enemies {
if IsPixelColliding(c.laserMask, e.GetSprite(),
image.Pt(0, 0),
image.Pt(int(e.GetPosition().X), int(e.GetPosition().Y))) {
e.SetHit()
if c.eventmap[gamedata.GameEventTargetHit] != nil {
c.eventmap[gamedata.GameEventTargetHit]()
}
fmt.Println("laser'd")
}
}
}
// straight up just pixel collision check, expensive though
func (c *Canvas) LaserAttempt4() {
for _, e := range c.enemies {
c.lasercoords[2] = e.GetPosition()
x0 := c.hero.Pos.X
y0 := c.hero.Pos.Y
x1 := e.GetPosition().X
y1 := e.GetPosition().Y
a := c.lastInputs.ShotAngle
var d float64 = 100
/*
if !math.IsNaN(math.Tan(a)) {
m0 := math.Tan(a)
if m0 == 0 {
d = math.Abs(y1 - y0)
fmt.Printf("horizontal beam\n")
c.lasercoords[3] = gamedata.Coordinates{X: x1, Y: y0}
} else {
m1 := -1 / m0
if (m0 - m1) != 0 {
xi := (y1 + x0*m0 - y0 - m1*x1) / (m0 - m1)
yi := xi*m0 - x0*m0 + y0
c.lasercoords[3] = gamedata.Coordinates{X: xi, Y: yi}
d = math.Sqrt(math.Pow(x1-xi, 2) + math.Pow(y1-yi, 2))
fmt.Printf("%f \n", d)
} else {
}
}
fmt.Printf("%f \n", a)
} else {
c.lasercoords[3] = gamedata.Coordinates{X: x1, Y: y1}
d = math.Abs(x1 - x0)
fmt.Printf("vertical beam\n")
}
*/
if math.Abs(math.Mod(a, math.Pi)) == math.Pi/2 { // Check for vertical beam
d = math.Abs(x1 - x0)
c.lasercoords[3] = gamedata.Coordinates{X: x0, Y: y1} // Align on x-axis
//fmt.Printf("vertical beam\n")
} else if math.Tan(a) == 0 { // Check for horizontal beam
d = math.Abs(y1 - y0)
c.lasercoords[3] = gamedata.Coordinates{X: x1, Y: y0} // Align on y-axis
//fmt.Printf("horizontal beam\n")
} else { // General case
m0 := math.Tan(a)
m1 := -1 / m0
xi := (y1 + x0*m0 - y0 - m1*x1) / (m0 - m1)
yi := xi*m0 - x0*m0 + y0
c.lasercoords[3] = gamedata.Coordinates{X: xi, Y: yi}
d = math.Sqrt(math.Pow(x1-xi, 2) + math.Pow(y1-yi, 2))
//fmt.Printf("%f \n", d)
}
//fmt.Printf("%f \n", a)
if d <= float64(e.GetSprite().Bounds().Dx()) && e.GetEnemyState() <= gamedata.EnemyStateHit {
if IsPixelColliding(c.laserMask, e.GetSprite(),
image.Pt(0, 0),
image.Pt(int(e.GetPosition().X), int(e.GetPosition().Y))) {
e.SetHit()
newsplash := elements.NewSplash()
//newsplash.SetPosition(c.lasercoords[3])
newsplash.SetPosition(e.GetPosition())
c.splashes = append(c.splashes, newsplash)
if c.eventmap[gamedata.GameEventTargetHit] != nil {
c.eventmap[gamedata.GameEventTargetHit]()
}
//fmt.Println("laser'd")
}
}
}
}
func (c *Canvas) UpdateSplashes() {
for _, sp := range c.splashes {
sp.Update()
}
}
func (c *Canvas) CleanSplashes() {
i := 0
for _, sp := range c.splashes {
if sp.GetAlpha() > 0 {
c.splashes[i] = sp
i++
}
}
for j := i; j < len(c.splashes); j++ {
c.splashes[j] = nil
}
c.splashes = c.splashes[:i]
}
func (c *Canvas) CleanupDrops() {
i := 0
for _, drop := range c.wpdrops {
if !drop.IsCollected() {
c.wpdrops[i] = drop
i++
}
}
for j := i; j < len(c.wpdrops); j++ {
c.wpdrops[j] = nil
}
c.wpdrops = c.wpdrops[:i]
}
func (c *Canvas) UpdateWeapons() {
if !c.gameover {
//check for weapon inputs
if c.lastInputs.CycleWeapon {
c.holster.CycleWeapon()
}
//now let's update some shit based on the weapon
switch c.holster.GetActiveWeapon().GetWeaponType() {
case gamedata.WeaponTypeGun:
c.AddProjectiles()
case gamedata.WeaponTypeLaser:
c.UpdateLaser()
}
} else {
c.laser.SetFiring(false)
}
}
func (c *Canvas) UpdateWeaponDrops() {
//do we have any drops? let's calculate the chances
//
for _, drop := range c.wpdrops {
drop.Update()
//has the hero collided with any? add to holster
//boundary box collision check
if c.hero.Pos.X >= drop.GetPosition().X-float64(drop.Sprite.Bounds().Dx())/2 &&
c.hero.Pos.X <= drop.GetPosition().X+float64(drop.Sprite.Bounds().Dx())/2 &&
c.hero.Pos.Y >= drop.GetPosition().Y-float64(drop.Sprite.Bounds().Dy())/2 &&
c.hero.Pos.Y <= drop.GetPosition().Y+float64(drop.Sprite.Bounds().Dy())/2 {
//fmt.Println("hero trying to pick up weapon maybe")
if IsPixelColliding(
c.hero.Sprite,
drop.Sprite,
image.Pt(int(c.hero.Pos.X), int(c.hero.Pos.Y)),
image.Pt(int(drop.GetPosition().X), int(drop.GetPosition().Y)),
) {
fmt.Println("weapon acquired")
drop.SetCollected(true)
c.holster.AddWeapon(weapons.NewLaser())
}
}
}
}
func (c *Canvas) ResetWeaponDrops() {
for i := range c.wpdrops {
c.wpdrops[i] = nil
}
c.wpdrops = c.wpdrops[:0]
c.holster = weapons.NewHolster()
}

109
gameelement/cloudlayer.go Normal file
View File

@@ -0,0 +1,109 @@
package gameelement
import (
"math"
"math/rand/v2"
"mover/elements"
"mover/gamedata"
"github.com/hajimehoshi/ebiten/v2"
)
type CloudLayer struct {
Sprite *ebiten.Image
clouds []*elements.Cloud
dimensions gamedata.Area
cycle int
}
func NewCloudLayer(a gamedata.Area) *CloudLayer {
c := &CloudLayer{
Sprite: ebiten.NewImage(a.Width, a.Height),
cycle: 0,
dimensions: a,
}
return c
}
func (c *CloudLayer) SetInputs(gamedata.GameInputs) {
}
func (c *CloudLayer) Update() error {
c.cycle++
for _, cloud := range c.clouds {
cloud.Update()
cpos := cloud.GetPosition()
if cpos.X > float64(c.dimensions.Width)+float64(cloud.Sprite.Bounds().Dx()) {
dx := -float64(cloud.Sprite.Bounds().Dx())
cloud.SetPosition(gamedata.Coordinates{X: dx, Y: cloud.GetPosition().Y})
}
if cpos.X < -float64(cloud.Sprite.Bounds().Dx()) {
dx := float64(c.dimensions.Width + cloud.Sprite.Bounds().Dx())
cloud.SetPosition(gamedata.Coordinates{X: dx, Y: cloud.GetPosition().Y})
}
if cpos.Y > float64(c.dimensions.Height)+float64(cloud.Sprite.Bounds().Dy()) {
dy := -float64(cloud.Sprite.Bounds().Dy())
cloud.SetPosition(gamedata.Coordinates{X: cloud.GetPosition().X, Y: dy})
}
if cpos.Y < -float64(cloud.Sprite.Bounds().Dy()) {
dy := float64(c.dimensions.Height + cloud.Sprite.Bounds().Dy())
cloud.SetPosition(gamedata.Coordinates{X: cloud.GetPosition().X, Y: dy})
}
}
return nil
}
func (c *CloudLayer) Draw(drawimg *ebiten.Image) {
c.Sprite.Clear()
for _, cloud := range c.clouds {
cloud.Draw()
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(cloud.GetPosition().X, cloud.GetPosition().Y)
op.ColorScale.ScaleAlpha(float32(cloud.Alpha))
c.Sprite.DrawImage(cloud.Sprite, op)
}
drawimg.DrawImage(c.Sprite, nil)
}
func (c *CloudLayer) Initialize() {
//cull previous cloud layer
for i := 0; i < len(c.clouds); i++ {
c.clouds[i] = nil
}
c.clouds = c.clouds[:0]
numclouds := rand.IntN(20)
angle := rand.Float64() * math.Pi * 2
for i := 0; i < numclouds; i++ {
a := gamedata.Area{
Height: rand.IntN(c.dimensions.Width/2) + 1,
Width: rand.IntN(c.dimensions.Height/2) + 1,
}
velocity := rand.Float64() * 3
//velocity := 0.
newcloud := elements.NewCloud(a, angle, velocity)
newcloud.Alpha = rand.Float64() / 2
newcloud.SetPosition(gamedata.Coordinates{
X: rand.Float64() * float64(c.dimensions.Width),
Y: rand.Float64() * float64(c.dimensions.Height),
})
c.clouds = append(c.clouds, newcloud)
}
}
func (c *CloudLayer) RegisterEvents(e gamedata.GameEvent, f func()) {
}

View File

@@ -0,0 +1,15 @@
package gameelement
import (
"mover/gamedata"
"github.com/hajimehoshi/ebiten/v2"
)
type GameElement interface {
SetInputs(gamedata.GameInputs)
Update() error
Draw(drawimg *ebiten.Image)
Initialize()
RegisterEvents(e gamedata.GameEvent, f func())
}

149
gameelement/rainlayer.go Normal file
View File

@@ -0,0 +1,149 @@
package gameelement
import (
"math/rand/v2"
"mover/elements"
"mover/gamedata"
"github.com/hajimehoshi/ebiten/v2"
)
type RainLayer struct {
Sprite *ebiten.Image
lastInputs gamedata.GameInputs
raindrops []*elements.RainDrop
nextsplashes []gamedata.Coordinates
rainsplashes []*elements.RainSplash
dimensions gamedata.Area
cycle int
}
func NewRainLayer(a gamedata.Area) *RainLayer {
rl := &RainLayer{
Sprite: ebiten.NewImage(a.Width, a.Height),
dimensions: a,
}
for i := 0; i < 50; i++ {
nrd := elements.NewRainDrop()
nrd.SetPosition(gamedata.Coordinates{X: rand.Float64() * float64(a.Width), Y: rand.Float64() * float64(a.Height)})
rl.raindrops = append(rl.raindrops, nrd)
}
for i := 0; i < 50; i++ {
nrd := elements.NewRainSplash()
nrd.SetPosition(gamedata.Coordinates{X: rand.Float64() * float64(a.Width), Y: rand.Float64() * float64(a.Height)})
rl.rainsplashes = append(rl.rainsplashes, nrd)
}
return rl
}
func (r *RainLayer) SetInputs(inputs gamedata.GameInputs) {
r.lastInputs = inputs
}
func (r *RainLayer) Update() error {
r.UpdateDrops()
r.UpdateSplashes()
r.cycle++
return nil
}
func (r *RainLayer) Draw(drawimg *ebiten.Image) {
r.Sprite.Clear()
for _, drop := range r.raindrops {
drop.Draw()
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(drop.GetPosition().X, drop.GetPosition().Y)
op.ColorScale.ScaleAlpha(0.5)
r.Sprite.DrawImage(drop.Sprite, op)
}
for _, drop := range r.rainsplashes {
drop.Draw()
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(drop.GetPosition().X, drop.GetPosition().Y)
op.ColorScale.ScaleAlpha(0.5)
r.Sprite.DrawImage(drop.Sprite, op)
}
drawimg.DrawImage(r.Sprite, nil)
}
func (r *RainLayer) Initialize() {
}
func (r *RainLayer) RegisterEvents(e gamedata.GameEvent, f func()) {
}
func (r *RainLayer) UpdateDrops() {
i := 0
for _, drop := range r.raindrops {
drop.Update()
if !drop.Expired() {
r.raindrops[i] = drop
i++
} else {
r.nextsplashes = append(r.nextsplashes, drop.GetPosition())
}
}
var j int = i
var newdrops int = 0
for ; j < len(r.raindrops); j++ {
r.raindrops[j] = nil
}
newdrops = len(r.raindrops) - i
r.raindrops = r.raindrops[:i]
for k := 0; k < newdrops; k++ {
nrd := elements.NewRainDrop()
nrd.SetPosition(gamedata.Coordinates{X: rand.Float64() * float64(r.dimensions.Width), Y: rand.Float64() * float64(r.dimensions.Height)})
r.raindrops = append(r.raindrops, nrd)
}
}
func (r *RainLayer) UpdateSplashes() {
i := 0
for _, drop := range r.rainsplashes {
drop.Update()
if !drop.Expired() {
r.rainsplashes[i] = drop
i++
}
}
var j int = i
//var newdrops int = 0
for ; j < len(r.rainsplashes); j++ {
r.rainsplashes[j] = nil
}
//newdrops = len(r.rainsplashes) - i
r.rainsplashes = r.rainsplashes[:i]
/*
for k := 0; k < newdrops; k++ {
nrd := elements.NewRainSplash()
nrd.SetPosition(gamedata.Coordinates{X: rand.Float64() * float64(r.dimensions.Width), Y: rand.Float64() * float64(r.dimensions.Height)})
r.rainsplashes = append(r.rainsplashes, nrd)
}*/
for _, splashloc := range r.nextsplashes {
nrd := elements.NewRainSplash()
nrd.SetPosition(splashloc)
r.rainsplashes = append(r.rainsplashes, nrd)
}
r.nextsplashes = r.nextsplashes[:0]
}

1
go.mod
View File

@@ -13,6 +13,7 @@ require (
require (
github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325 // indirect
github.com/ebitengine/hideconsole v1.0.0 // indirect
github.com/ebitengine/oto/v3 v3.3.1 // indirect
github.com/ebitengine/purego v0.8.0 // indirect
github.com/jezek/xgb v1.1.1 // indirect
golang.org/x/sync v0.8.0 // indirect

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

BIN
hero.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

28
main.go
View File

@@ -1,8 +1,11 @@
package main
import (
"fmt"
"log"
"mover/assets"
"mover/gamedata"
"mover/screenmanager"
"mover/screens"
"github.com/hajimehoshi/ebiten/v2"
)
@@ -13,17 +16,26 @@ const (
)
func main() {
ver := "survive v0.08"
fmt.Println(ver)
moverGame := &Game{}
//moverGame := &Game{}
moverGame := screenmanager.NewManager()
moverGame.SetDimensions(gamedata.Area{Width: screenWidth, Height: screenHeight})
ebiten.SetWindowSize(screenWidth*1.5, screenHeight*1.5)
ebiten.SetWindowTitle(ver)
ebiten.SetWindowTitle(moverGame.Info.Name + ": v" + moverGame.Info.Version)
if err := ebiten.RunGame(moverGame); err != nil {
loadScreens(&moverGame)
if err := ebiten.RunGame(&moverGame); err != nil {
log.Fatal(err)
}
}
func loadScreens(m *screenmanager.Manager) {
assets.LoadImages()
assets.LoadSounds()
m.AddScene(screens.NewStartScreen())
//m.AddScene(screens.NewGame())
m.AddScene(screens.NewPrimary())
m.ResetScenes()
}

174
mover.go
View File

@@ -1,174 +0,0 @@
package main
import (
"bytes"
"image"
"log"
_ "embed"
"image/color"
_ "image/png"
"github.com/hajimehoshi/ebiten/v2"
)
var (
flyeyeImage *ebiten.Image
flyeyeImage2 *ebiten.Image
flyeyeImage3 *ebiten.Image
//go:embed fly-eye.png
flyeye_img []byte
//go:embed fly-eye2.png
flyeye_img2 []byte
//go:embed fly-eye3.png
flyeye_img3 []byte
)
const (
MoverActionDefault = iota
MoverActionDamaged
MoverActionDying
MoverActionExploding
MoverActionDead
MoverActionMax
)
type MoverAction uint
func init() {
img, _, err := image.Decode(bytes.NewReader(flyeye_img))
if err != nil {
log.Fatal(err)
}
flyeyeImage = ebiten.NewImageFromImage(img)
img, _, err = image.Decode(bytes.NewReader(flyeye_img2))
if err != nil {
log.Fatal(err)
}
flyeyeImage2 = ebiten.NewImageFromImage(img)
img, _, err = image.Decode(bytes.NewReader(flyeye_img3))
if err != nil {
log.Fatal(err)
}
flyeyeImage3 = ebiten.NewImageFromImage(img)
}
type Mover struct {
Sprite *ebiten.Image
Maks *ebiten.Image
MaksDest *ebiten.Image
Angle float64
Pos Coordinates
Origin Coordinates
Action MoverAction
cycles int
rotating bool
Toggled bool
Hit bool
Touched bool
dyingcount int
}
func NewMover() *Mover {
m := &Mover{
Sprite: ebiten.NewImage(48, 48),
Maks: ebiten.NewImage(48, 48),
MaksDest: ebiten.NewImage(48, 48),
Action: MoverActionDefault,
cycles: 4,
Angle: 0,
rotating: false,
Toggled: false,
dyingcount: 0,
}
m.Maks.Fill(color.White)
return m
}
func (m *Mover) ToggleRotate() {
m.rotating = !m.rotating
}
func (m *Mover) SetAngle(a float64) {
m.Angle = a
}
func (m *Mover) SetOrigin(coords Coordinates) {
m.Origin = coords
m.Pos = coords
}
func (m *Mover) Draw() {
m.Sprite.Clear()
m.MaksDest.Clear()
idx := (m.cycles / 8) % 4
y0 := 0
y1 := 48
x0 := 48 * idx
x1 := x0 + 48
switch m.Action {
case MoverActionDefault:
m.Sprite.DrawImage(flyeyeImage.SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), nil)
case MoverActionDamaged:
m.Sprite.DrawImage(flyeyeImage2.SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), nil)
case MoverActionDying:
m.dyingcount++
if (m.cycles/5)%2 == 0 {
m.MaksDest.DrawImage(flyeyeImage2.SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), nil)
op := &ebiten.DrawImageOptions{}
op.GeoM.Reset()
op.Blend = ebiten.BlendSourceAtop
m.MaksDest.DrawImage(m.Maks, op)
m.Sprite.DrawImage(m.MaksDest, nil)
} else {
m.Sprite.DrawImage(flyeyeImage2.SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), nil)
}
if m.dyingcount > 60 {
m.cycles = 0
m.SetHit()
}
case MoverActionExploding:
m.Sprite.DrawImage(flyeyeImage3.SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), nil)
if idx == 3 {
m.SetHit()
}
default:
}
}
func (m *Mover) Update() {
/*
dx := 0. //40 * math.Cos(float64(m.cycles)/16)
dy := 0. //40 * math.Sin(float64(m.cycles)/16)
m.Pos = Coordinates{X: m.Origin.X + dx, Y: m.Origin.Y + dy}
*/
/*
if m.rotating {
m.Angle = float64(m.cycles) / (math.Pi * 2)
}
*/
m.cycles++
}
func (m *Mover) SetHit() {
m.Action++ // = (m.Action + 1) % MoverActionMax
}
func (m *Mover) ToggleColor() {
//m.Toggled = !m.Toggled
if m.Action == MoverActionDefault {
m.Action = MoverActionDamaged
} else if m.Action == MoverActionDamaged {
m.Action = MoverActionDefault
}
}

136
screenmanager/manager.go Normal file
View File

@@ -0,0 +1,136 @@
package screenmanager
import (
"mover/gamedata"
"mover/screens"
"github.com/hajimehoshi/ebiten/v2"
)
const (
defaultWidth = 1024
defaultHeight = 768
)
type Manager struct {
Info gamedata.GameInfo
currentScene screens.Screen
currentSceneId uint
nextSceneId uint
screens []screens.Screen
internalerr error
}
// can be used to create default manager instance
func NewManager() Manager {
return Manager{
Info: gamedata.GameInfo{
Name: "survive",
Version: "0.34",
Dimensions: gamedata.Area{
Width: defaultWidth,
Height: defaultHeight,
},
},
currentSceneId: 0,
nextSceneId: 1,
internalerr: nil,
}
}
// ebitengine update proxy on behalf of current scene
func (m *Manager) Update() error {
if m.currentScene == nil {
return nil
}
err := m.currentScene.Update()
if err != nil {
return err
}
return m.internalerr
}
// shutdown application
func (m *Manager) Quit() {
m.internalerr = ebiten.Termination
}
// calls current scene's draw method if the currentscene is valid
func (m *Manager) Draw(screen *ebiten.Image) {
if m.currentScene != nil {
m.currentScene.Draw(screen)
}
}
// ebitengine proxy for layout
func (m *Manager) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
return m.Info.Dimensions.Width, m.Info.Dimensions.Height
}
// appends scene to the managed screens
func (m *Manager) AddScene(s screens.Screen) {
setDefaultHandlers(m, s)
s.SetDimensions(m.Info.Dimensions)
m.screens = append(m.screens, s)
}
// sets the default callback handlers for a given scene within manager
// Default Handling behaviours:
//
// reset: sets (scene, nextscene) to {0, 1}
// scene completion: sets (scene, nextscene) to {nextscene, nextscene+1}
// end game: shutdown groovy
//
// note: NOOP and RELOAD are purposefully not mapped; they are scene
// specific and should be mapped to by user of groovy
func setDefaultHandlers(m *Manager, s screens.Screen) {
s.SetEventHandler(screens.EventReset, func() { m.ResetScenes() })
s.SetEventHandler(screens.EventCompleted, func() { m.TransitionScene() })
s.SetEventHandler(screens.EventEndgame, func() { m.Quit() })
}
// we're going to reset the scene to the first one
func (m *Manager) ResetScenes() {
m.currentSceneId = 0
m.nextSceneId = 1
m.SetCurrentScene(0)
}
// sets the current scene, based on sceneindex n
// n > scenelist, quit
// otherwise, scene = n
func (m *Manager) SetCurrentScene(sceneId uint) {
if sceneId >= uint(len(m.screens)) {
m.Quit()
} else {
m.currentSceneId = sceneId
m.currentScene = m.screens[sceneId]
m.nextSceneId = m.currentSceneId + 1
}
}
// handle scene transition
func (m *Manager) TransitionScene() {
m.SetCurrentScene(m.nextSceneId)
}
// set new sceneId as the successor
func (m *Manager) SetNextScene(sceneId uint) {
m.nextSceneId = sceneId
}
// sets sene dimensions
func (m *Manager) SetDimensions(a gamedata.Area) {
m.Info.Dimensions = a
}
// report number of total screens
func (m *Manager) SceneCount() uint {
return uint(len(m.screens))
}
func (m *Manager) GetScene(sceneId uint) screens.Screen {
return m.screens[sceneId]
}

729
screens/game.go Normal file
View File

@@ -0,0 +1,729 @@
package screens
import (
"fmt"
"image"
"image/color"
"math"
"math/rand/v2"
"mover/assets"
"mover/elements"
"mover/fonts"
"mover/gamedata"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/audio"
"github.com/hajimehoshi/ebiten/v2/inpututil"
"github.com/hajimehoshi/ebiten/v2/text"
"github.com/hajimehoshi/ebiten/v2/vector"
)
const (
MOVER_WIDTH = 48
MOVER_HEIGHT = 48
sampleRate = 44100
)
type Game struct {
events map[ScreenManagerEvent]func()
background *ebiten.Image
collisionMask *ebiten.Image
projectileMask *ebiten.Image
heroCollisionMask *ebiten.Image
heroCollisionCpy *ebiten.Image
dimensions gamedata.Area
Pos gamedata.Coordinates
Paused bool
initialized bool
gameover bool
reset bool
musicInitialized bool
runtime float64
hero *elements.Hero
projectiles map[int]*elements.Projectile
explosion *elements.Explosion
audioplayer *audio.Player
score int
counter int
timer int
targets []*elements.Mover
boss *elements.Boss
}
var (
audioContext = audio.NewContext(sampleRate)
)
func NewGame() *Game {
g := &Game{
events: make(map[ScreenManagerEvent]func()),
musicInitialized: false,
boss: elements.NewBoss(),
}
return g
}
func (g *Game) Initialize() {
if !g.musicInitialized {
s := audio.NewInfiniteLoop(assets.SoundBank[assets.MainLoop], assets.SoundBank[assets.MainLoop].Length())
g.audioplayer, _ = audioContext.NewPlayer(s)
g.audioplayer.Play()
g.musicInitialized = true
}
origin := gamedata.Coordinates{X: 640 / 2, Y: 480 / 2}
g.ConstructBackground()
g.hero = elements.NewHero()
g.hero.SetOrigin(origin)
g.hero.ToggleRotate()
g.gameover = false
g.collisionMask = ebiten.NewImage(g.dimensions.Width, g.dimensions.Height)
g.projectileMask = ebiten.NewImage(g.dimensions.Width, g.dimensions.Height)
g.heroCollisionMask = ebiten.NewImage(MOVER_WIDTH, MOVER_HEIGHT)
g.heroCollisionCpy = ebiten.NewImage(MOVER_WIDTH, MOVER_HEIGHT)
g.explosion = elements.NewExplosion()
g.explosion.SetOrigin(origin)
g.score = 0
g.reset = false
//clean up all targets
for j := 0; j < len(g.targets); j++ {
g.targets[j] = nil
}
g.targets = g.targets[:0]
g.score = 0
g.counter = 0
g.timer = 0
g.runtime = 0.
g.boss.Reset()
g.projectiles = make(map[int]*elements.Projectile)
g.initialized = true
g.reset = false
}
func (g *Game) Update() error {
if !g.initialized || g.reset {
g.Initialize()
} else {
g.StepGame()
}
g.timer++
return nil
}
func (g *Game) Draw(screen *ebiten.Image) {
screen.Clear()
if g.initialized {
screen.DrawImage(g.background, nil)
g.hero.Draw()
op := &ebiten.DrawImageOptions{}
if !g.gameover {
g.runtime = float64(g.counter) / 60.
}
op.GeoM.Translate(-MOVER_WIDTH/2, -MOVER_HEIGHT/2)
op.GeoM.Translate(g.hero.Pos.X, g.hero.Pos.Y)
screen.DrawImage(g.hero.Sprite, op)
op.GeoM.Reset()
op.GeoM.Translate(0, -16)
op.GeoM.Rotate(g.hero.Angle)
op.GeoM.Translate(g.hero.Pos.X, g.hero.Pos.Y)
screen.DrawImage(assets.ImageBank[assets.Weapon], op)
//secondary/upgraded weapon sprite; in testing proves sort of distracting
/*
if g.hero.Upgrade {
op.GeoM.Reset()
op.GeoM.Translate(-16, -16)
op.GeoM.Scale(0.75, 0.75)
op.GeoM.Translate(16, 0)
op.GeoM.Rotate(g.hero.Angle + math.Pi)
op.GeoM.Translate(g.hero.Pos.X, g.hero.Pos.Y)
screen.DrawImage(weaponImage, op)
}
*/
//draw shadows--------------------------------------------------------------
for _, target := range g.targets {
if target.Action < elements.MoverActionExploding {
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(target.Pos.X-10, target.Pos.Y+10)
screen.DrawImage(assets.ImageBank[assets.FlyEyeShadow], op)
}
}
if g.boss.Spawned && g.boss.Action < elements.MoverActionExploding {
op := &ebiten.DrawImageOptions{}
op.GeoM.Scale(4, 2)
op.GeoM.Translate(g.boss.Pos.X-96/2, g.boss.Pos.Y+96/2-10)
screen.DrawImage(assets.ImageBank[assets.FlyEyeShadow], op)
}
//draw enemies--------------------------------------------------------------
for _, target := range g.targets {
target.Draw()
op.GeoM.Reset()
op.GeoM.Translate(-MOVER_WIDTH/2, -MOVER_HEIGHT/2)
op.GeoM.Rotate(target.Angle)
op.GeoM.Translate(target.Pos.X, target.Pos.Y)
screen.DrawImage(target.Sprite, op)
}
if g.boss.Spawned {
g.boss.Draw()
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(-MOVER_WIDTH, -MOVER_HEIGHT)
op.GeoM.Translate(g.boss.Pos.X, g.boss.Pos.Y)
screen.DrawImage(g.boss.Sprite, op)
//text.Draw(screen, fmt.Sprintf("%d", g.boss.Health), fonts.SurviveFont.Arcade, 100, 50, color.White)
//boss health bar
x0 := g.boss.Pos.X - 96
y0 := g.boss.Pos.Y - 60
vector.DrawFilledRect(screen, float32(x0), float32(y0), 204, 12, color.Black, true)
vector.DrawFilledRect(screen, float32(x0+2), float32(y0+2), float32(g.boss.Health)*2, 8, color.RGBA{R: 0xff, G: 0x00, B: 0x00, A: 0xff}, true)
}
g.projectileMask.Clear()
for _, p := range g.projectiles {
vector.DrawFilledCircle(g.projectileMask, float32(p.Pos.X), float32(p.Pos.Y), 3, color.White, true)
}
screen.DrawImage(g.projectileMask, nil)
/*
op.GeoM.Reset()
op.GeoM.Scale(0.25, 0.25)
screen.DrawImage(g.collisionMask, op)
*/
vector.StrokeCircle(screen, float32(g.explosion.Origin.X), float32(g.explosion.Origin.Y), float32(g.explosion.Radius), 3, color.White, true)
s := fmt.Sprintf("%02.3f", g.runtime)
if !g.gameover {
text.Draw(screen, "TIME: "+s, fonts.SurviveFont.Arcade, 640/2-250, 25, color.White)
text.Draw(screen, fmt.Sprintf("SCORE: %d", g.score*10), fonts.SurviveFont.Arcade, 640/2+100, 25, color.White)
} else {
if (g.counter/30)%2 == 0 {
text.Draw(screen, "TIME: "+s, fonts.SurviveFont.Arcade, 640/2-250, 25, color.White)
text.Draw(screen, fmt.Sprintf("SCORE: %d", g.score*10), fonts.SurviveFont.Arcade, 640/2+100, 25, color.White)
}
text.Draw(screen, "PRESS START TO TRY AGAIN", fonts.SurviveFont.Arcade, 640/2-150, 480/2, color.White)
}
}
}
func (g *Game) Layout(width, height int) (int, int) {
return g.dimensions.Width, g.dimensions.Height
}
func (g *Game) CleanupTargets() {
// remove dead targets by 1) iterating over all targets
i := 0
for _, target := range g.targets {
//moving valid targets to the front of the slice
if target.Action < elements.MoverActionDead {
g.targets[i] = target
i++
}
}
//then culling the last elements of the slice
if len(g.targets)-i > 0 {
// fmt.Printf("Removing %d elements\n", len(g.targets)-i)
g.score += len(g.targets) - i
}
for j := i; j < len(g.targets); j++ {
g.targets[j] = nil
}
g.targets = g.targets[:i]
}
func (g *Game) StepGame() {
g.HandleInput()
if !g.Paused {
if !g.audioplayer.IsPlaying() {
g.audioplayer.Play()
}
g.hero.Update()
g.explosion.Update()
g.UpdateTargets()
if g.boss.Spawned {
g.UpdateBoss()
}
g.UpdateProjectiles()
if !g.gameover {
g.UpdateHeroPosition()
//append new projectiles
g.AppendProjectiles()
//handle pulsewave updates
g.HandlePulseWaveUpdate()
if !g.boss.Spawned {
//add new target with increasing frequency
g.SpawnEnemies()
if g.counter > 2000 {
g.SpawnBoss()
}
}
}
g.CleanupTargets()
g.counter++
} else {
g.audioplayer.Pause()
}
}
func (g *Game) SpawnEnemies() {
f := 40000 / (g.counter + 1)
if g.counter%f == 0 {
g.targets = append(g.targets, elements.NewMover())
x0 := rand.Float64() * 640
y0 := rand.Float64() * 480
quadrant := rand.IntN(3)
switch quadrant {
case 0:
g.targets[len(g.targets)-1].SetOrigin(gamedata.Coordinates{X: x0, Y: -MOVER_HEIGHT})
case 1:
g.targets[len(g.targets)-1].SetOrigin(gamedata.Coordinates{X: x0, Y: float64(g.dimensions.Height) + MOVER_HEIGHT})
case 2:
g.targets[len(g.targets)-1].SetOrigin(gamedata.Coordinates{X: -MOVER_WIDTH, Y: y0})
case 3:
g.targets[len(g.targets)-1].SetOrigin(gamedata.Coordinates{X: float64(g.dimensions.Width) + x0, Y: y0})
default:
g.targets[len(g.targets)-1].SetOrigin(gamedata.Coordinates{X: x0, Y: y0})
fmt.Println("WTF " + string(quadrant))
}
}
}
func (g *Game) HandlePulseWaveUpdate() {
if g.explosion.Active {
if g.explosion.Radius > math.Sqrt(640*640+480*480) {
g.explosion.ToggleActivate()
g.explosion.Reset()
g.ResetTargetTouches()
}
//check collisions
for _, target := range g.targets {
dx := target.Pos.X - g.hero.Pos.X
dy := target.Pos.Y - g.hero.Pos.Y
r := math.Sqrt(dx*dx + dy*dy)
if r >= g.explosion.Radius-5 && r <= g.explosion.Radius+5 &&
target.Action <= elements.MoverActionDamaged && !target.Touched {
target.ToggleColor()
target.Touched = true
//target.SetHit()
}
}
//check for boss
if g.boss.Spawned {
dx := g.boss.Pos.X - g.hero.Pos.X
dy := g.boss.Pos.Y - g.hero.Pos.Y
r := math.Sqrt(dx*dx + dy*dy)
if r >= g.explosion.Radius-40 && r <= g.explosion.Radius+40 &&
g.boss.Action <= elements.MoverActionDamaged && !g.boss.Touched {
g.boss.ToggleColor()
g.boss.Touched = true
//target.SetHit()
}
}
}
}
func (g *Game) UpdateProjectiles() {
for k, p := range g.projectiles {
p.Update()
//cleanup projectiles at boundaries
if p.Pos.X < -640/2 || p.Pos.X > 1.5*640 || p.Pos.Y < -480/2 || p.Pos.Y > 1.5*480 {
p.Velocity = 0
delete(g.projectiles, k)
}
//compute projectile collisions
for _, target := range g.targets {
//first, boundary check
if p.Pos.X >= target.Pos.X-MOVER_WIDTH/2 && p.Pos.X <= target.Pos.X+MOVER_WIDTH/2 &&
p.Pos.Y >= target.Pos.Y-MOVER_HEIGHT/2 && p.Pos.Y <= target.Pos.Y+MOVER_HEIGHT/2 &&
target.Action == elements.MoverActionDamaged {
//fmt.Println("potential collision")
//the following computes total collisions in the image using a projectile mask that is a duplicate of what is on screen
//there's definitely room for optimization here
g.collisionMask.Clear()
g.collisionMask.DrawImage(g.projectileMask, nil)
op := &ebiten.DrawImageOptions{}
op.GeoM.Reset()
op.Blend = ebiten.BlendSourceIn
op.GeoM.Translate(target.Pos.X-MOVER_WIDTH/2, target.Pos.Y-MOVER_HEIGHT/2)
g.collisionMask.DrawImage(target.Sprite, op)
if g.HasCollided(g.collisionMask, g.dimensions.Width*g.dimensions.Height*4) {
//fmt.Println("pixel collision")
delete(g.projectiles, k)
//target.ToggleColor()
target.SetHit()
//target.SetOrigin(gamedata.Coordinates{X: rand.Float64() * 640, Y: rand.Float64() * 480})
target.Hit = true
player := audioContext.NewPlayerFromBytes(assets.TargetHit)
player.Play()
}
}
}
//boss check: first, boundary check
if p.Pos.X >= g.boss.Pos.X-MOVER_WIDTH && p.Pos.X <= g.boss.Pos.X+MOVER_WIDTH &&
p.Pos.Y >= g.boss.Pos.Y-MOVER_HEIGHT && p.Pos.Y <= g.boss.Pos.Y+MOVER_HEIGHT &&
g.boss.Action == elements.MoverActionDamaged {
//fmt.Println("potential collision")
//the following computes total collisions in the image using a projectile mask that is a duplicate of what is on screen
//there's definitely room for optimization here
g.collisionMask.Clear()
g.collisionMask.DrawImage(g.projectileMask, nil)
op := &ebiten.DrawImageOptions{}
op.GeoM.Reset()
op.Blend = ebiten.BlendSourceIn
op.GeoM.Translate(g.boss.Pos.X-MOVER_WIDTH/2, g.boss.Pos.Y-MOVER_HEIGHT/2)
g.collisionMask.DrawImage(g.boss.Sprite, op)
if g.HasCollided(g.collisionMask, g.dimensions.Width*g.dimensions.Height*4) {
//fmt.Println("pixel collision")
delete(g.projectiles, k)
//target.ToggleColor()
g.boss.SetHit()
//target.SetOrigin(gamedata.Coordinates{X: rand.Float64() * 640, Y: rand.Float64() * 480})
//g.boss.Hit = true
player := audioContext.NewPlayerFromBytes(assets.TargetHit)
player.Play()
}
}
}
}
func (g *Game) UpdateTargets() {
for _, target := range g.targets {
if target.Action == elements.MoverActionExploding && !target.SplodeInitiated {
player := audioContext.NewPlayerFromBytes(assets.Splode)
player.Play()
target.SplodeInitiated = true
}
if !target.Hit && g.hero.Action < elements.HeroActionDying {
dx := g.hero.Pos.X - target.Pos.X
dy := g.hero.Pos.Y - target.Pos.Y
angle := math.Atan2(dy, dx)
//maxspeed := (float64(g.counter) + 1.) / 1000.
maxspeed := 2.9
target.Pos.X += maxspeed * math.Cos(angle)
target.Pos.Y += maxspeed * math.Sin(angle)
}
//compute collision with hero
if g.hero.Pos.X >= target.Pos.X-MOVER_WIDTH/2 && g.hero.Pos.X <= target.Pos.X+MOVER_WIDTH/2 &&
g.hero.Pos.Y >= target.Pos.Y-MOVER_HEIGHT/2 && g.hero.Pos.Y <= target.Pos.Y+MOVER_HEIGHT/2 &&
target.Action < elements.MoverActionDying && g.hero.Action < elements.HeroActionDying {
g.heroCollisionMask.Clear()
g.heroCollisionMask.DrawImage(g.hero.Sprite, nil)
op := &ebiten.DrawImageOptions{}
op.GeoM.Reset()
op.Blend = ebiten.BlendSourceIn
op.GeoM.Translate((g.hero.Pos.X-target.Pos.X)-MOVER_WIDTH/2, (g.hero.Pos.Y-target.Pos.Y)-MOVER_HEIGHT/2)
g.heroCollisionMask.DrawImage(target.Sprite, op)
if g.HasCollided(g.heroCollisionMask, MOVER_HEIGHT*MOVER_HEIGHT*4) {
g.hero.SetHit()
g.gameover = true
player := audioContext.NewPlayerFromBytes(assets.HeroDeath)
player.Play()
}
}
target.Update()
}
}
func (g *Game) ResetTargetTouches() {
for _, t := range g.targets {
t.Touched = false
}
g.boss.Touched = false
}
func (g *Game) AppendProjectiles() {
if g.counter%14 == 0 && ebiten.IsStandardGamepadButtonPressed(0, ebiten.StandardGamepadButtonFrontBottomRight) {
g.projectiles[g.counter] = elements.NewProjectile(gamedata.Coordinates{X: g.hero.Pos.X, Y: g.hero.Pos.Y}, g.hero.Angle, 5.)
if g.hero.Upgrade {
g.projectiles[g.counter+1] = elements.NewProjectile(gamedata.Coordinates{X: g.hero.Pos.X, Y: g.hero.Pos.Y}, g.hero.Angle+math.Pi, 5.)
}
player := audioContext.NewPlayerFromBytes(assets.Shot)
player.Play()
}
}
func (g *Game) HandleInput() {
//if len(g.gamepadIDs) > 0 {
if inpututil.IsStandardGamepadButtonJustPressed(0, ebiten.StandardGamepadButtonRightStick) {
if !g.explosion.Active && !g.gameover {
g.explosion.SetOrigin(g.hero.Pos)
g.explosion.Reset()
g.explosion.ToggleActivate()
player := audioContext.NewPlayerFromBytes(assets.Magic)
player.Play()
}
}
if inpututil.IsStandardGamepadButtonJustPressed(0, ebiten.StandardGamepadButtonCenterRight) {
if g.gameover {
g.reset = true
} else {
g.Paused = !g.Paused
var player *audio.Player
if g.Paused {
player = audioContext.NewPlayerFromBytes(assets.PauseIn)
} else {
player = audioContext.NewPlayerFromBytes(assets.PauseOut)
}
player.Play()
}
}
//account for controller sensitivity
if !g.gameover && !g.Paused {
xaxis := ebiten.StandardGamepadAxisValue(0, ebiten.StandardGamepadAxisRightStickHorizontal)
yaxis := ebiten.StandardGamepadAxisValue(0, ebiten.StandardGamepadAxisRightStickVertical)
if yaxis <= 0.09 && yaxis >= -0.09 {
yaxis = 0
}
if xaxis <= 0.09 && xaxis >= -0.09 {
xaxis = 0
}
inputangle := math.Atan2(yaxis, xaxis)
g.hero.SetAngle(inputangle)
}
if inpututil.IsKeyJustPressed(ebiten.KeyQ) {
g.events[EventEndgame]()
}
//}
}
func (g *Game) UpdateHeroPosition() {
//handle gamepad input
inpx := ebiten.GamepadAxisValue(0, 0)
inpy := ebiten.GamepadAxisValue(0, 1)
//handle wasd input
if ebiten.IsKeyPressed(ebiten.KeyD) {
inpx = 1
}
if ebiten.IsKeyPressed(ebiten.KeyA) {
inpx = -1
}
if ebiten.IsKeyPressed(ebiten.KeyS) {
inpy = 1
}
if ebiten.IsKeyPressed(ebiten.KeyW) {
inpy = -1
}
if inpx >= 0.15 || inpx <= -0.15 {
g.hero.Left = inpx < 0
g.hero.Pos.X += inpx * 5
}
if inpy >= 0.15 || inpy <= -0.15 {
g.hero.Pos.Y += inpy * 5
}
}
func (g *Game) ConstructBackground() {
g.background = ebiten.NewImage(g.dimensions.Width, g.dimensions.Height)
BLOCK_SIZE := 32
for i := 0; i < 640/BLOCK_SIZE; i++ {
for j := 0; j < 480/BLOCK_SIZE; j++ {
//select random tile in x and y from tileset
idx_y := rand.IntN(256 / BLOCK_SIZE)
idx_x := rand.IntN(256 / BLOCK_SIZE)
x0 := BLOCK_SIZE * idx_x
y0 := BLOCK_SIZE * idx_y
x1 := x0 + BLOCK_SIZE
y1 := y0 + BLOCK_SIZE
//translate for grid element we're painting
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(float64(i*BLOCK_SIZE), float64(j*BLOCK_SIZE))
g.background.DrawImage(assets.ImageBank[assets.TileSet].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), op)
}
}
ax := float64(rand.IntN(640/BLOCK_SIZE) * BLOCK_SIZE)
ay := float64(rand.IntN(480/BLOCK_SIZE) * BLOCK_SIZE)
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(ax, ay)
g.background.DrawImage(assets.ImageBank[assets.Altar], op)
}
func (g *Game) SetDimensions(a gamedata.Area) {
g.dimensions = a
}
func (g *Game) SetEventHandler(e ScreenManagerEvent, f func()) {
g.events[e] = f
}
func (g *Game) SpawnBoss() {
x0 := rand.Float64() * 640
y0 := rand.Float64() * 480
quadrant := rand.IntN(3)
switch quadrant {
case 0:
g.boss.Pos = gamedata.Coordinates{X: x0, Y: -(MOVER_HEIGHT * 2)}
case 1:
g.boss.Pos = gamedata.Coordinates{X: x0, Y: float64(g.dimensions.Height) + (MOVER_HEIGHT * 2)}
case 2:
g.boss.Pos = gamedata.Coordinates{X: -(MOVER_HEIGHT * 2), Y: y0}
case 3:
g.boss.Pos = gamedata.Coordinates{X: float64(g.dimensions.Width) + x0, Y: y0}
default:
g.boss.Pos = gamedata.Coordinates{X: x0, Y: y0}
fmt.Println("WTF " + string(quadrant))
}
//g.boss.Pos = gamedata.Coordinates{X: 640 / 2, Y: 480 / 2}
g.boss.Spawned = true
}
func (g *Game) UpdateBoss() {
g.boss.Update()
if g.boss.Action == elements.MoverActionExploding && !g.boss.SplodeInitiated {
g.score += 10
player := audioContext.NewPlayerFromBytes(assets.Splode)
player.Play()
g.boss.SplodeInitiated = true
}
/*
if g.boss.Action >= elements.MoverActionDying {
g.boss.Spawned = false
}*/
if g.boss.Action >= elements.MoverActionDead {
g.boss.Pos = gamedata.Coordinates{X: -96, Y: -96}
}
if g.boss.Action < elements.MoverActionDying {
dx := g.hero.Pos.X - g.boss.Pos.X
dy := g.hero.Pos.Y - g.boss.Pos.Y
g.boss.Right = dx/48 > 0
g.boss.Pos = gamedata.Coordinates{
X: g.boss.Pos.X + dx/48,
Y: g.boss.Pos.Y + dy/48,
}
}
//compute collision with hero
if g.hero.Pos.X >= g.boss.Pos.X-MOVER_WIDTH && g.hero.Pos.X <= g.boss.Pos.X+MOVER_WIDTH &&
g.hero.Pos.Y >= g.boss.Pos.Y-MOVER_HEIGHT && g.hero.Pos.Y <= g.boss.Pos.Y+MOVER_HEIGHT &&
g.boss.Action < elements.MoverActionDying && g.hero.Action < elements.HeroActionDying {
g.heroCollisionMask.Clear()
g.heroCollisionMask.DrawImage(g.hero.Sprite, nil)
op := &ebiten.DrawImageOptions{}
op.GeoM.Reset()
op.Blend = ebiten.BlendSourceIn
op.GeoM.Translate((g.hero.Pos.X-g.boss.Pos.X)-MOVER_WIDTH, (g.hero.Pos.Y-g.boss.Pos.Y)-MOVER_HEIGHT)
g.heroCollisionMask.DrawImage(g.boss.Sprite, op)
if g.HasCollided(g.heroCollisionMask, MOVER_HEIGHT*MOVER_HEIGHT*4) {
g.hero.SetHit()
g.gameover = true
player := audioContext.NewPlayerFromBytes(assets.HeroDeath)
player.Play()
}
}
}
func (g *Game) HasCollided(mask *ebiten.Image, size int) bool {
var result bool = false
var pixels []byte = make([]byte, size)
mask.ReadPixels(pixels)
for i := 0; i < len(pixels); i = i + 4 {
if pixels[i+3] != 0 {
result = true
break
}
}
return result
}
func (g *Game) SetInputs(gamedata.GameInputs) {
}

250
screens/primary.go Normal file
View File

@@ -0,0 +1,250 @@
package screens
import (
"math"
"mover/assets"
"mover/gamedata"
"mover/gameelement"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/audio"
"github.com/hajimehoshi/ebiten/v2/inpututil"
)
type Primary struct {
events map[ScreenManagerEvent]func()
dimensions gamedata.Area
elements []gameelement.GameElement
gameevents map[gamedata.GameEvent]bool
paused bool
gameover bool
musicInitialized bool
audioplayer *audio.Player
}
func NewPrimary() *Primary {
p := &Primary{
events: make(map[ScreenManagerEvent]func()),
paused: false,
gameover: false,
musicInitialized: false,
}
gamearea := gamedata.Area{Width: 640, Height: 480}
//initialize our layer map
p.gameevents = make(map[gamedata.GameEvent]bool)
//create background layer
p.elements = append(p.elements, gameelement.NewBackground(gamearea))
//create canvas (game) layer
canvas := gameelement.NewCanvas(gamearea)
canvas.RegisterEvents(gamedata.GameEventPlayerDeath, p.EventHandlerPlayerDeath)
canvas.RegisterEvents(gamedata.GameEventCharge, p.EventHandlerCharge)
canvas.RegisterEvents(gamedata.GameEventNewShot, p.EventHandlerNewShot)
canvas.RegisterEvents(gamedata.GameEventTargetHit, p.EventHandlerTargetHit)
canvas.RegisterEvents(gamedata.GameEventExplosion, p.EventHandlerExplosion)
canvas.RegisterEvents(gamedata.GameEventFireball, p.EventHandlerFireball)
p.elements = append(p.elements, canvas)
//rainlayer
rain := gameelement.NewRainLayer(gamearea)
rain.Initialize()
p.elements = append(p.elements, rain)
//create foreground cloud layer
clouds := gameelement.NewCloudLayer(gamearea)
clouds.Initialize()
p.elements = append(p.elements, clouds)
return p
}
func (p *Primary) Update() error {
if !p.musicInitialized {
s := audio.NewInfiniteLoop(assets.SoundBank[assets.MainLoop], assets.SoundBank[assets.MainLoop].Length())
p.audioplayer, _ = audioContext.NewPlayer(s)
p.audioplayer.Play()
p.musicInitialized = true
}
//collect all inputs
inputs := p.CollectInputs()
if inputs.Quit {
p.events[EventEndgame]()
}
if inputs.Reset {
p.Reset()
}
p.ProcessEventAudio()
if inputs.Start {
if p.gameover {
p.Reset()
} else {
p.TogglePause()
}
}
//primary game loop, for each element pass along the inputs
//and process its update logic
if !p.paused {
for _, ge := range p.elements {
ge.SetInputs(inputs)
ge.Update()
}
}
return nil
}
func (p *Primary) Draw(screen *ebiten.Image) {
//here we simply call each game elements draw function
//as a layer on top of each other
for _, ge := range p.elements {
ge.Draw(screen)
}
}
func (p *Primary) SetEventHandler(e ScreenManagerEvent, f func()) {
p.events[e] = f
}
func (p *Primary) SetDimensions(a gamedata.Area) {
p.dimensions = a
}
func (p *Primary) CollectInputs() gamedata.GameInputs {
if inpututil.IsKeyJustPressed(ebiten.KeyQ) {
p.events[EventEndgame]()
}
gi := gamedata.GameInputs{}
//axes
inpx := ebiten.GamepadAxisValue(0, 0)
inpy := ebiten.GamepadAxisValue(0, 1)
//handle wasd input
if ebiten.IsKeyPressed(ebiten.KeyD) {
inpx = 1
}
if ebiten.IsKeyPressed(ebiten.KeyA) {
inpx = -1
}
if ebiten.IsKeyPressed(ebiten.KeyS) {
inpy = 1
}
if ebiten.IsKeyPressed(ebiten.KeyW) {
inpy = -1
}
gi.XAxis = inpx
gi.YAxis = inpy
xaxis := ebiten.StandardGamepadAxisValue(0, ebiten.StandardGamepadAxisRightStickHorizontal)
yaxis := ebiten.StandardGamepadAxisValue(0, ebiten.StandardGamepadAxisRightStickVertical)
if yaxis <= 0.09 && yaxis >= -0.09 {
yaxis = 0
}
if xaxis <= 0.09 && xaxis >= -0.09 {
xaxis = 0
}
gi.ShotAngle = math.Atan2(yaxis, xaxis)
gi.CycleWeapon = inpututil.IsStandardGamepadButtonJustPressed(0, ebiten.StandardGamepadButtonFrontTopRight)
gi.Charge = inpututil.IsStandardGamepadButtonJustPressed(0, ebiten.StandardGamepadButtonRightStick)
gi.Start = inpututil.IsStandardGamepadButtonJustPressed(0, ebiten.StandardGamepadButtonCenterRight)
gi.Shot = ebiten.IsStandardGamepadButtonPressed(0, ebiten.StandardGamepadButtonFrontBottomRight)
gi.Quit = inpututil.IsKeyJustPressed(ebiten.KeyQ)
gi.Reset = inpututil.IsKeyJustPressed(ebiten.KeyR)
return gi
}
func (p *Primary) TogglePause() {
p.paused = !p.paused
var player *audio.Player
if p.paused {
player = audioContext.NewPlayerFromBytes(assets.PauseIn)
p.audioplayer.Pause()
} else {
player = audioContext.NewPlayerFromBytes(assets.PauseOut)
p.audioplayer.Play()
}
player.Play()
}
func (p *Primary) Reset() {
p.paused = false
p.gameover = false
for _, ge := range p.elements {
ge.Initialize()
}
}
func (p *Primary) ProcessEventAudio() {
for event, occurred := range p.gameevents {
if occurred {
p.PlayAudio(event)
p.gameevents[event] = false
}
}
}
func (p *Primary) PlayAudio(e gamedata.GameEvent) {
switch e {
case gamedata.GameEventPlayerDeath:
player := audioContext.NewPlayerFromBytes(assets.HeroDeath)
player.Play()
case gamedata.GameEventCharge:
player := audioContext.NewPlayerFromBytes(assets.Magic)
player.Play()
case gamedata.GameEventNewShot:
player := audioContext.NewPlayerFromBytes(assets.Shot)
player.Play()
case gamedata.GameEventTargetHit:
player := audioContext.NewPlayerFromBytes(assets.TargetHit)
player.Play()
case gamedata.GameEventExplosion:
player := audioContext.NewPlayerFromBytes(assets.Splode)
player.Play()
case gamedata.GameEventFireball:
player := audioContext.NewPlayerFromBytes(assets.Flare)
player.Play()
}
}
func (p *Primary) EventHandlerPlayerDeath() {
p.gameevents[gamedata.GameEventPlayerDeath] = true
p.gameover = true
}
func (p *Primary) EventHandlerCharge() {
p.gameevents[gamedata.GameEventCharge] = true
}
func (p *Primary) EventHandlerNewShot() {
p.gameevents[gamedata.GameEventNewShot] = true
}
func (p *Primary) EventHandlerTargetHit() {
p.gameevents[gamedata.GameEventTargetHit] = true
}
func (p *Primary) EventHandlerExplosion() {
p.gameevents[gamedata.GameEventExplosion] = true
}
func (p *Primary) EventHandlerFireball() {
p.gameevents[gamedata.GameEventFireball] = true
}

25
screens/scene.go Normal file
View File

@@ -0,0 +1,25 @@
package screens
import (
"mover/gamedata"
"github.com/hajimehoshi/ebiten/v2"
)
type ScreenManagerEvent int
const (
EventNoop ScreenManagerEvent = iota
EventReset // reset to initial scene
EventLoad // loading elements
EventReload // reload current scene
EventCompleted // current scene has completed
EventEndgame // shutdown all scenes
)
type Screen interface {
Update() error
Draw(screen *ebiten.Image)
SetEventHandler(e ScreenManagerEvent, f func())
SetDimensions(a gamedata.Area)
}

102
screens/start.go Normal file
View File

@@ -0,0 +1,102 @@
package screens
import (
"image/color"
"math"
"mover/assets"
"mover/fonts"
"mover/gamedata"
"mover/touch"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/audio"
"github.com/hajimehoshi/ebiten/v2/inpututil"
"github.com/hajimehoshi/ebiten/v2/text"
)
type StartScreen struct {
eHandler map[ScreenManagerEvent]func()
dimensions gamedata.Area
target gamedata.Coordinates
current gamedata.Coordinates
targetreached bool
audioplayed bool
cycle int
audioplayer *audio.Player
}
func NewStartScreen() *StartScreen {
s := &StartScreen{
eHandler: make(map[ScreenManagerEvent]func()),
target: gamedata.Coordinates{X: 640/2 - 150, Y: 480 / 2},
current: gamedata.Coordinates{X: 640/2 - 150, Y: -100},
targetreached: false,
cycle: 0,
audioplayed: false,
}
return s
}
func (s *StartScreen) Update() error {
touch.UpdateTouchIDs()
ids := touch.GetTouchIDs()
var touched bool = false
for _, id := range ids {
touched = touch.IsTouchJustPressed(id)
if touched {
break
}
}
if inpututil.IsKeyJustPressed(ebiten.KeyEnter) ||
ebiten.IsStandardGamepadButtonPressed(0, ebiten.StandardGamepadButtonCenterRight) ||
touched {
s.eHandler[EventCompleted]()
if s.audioplayer.IsPlaying() {
s.audioplayer.Close()
}
}
if !s.audioplayed {
s.audioplayer = audioContext.NewPlayerFromBytes(assets.Survive)
s.audioplayer.Play()
s.audioplayed = true
}
if !s.targetreached {
s.current.X += (s.target.X - s.current.X) / 8
s.current.Y += (s.target.Y - s.current.Y) / 8
} else {
s.current.Y += 0.5 * math.Sin(float64(s.cycle)/(math.Pi*8))
}
if math.Abs(s.current.Y-s.target.Y) < 1 && !s.targetreached {
s.targetreached = true
}
s.cycle++
return nil
}
func (s *StartScreen) Draw(screen *ebiten.Image) {
screen.Clear()
screen.DrawImage(assets.ImageBank[assets.Title], nil)
text.Draw(screen, "survive", fonts.SurviveFont.ArcadeLarge, int(s.current.X), int(s.current.Y), color.Black)
if s.targetreached && (s.cycle/16)%4 < 2 {
text.Draw(screen, "press start", fonts.SurviveFont.Arcade, 640/2-25, 300, color.Black)
}
text.Draw(screen, "©bsoft games", fonts.SurviveFont.ArcadeSmall, 640/2+25, 180, color.Black)
}
func (s *StartScreen) SetEventHandler(e ScreenManagerEvent, f func()) {
s.eHandler[e] = f
}
func (s *StartScreen) SetDimensions(a gamedata.Area) {
s.dimensions = a
}

51
touch/touch.go Normal file
View File

@@ -0,0 +1,51 @@
package touch
import (
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil"
"golang.org/x/exp/maps"
)
var (
allTouchIDs []ebiten.TouchID
currentTouchIDs map[ebiten.TouchID]bool
justPressedTouchIDs map[ebiten.TouchID]bool
justReleasedTouchIDs map[ebiten.TouchID]bool
)
func UpdateTouchIDs() {
newPressedTouchIDs := []ebiten.TouchID{}
newPressedTouchIDs = inpututil.AppendJustPressedTouchIDs(newPressedTouchIDs)
justPressedTouchIDs = map[ebiten.TouchID]bool{}
for i := 0; i < len(newPressedTouchIDs); i++ {
justPressedTouchIDs[newPressedTouchIDs[i]] = true
currentTouchIDs[newPressedTouchIDs[i]] = true
}
justReleasedTouchIDs = map[ebiten.TouchID]bool{}
allTouchIDs = maps.Keys(currentTouchIDs)
newReleasedTouchIDs := []ebiten.TouchID{}
newReleasedTouchIDs = inpututil.AppendJustReleasedTouchIDs(newReleasedTouchIDs)
for i := 0; i < len(newReleasedTouchIDs); i++ {
justReleasedTouchIDs[newReleasedTouchIDs[i]] = true
delete(currentTouchIDs, newReleasedTouchIDs[i])
}
}
func GetTouchIDs() []ebiten.TouchID {
return allTouchIDs
}
func IsTouchJustPressed(touchID ebiten.TouchID) bool {
return justPressedTouchIDs[touchID]
}
func IsTouchJustReleased(touchID ebiten.TouchID) bool {
return justReleasedTouchIDs[touchID]
}
func init() {
allTouchIDs = []ebiten.TouchID{}
currentTouchIDs = map[ebiten.TouchID]bool{}
justPressedTouchIDs = map[ebiten.TouchID]bool{}
justReleasedTouchIDs = map[ebiten.TouchID]bool{}
}

26
weapons/gun.go Normal file
View File

@@ -0,0 +1,26 @@
package weapons
import "mover/gamedata"
type Gun struct {
active bool
}
func NewGun() *Gun {
g := &Gun{
active: false,
}
return g
}
func (g *Gun) IsActive() bool {
return g.active
}
func (g *Gun) SetActivity(active bool) {
g.active = active
}
func (g *Gun) GetWeaponType() gamedata.WeaponType {
return gamedata.WeaponTypeGun
}

59
weapons/holster.go Normal file
View File

@@ -0,0 +1,59 @@
package weapons
import "mover/gamedata"
type Holster struct {
activewp gamedata.WeaponType
guns map[gamedata.WeaponType]Weapon
}
func NewHolster() *Holster {
holster := &Holster{
guns: make(map[gamedata.WeaponType]Weapon),
activewp: gamedata.WeaponTypeGun,
}
holster.AddWeapon(NewGun())
//holster.AddWeapon(NewLaser())
return holster
}
func (h *Holster) SetActiveWeapon(wt gamedata.WeaponType) {
_, ok := h.guns[wt]
if ok {
h.activewp = wt
}
}
func (h *Holster) GetActiveWeapon() Weapon {
return h.guns[h.activewp]
}
func (h *Holster) GetActiveWeaponType() gamedata.WeaponType {
return h.guns[h.activewp].GetWeaponType()
}
func (h *Holster) AddWeapon(w Weapon) {
_, ok := h.guns[w.GetWeaponType()]
if !ok {
h.guns[w.GetWeaponType()] = w
}
}
func (h *Holster) CycleWeapon() {
//no weapons, nothing to do
if len(h.guns) == 0 {
return
}
//keep searching until we find the next weapon that exists
var nextwp gamedata.WeaponType = h.activewp
for ok := false; !ok; {
nextwp = (nextwp + 1) % gamedata.WeaponTypeMax
_, ok = h.guns[nextwp]
if ok {
h.activewp = nextwp
}
}
}

26
weapons/laser.go Normal file
View File

@@ -0,0 +1,26 @@
package weapons
import "mover/gamedata"
type Laser struct {
active bool
}
func NewLaser() *Laser {
l := &Laser{
active: false,
}
return l
}
func (g *Laser) IsActive() bool {
return g.active
}
func (g *Laser) SetActivity(active bool) {
g.active = active
}
func (g *Laser) GetWeaponType() gamedata.WeaponType {
return gamedata.WeaponTypeLaser
}

9
weapons/weapon.go Normal file
View File

@@ -0,0 +1,9 @@
package weapons
import "mover/gamedata"
type Weapon interface {
IsActive() bool
SetActivity(bool)
GetWeaponType() gamedata.WeaponType
}