Compare commits

..

6 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
20 changed files with 1143 additions and 15 deletions

View File

@@ -27,6 +27,10 @@ const (
Worm ImgAssetName = "WormDefault" Worm ImgAssetName = "WormDefault"
Cloud ImgAssetName = "Cloud" Cloud ImgAssetName = "Cloud"
Fireball ImgAssetName = "Fireball" Fireball ImgAssetName = "Fireball"
Splash ImgAssetName = "Splash"
LaserBeam ImgAssetName = "LaserBeam"
ItemLaser ImgAssetName = "ItemLaser"
RainSplash ImgAssetName = "RainSplash"
) )
var ( var (
@@ -60,6 +64,14 @@ var (
cloud_img []byte cloud_img []byte
//go:embed hot.png //go:embed hot.png
fireball_img []byte 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() { func LoadImages() {
@@ -79,6 +91,10 @@ func LoadImages() {
ImageBank[Worm] = LoadImagesFatal(wormdefault_img) ImageBank[Worm] = LoadImagesFatal(wormdefault_img)
ImageBank[Cloud] = LoadImagesFatal(cloud_img) ImageBank[Cloud] = LoadImagesFatal(cloud_img)
ImageBank[Fireball] = LoadImagesFatal(fireball_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)
} }

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/rain-splash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

BIN
assets/splash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 615 B

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
}

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

@@ -1,12 +1,13 @@
package gamedata package gamedata
type GameInputs struct { type GameInputs struct {
XAxis float64 XAxis float64
YAxis float64 YAxis float64
ShotAngle float64 ShotAngle float64
Shot bool Shot bool
Start bool Start bool
Charge bool Charge bool
Quit bool Quit bool
Reset 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
)

View File

@@ -2,6 +2,7 @@ package gameelement
import ( import (
"fmt" "fmt"
"image"
"image/color" "image/color"
"math" "math"
"math/rand/v2" "math/rand/v2"
@@ -9,6 +10,7 @@ import (
"mover/elements" "mover/elements"
"mover/fonts" "mover/fonts"
"mover/gamedata" "mover/gamedata"
"mover/weapons"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/text" "github.com/hajimehoshi/ebiten/v2/text"
@@ -19,6 +21,7 @@ type Canvas struct {
Sprite *ebiten.Image Sprite *ebiten.Image
collisionMask *ebiten.Image collisionMask *ebiten.Image
projectileMask *ebiten.Image projectileMask *ebiten.Image
laserMask *ebiten.Image
heroCollisionMask *ebiten.Image heroCollisionMask *ebiten.Image
heroCollisionCpy *ebiten.Image heroCollisionCpy *ebiten.Image
@@ -31,12 +34,18 @@ type Canvas struct {
runtime float64 runtime float64
counter int counter int
score int score int
splashes []*elements.Splash
wpdrops []*elements.WeaponDrop
hero *elements.Hero hero *elements.Hero
charge *elements.Explosion charge *elements.Explosion
goblin *elements.FlyGoblin goblin *elements.FlyGoblin
enemies []elements.Enemies enemies []elements.Enemies
projectiles []*elements.Projectile projectiles []*elements.Projectile
laser *elements.Laser
gameover bool gameover bool
lasercoords []gamedata.Coordinates
holster *weapons.Holster
} }
func NewCanvas(a gamedata.Area) *Canvas { func NewCanvas(a gamedata.Area) *Canvas {
@@ -44,10 +53,12 @@ func NewCanvas(a gamedata.Area) *Canvas {
Sprite: ebiten.NewImage(a.Width, a.Height), Sprite: ebiten.NewImage(a.Width, a.Height),
projectileMask: ebiten.NewImage(a.Width, a.Height), projectileMask: ebiten.NewImage(a.Width, a.Height),
collisionMask: 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), heroCollisionMask: ebiten.NewImage(48, 48),
heroCollisionCpy: ebiten.NewImage(48, 48), heroCollisionCpy: ebiten.NewImage(48, 48),
hero: elements.NewHero(), hero: elements.NewHero(),
charge: elements.NewExplosion(), charge: elements.NewExplosion(),
laser: elements.NewLaser(gamedata.Coordinates{X: 320, Y: 240}, 0),
initialized: false, initialized: false,
gameover: false, gameover: false,
goblinspawned: false, goblinspawned: false,
@@ -55,8 +66,11 @@ func NewCanvas(a gamedata.Area) *Canvas {
score: 0, score: 0,
runtime: 0., runtime: 0.,
counter: 0, counter: 0,
holster: weapons.NewHolster(),
} }
c.laserMask.Clear()
c.eventmap = make(map[gamedata.GameEvent]func()) c.eventmap = make(map[gamedata.GameEvent]func())
c.lasercoords = make([]gamedata.Coordinates, 4)
return c return c
} }
@@ -69,11 +83,16 @@ func (c *Canvas) Update() error {
c.Initialize() c.Initialize()
} else { } else {
c.UpdateHero() c.UpdateHero()
c.UpdateWeaponDrops()
c.UpdateWeapons()
c.UpdateProjectiles() c.UpdateProjectiles()
c.UpdateCharge() c.UpdateCharge()
c.UpdateEnemies() c.UpdateEnemies()
c.SpawnEnemies() c.SpawnEnemies()
c.CleanupTargets() c.CleanupTargets()
c.UpdateSplashes()
c.CleanSplashes()
c.CleanupDrops()
c.counter++ c.counter++
} }
@@ -83,14 +102,17 @@ func (c *Canvas) Update() error {
func (c *Canvas) Draw(drawimg *ebiten.Image) { func (c *Canvas) Draw(drawimg *ebiten.Image) {
c.Sprite.Clear() c.Sprite.Clear()
c.projectileMask.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) //vector.DrawFilledCircle(c.Sprite, float32(c.hero.Pos.X), float32(c.hero.Pos.Y), 100, color.White, true)
//render heor
c.hero.Draw() c.hero.Draw()
op := &ebiten.DrawImageOptions{} op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(c.hero.Pos.X-48/2, c.hero.Pos.Y-48/2) op.GeoM.Translate(c.hero.Pos.X-48/2, c.hero.Pos.Y-48/2)
c.Sprite.DrawImage(c.hero.Sprite, op) c.Sprite.DrawImage(c.hero.Sprite, op)
//render weapon
if !c.gameover { if !c.gameover {
op.GeoM.Reset() op.GeoM.Reset()
op.GeoM.Translate(0, -16) op.GeoM.Translate(0, -16)
@@ -116,6 +138,14 @@ func (c *Canvas) Draw(drawimg *ebiten.Image) {
} }
} }
//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 //draw enemies
for _, e := range c.enemies { for _, e := range c.enemies {
e.Draw() e.Draw()
@@ -143,15 +173,32 @@ func (c *Canvas) Draw(drawimg *ebiten.Image) {
} }
} }
//draw projectiles
for _, p := range c.projectiles { for _, p := range c.projectiles {
//drawimg.DrawImage()
vector.DrawFilledCircle(c.projectileMask, float32(p.Pos.X), float32(p.Pos.Y), 3, color.White, true) vector.DrawFilledCircle(c.projectileMask, float32(p.Pos.X), float32(p.Pos.Y), 3, color.White, true)
} }
c.Sprite.DrawImage(c.projectileMask, nil) 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) 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 { if !c.gameover {
c.runtime = float64(c.counter) / 60. c.runtime = float64(c.counter) / 60.
} }
@@ -175,6 +222,8 @@ func (c *Canvas) Draw(drawimg *ebiten.Image) {
func (c *Canvas) Initialize() { func (c *Canvas) Initialize() {
c.InitializeHero() c.InitializeHero()
c.CleanSplashes()
c.ResetWeaponDrops()
c.enemies = c.enemies[:0] c.enemies = c.enemies[:0]
c.gameover = false c.gameover = false
c.initialized = true c.initialized = true
@@ -186,6 +235,9 @@ func (c *Canvas) Initialize() {
//temporary //temporary
c.hero.Action = elements.HeroActionDefault c.hero.Action = elements.HeroActionDefault
c.holster.SetActiveWeapon(gamedata.WeaponTypeGun)
c.laser.SetFiring(false)
} }
func (c *Canvas) UpdateHero() { func (c *Canvas) UpdateHero() {
@@ -193,7 +245,6 @@ func (c *Canvas) UpdateHero() {
if !c.gameover { if !c.gameover {
c.UpdateHeroPosition() c.UpdateHeroPosition()
c.ComputeHeroCollisions() c.ComputeHeroCollisions()
c.AddProjectiles()
} }
} }
@@ -241,13 +292,16 @@ func (c *Canvas) ComputeHeroCollisions() {
func (c *Canvas) AddProjectiles() { func (c *Canvas) AddProjectiles() {
//add new projectiles //add new projectiles
if c.lastInputs.Shot && c.counter%14 == 0 { if c.lastInputs.Shot && c.counter%14 == 0 {
loc := gamedata.Coordinates{ loc := gamedata.Coordinates{
X: c.hero.Pos.X, X: c.hero.Pos.X,
Y: c.hero.Pos.Y, Y: c.hero.Pos.Y,
} }
angle := c.lastInputs.ShotAngle angle := c.lastInputs.ShotAngle
velocity := 5. velocity := 5.
c.projectiles = append(c.projectiles, elements.NewProjectile(loc, angle, velocity)) c.projectiles = append(c.projectiles, elements.NewProjectile(loc, angle, velocity))
if c.hero.Upgrade { if c.hero.Upgrade {
@@ -257,6 +311,7 @@ func (c *Canvas) AddProjectiles() {
if c.eventmap[gamedata.GameEventNewShot] != nil { if c.eventmap[gamedata.GameEventNewShot] != nil {
c.eventmap[gamedata.GameEventNewShot]() c.eventmap[gamedata.GameEventNewShot]()
} }
} }
} }
@@ -282,8 +337,8 @@ func (c *Canvas) UpdateProjectiles() {
} }
for _, e := range c.enemies { for _, e := range c.enemies {
if p.Pos.X >= e.GetPosition().X-48/2 && p.Pos.X <= e.GetPosition().X+48/2 && 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-48/2 && p.Pos.Y <= e.GetPosition().Y+48/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 { e.IsToggled() && e.GetEnemyState() < gamedata.EnemyStateDying {
c.collisionMask.Clear() c.collisionMask.Clear()
c.collisionMask.DrawImage(c.projectileMask, nil) c.collisionMask.DrawImage(c.projectileMask, nil)
@@ -318,6 +373,32 @@ func (c *Canvas) UpdateProjectiles() {
} }
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() { func (c *Canvas) UpdateCharge() {
if c.lastInputs.Charge && !c.charge.Active && !c.gameover { if c.lastInputs.Charge && !c.charge.Active && !c.gameover {
@@ -467,8 +548,20 @@ func (c *Canvas) CleanupTargets() {
// remove dead targets by iterating over all targets // remove dead targets by iterating over all targets
i := 0 i := 0
for _, e := range c.enemies { 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 //moving valid targets to the front of the slice
if e.GetEnemyState() < elements.MoverActionDead { 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 c.enemies[i] = e
i++ i++
} }
@@ -513,3 +606,383 @@ func (c *Canvas) GoblinFireballEvent() {
} }
} }
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()
}

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]
}

View File

@@ -26,7 +26,7 @@ func NewManager() Manager {
return Manager{ return Manager{
Info: gamedata.GameInfo{ Info: gamedata.GameInfo{
Name: "survive", Name: "survive",
Version: "0.30", Version: "0.34",
Dimensions: gamedata.Area{ Dimensions: gamedata.Area{
Width: defaultWidth, Width: defaultWidth,
Height: defaultHeight, Height: defaultHeight,

View File

@@ -50,6 +50,11 @@ func NewPrimary() *Primary {
canvas.RegisterEvents(gamedata.GameEventFireball, p.EventHandlerFireball) canvas.RegisterEvents(gamedata.GameEventFireball, p.EventHandlerFireball)
p.elements = append(p.elements, canvas) p.elements = append(p.elements, canvas)
//rainlayer
rain := gameelement.NewRainLayer(gamearea)
rain.Initialize()
p.elements = append(p.elements, rain)
//create foreground cloud layer //create foreground cloud layer
clouds := gameelement.NewCloudLayer(gamearea) clouds := gameelement.NewCloudLayer(gamearea)
clouds.Initialize() clouds.Initialize()
@@ -155,6 +160,7 @@ func (p *Primary) CollectInputs() gamedata.GameInputs {
gi.ShotAngle = math.Atan2(yaxis, xaxis) gi.ShotAngle = math.Atan2(yaxis, xaxis)
gi.CycleWeapon = inpututil.IsStandardGamepadButtonJustPressed(0, ebiten.StandardGamepadButtonFrontTopRight)
gi.Charge = inpututil.IsStandardGamepadButtonJustPressed(0, ebiten.StandardGamepadButtonRightStick) gi.Charge = inpututil.IsStandardGamepadButtonJustPressed(0, ebiten.StandardGamepadButtonRightStick)
gi.Start = inpututil.IsStandardGamepadButtonJustPressed(0, ebiten.StandardGamepadButtonCenterRight) gi.Start = inpututil.IsStandardGamepadButtonJustPressed(0, ebiten.StandardGamepadButtonCenterRight)
gi.Shot = ebiten.IsStandardGamepadButtonPressed(0, ebiten.StandardGamepadButtonFrontBottomRight) gi.Shot = ebiten.IsStandardGamepadButtonPressed(0, ebiten.StandardGamepadButtonFrontBottomRight)

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
}