diff --git a/assets/imagebank.go b/assets/imagebank.go index 00854df..cadfdb6 100644 --- a/assets/imagebank.go +++ b/assets/imagebank.go @@ -27,6 +27,7 @@ const ( Worm ImgAssetName = "WormDefault" Cloud ImgAssetName = "Cloud" Fireball ImgAssetName = "Fireball" + Splash ImgAssetName = "Splash" ) var ( @@ -60,6 +61,8 @@ var ( cloud_img []byte //go:embed hot.png fireball_img []byte + //go:embed splash.png + splash_img []byte ) func LoadImages() { @@ -79,6 +82,7 @@ func LoadImages() { ImageBank[Worm] = LoadImagesFatal(wormdefault_img) ImageBank[Cloud] = LoadImagesFatal(cloud_img) ImageBank[Fireball] = LoadImagesFatal(fireball_img) + ImageBank[Splash] = LoadImagesFatal(splash_img) } diff --git a/assets/splash.png b/assets/splash.png new file mode 100644 index 0000000..e09a4a3 Binary files /dev/null and b/assets/splash.png differ diff --git a/elements/laser.go b/elements/laser.go new file mode 100644 index 0000000..9b1f1ab --- /dev/null +++ b/elements/laser.go @@ -0,0 +1,60 @@ +package elements + +import ( + "image/color" + "mover/gamedata" + + "github.com/hajimehoshi/ebiten/v2" +) + +type Laser struct { + Sprite *ebiten.Image + position gamedata.Coordinates + angle float64 + cycle int + firing bool +} + +func NewLaser(pos gamedata.Coordinates, angle float64) *Laser { + l := &Laser{ + Sprite: ebiten.NewImage(200, 20), + angle: angle, + cycle: 0, + position: pos, + firing: false, + } + return l +} + +func (l *Laser) Update() error { + l.cycle++ + return nil +} + +func (l *Laser) Draw() { + l.Sprite.Clear() + l.Sprite.Fill(color.White) +} + +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 +} diff --git a/elements/splash.go b/elements/splash.go new file mode 100644 index 0000000..854e380 --- /dev/null +++ b/elements/splash.go @@ -0,0 +1,92 @@ +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) + } + +} + +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 +} diff --git a/gameelement/canvas.go b/gameelement/canvas.go index 4daccaa..cd5bba3 100644 --- a/gameelement/canvas.go +++ b/gameelement/canvas.go @@ -2,6 +2,7 @@ package gameelement import ( "fmt" + "image" "image/color" "math" "math/rand/v2" @@ -19,6 +20,7 @@ type Canvas struct { Sprite *ebiten.Image collisionMask *ebiten.Image projectileMask *ebiten.Image + laserMask *ebiten.Image heroCollisionMask *ebiten.Image heroCollisionCpy *ebiten.Image @@ -31,12 +33,16 @@ type Canvas struct { runtime float64 counter int score int + splashes []*elements.Splash hero *elements.Hero charge *elements.Explosion goblin *elements.FlyGoblin enemies []elements.Enemies projectiles []*elements.Projectile + laser *elements.Laser gameover bool + + lasercoords []gamedata.Coordinates } func NewCanvas(a gamedata.Area) *Canvas { @@ -44,10 +50,12 @@ func NewCanvas(a gamedata.Area) *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, @@ -56,7 +64,9 @@ func NewCanvas(a gamedata.Area) *Canvas { runtime: 0., counter: 0, } + c.laserMask.Clear() c.eventmap = make(map[gamedata.GameEvent]func()) + c.lasercoords = make([]gamedata.Coordinates, 4) return c } @@ -70,10 +80,13 @@ func (c *Canvas) Update() error { } else { c.UpdateHero() c.UpdateProjectiles() + c.UpdateLaser() c.UpdateCharge() c.UpdateEnemies() c.SpawnEnemies() c.CleanupTargets() + c.UpdateSplashes() + c.CleanSplashes() c.counter++ } @@ -83,14 +96,17 @@ func (c *Canvas) Update() error { 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) @@ -143,15 +159,32 @@ func (c *Canvas) Draw(drawimg *ebiten.Image) { } } + //draw projectiles for _, p := range c.projectiles { - //drawimg.DrawImage() 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. } @@ -175,6 +208,7 @@ func (c *Canvas) Draw(drawimg *ebiten.Image) { func (c *Canvas) Initialize() { c.InitializeHero() + c.CleanSplashes() c.enemies = c.enemies[:0] c.gameover = false c.initialized = true @@ -241,22 +275,31 @@ func (c *Canvas) ComputeHeroCollisions() { 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.lastInputs.Shot && c.counter%14 == 0 { - if c.hero.Upgrade { - c.projectiles = append(c.projectiles, elements.NewProjectile(loc, angle+math.Pi, velocity)) - } + 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]() + } - if c.eventmap[gamedata.GameEventNewShot] != nil { - c.eventmap[gamedata.GameEventNewShot]() } + */ + if c.lastInputs.Shot { + c.laser.SetPosition(c.hero.Pos) + c.laser.SetAngle(c.lastInputs.ShotAngle) } } @@ -282,8 +325,8 @@ func (c *Canvas) UpdateProjectiles() { } for _, e := range c.enemies { - if p.Pos.X >= e.GetPosition().X-48/2 && p.Pos.X <= e.GetPosition().X+48/2 && - p.Pos.Y >= e.GetPosition().Y-48/2 && p.Pos.Y <= e.GetPosition().Y+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-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) @@ -318,6 +361,27 @@ func (c *Canvas) UpdateProjectiles() { } +func (c *Canvas) UpdateLaser() { + + c.laser.SetFiring(c.lastInputs.Shot) + if c.lastInputs.Shot { + 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 { @@ -468,7 +532,9 @@ func (c *Canvas) CleanupTargets() { i := 0 for _, e := range c.enemies { //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 i++ } @@ -513,3 +579,308 @@ 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 <= 50 && 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] +}