diff --git a/game.go b/game.go index 43fb693..13f3c23 100644 --- a/game.go +++ b/game.go @@ -10,6 +10,7 @@ import ( "os" "path" "runtime/pprof" + "sync" "time" "github.com/hajimehoshi/ebiten/v2" @@ -116,7 +117,8 @@ type game struct { flashMessageText string flashMessageUntil time.Time - forceColorScale float64 + minLevelColorScale float64 + minPlayerColorScale float64 godMode bool noclipMode bool @@ -124,6 +126,8 @@ type game struct { debugMode bool fullBrightMode bool cpuProfile *os.File + + sync.Mutex } const sampleRate = 44100 @@ -131,13 +135,14 @@ const sampleRate = 44100 // NewGame returns a new isometric demo game. func NewGame() (*game, error) { g := &game{ - camScale: 2, - camScaleTo: 2, - mousePanX: math.MinInt32, - mousePanY: math.MinInt32, - activeGamepad: -1, - forceColorScale: -1, - levelNum: 1, + camScale: 2, + camScaleTo: 2, + mousePanX: math.MinInt32, + mousePanY: math.MinInt32, + activeGamepad: -1, + minLevelColorScale: -1, + minPlayerColorScale: -1, + levelNum: 1, op: &ebiten.DrawImageOptions{}, } @@ -286,7 +291,8 @@ func (g *game) reset() error { g.updateCursor() - g.forceColorScale = -1 + g.minLevelColorScale = -1 + g.minPlayerColorScale = -1 g.player.hasTorch = true g.player.weapon = weaponUzi @@ -359,10 +365,10 @@ func (g *game) handlePlayerDeath() { } func (g *game) checkLevelComplete() { - if g.player.soulsRescued < g.level.requiredSouls || g.level.exitOpen { + if g.player.soulsRescued < g.level.requiredSouls || !g.level.exitOpenTime.IsZero() { return } - g.level.exitOpen = true + g.level.exitOpenTime = time.Now() g.level.tiles[g.level.exitY][g.level.exitX].sprites = nil g.level.tiles[g.level.exitY][g.level.exitX].AddSprite(sandstoneSS.FloorA) @@ -374,6 +380,9 @@ func (g *game) checkLevelComplete() { // Update reads current user input and updates the game state. func (g *game) Update() error { + g.Lock() + defer g.Unlock() + gamepadDeadZone := 0.1 if ebiten.IsKeyPressed(ebiten.KeyEscape) || ebiten.IsWindowBeingClosed() { @@ -494,13 +503,6 @@ func (g *game) Update() error { } g.level.liveCreeps = liveCreeps - // Clamp target zoom level. - /*if g.camScaleTo < 2 { - g.camScaleTo = 2 - } else if g.camScaleTo > 4 { - g.camScaleTo = 4 - } TODO */ - // Update target zoom level. if g.debugMode { var scrollY float64 @@ -883,6 +885,9 @@ func (g *game) drawText(target *ebiten.Image, offsetX float64, y float64, scale // Draw draws the game on the screen. func (g *game) Draw(screen *ebiten.Image) { + g.Lock() + defer g.Unlock() + if g.gameStartTime.IsZero() { screen.Fill(colorBlood) @@ -915,18 +920,22 @@ func (g *game) Draw(screen *ebiten.Image) { } drawn = g.renderLevel(screen) } else { + drawn += g.drawProjectiles(screen) + + drawn += g.drawPlayer(screen) + // Draw game over screen. img := ebiten.NewImage(g.w, g.h) img.Fill(colorBlood) - a := g.forceColorScale + a := g.minLevelColorScale if a == -1 { a = 1 } g.op.GeoM.Reset() g.op.ColorM.Reset() - g.op.ColorM.Scale(a, a, a, 1) + g.op.ColorM.Scale(a, a, a, a) screen.DrawImage(img, g.op) g.op.ColorM.Reset() @@ -935,7 +944,6 @@ func (g *game) Draw(screen *ebiten.Image) { if time.Since(g.gameOverTime).Milliseconds()%2000 < 1500 { g.drawText(screen, 0, 8, 4, a, "PRESS ENTER OR START TO PLAY AGAIN") } - } if g.gameOverTime.IsZero() { @@ -959,7 +967,7 @@ func (g *game) Draw(screen *ebiten.Image) { scale := 3.0 soulsY := 104.0 - if !g.level.exitOpen { + if g.level.exitOpenTime.IsZero() { // Draw souls. soulsLabel := fmt.Sprintf("%d", g.level.requiredSouls-g.player.soulsRescued) @@ -970,8 +978,9 @@ func (g *game) Draw(screen *ebiten.Image) { g.drawText(screen, 0, soulsY, scale, 1.0, soulsLabel) } else { // Draw exit message. - // TODO flash text - g.drawText(screen, 0, soulsY, scale, 1.0, "EXIT OPEN") + if time.Since(g.level.exitOpenTime).Milliseconds()%2000 < 1500 { + g.drawText(screen, 0, soulsY, scale, 1.0, "EXIT OPEN") + } } } @@ -985,7 +994,7 @@ func (g *game) Draw(screen *ebiten.Image) { } if !g.gameWon { - a := g.forceColorScale + a := g.minLevelColorScale if a == -1 { a = 1 } @@ -1013,9 +1022,9 @@ func (g *game) tilePosition(x, y float64) (float64, float64) { } // renderSprite renders a sprite on the screen. -func (g *game) renderSprite(x float64, y float64, offsetx float64, offsety float64, angle float64, scale float64, colorScale float64, alpha float64, sprite *ebiten.Image, target *ebiten.Image) int { - if g.forceColorScale != -1 { - colorScale = g.forceColorScale +func (g *game) renderSprite(x float64, y float64, offsetx float64, offsety float64, angle float64, geoScale float64, colorScale float64, alpha float64, sprite *ebiten.Image, target *ebiten.Image) int { + if g.minLevelColorScale != -1 && colorScale < g.minLevelColorScale { + colorScale = g.minLevelColorScale } if alpha < .01 || colorScale < .01 { @@ -1033,7 +1042,7 @@ func (g *game) renderSprite(x float64, y float64, offsetx float64, offsety float g.op.GeoM.Reset() - g.op.GeoM.Scale(scale, scale) + g.op.GeoM.Scale(geoScale, geoScale) // Rotate g.op.GeoM.Translate(-16+offsetx, -16+offsety) g.op.GeoM.Rotate(angle) @@ -1057,81 +1066,48 @@ func (g *game) renderSprite(x float64, y float64, offsetx float64, offsety float } // Calculate color scale to apply shadows. -func (g *game) colorScale(x, y float64) float64 { - if g.gameWon || g.fullBrightMode { +func (g *game) levelColorScale(x, y float64) float64 { + if g.fullBrightMode { return 1 } - v := colorScaleValue(x, y, g.player.x, g.player.y) + var v float64 + if g.player.hasTorch { + v = colorScaleValue(x, y, g.player.x, g.player.y) + } - tileV := g.level.Tile(int(x), int(y)).colorScale + t := g.level.Tile(int(x), int(y)) + if t == nil { + return 0 + } + tileV := t.colorScale s := math.Min(1, v+tileV) return s } -// renderLevel draws the current Level on the screen. -func (g *game) renderLevel(screen *ebiten.Image) int { +func (g *game) drawProjectiles(screen *ebiten.Image) int { var drawn int - - drawCreeps := func() { - for _, c := range g.level.creeps { - if c.health == 0 && c.creepType != TypeTorch { - continue - } - - a := 1.0 - if c.creepType == TypeSoul { - a = 0.35 - } - - drawn += g.renderSprite(c.x, c.y, 0, 0, c.angle, 1.0, g.colorScale(c.x, c.y), a, c.sprites[c.frame], screen) - if c.frames > 1 && time.Since(c.lastFrame) >= 75*time.Millisecond { - c.frame++ - if c.frame == c.frames { - c.frame = 0 - } - c.lastFrame = time.Now() - } - } - - } - - var t *Tile - for y := 0; y < g.level.h; y++ { - for x := 0; x < g.level.w; x++ { - t = g.level.tiles[y][x] - if t == nil { - continue // No tile at this position. - } - - for i := range t.sprites { - drawn += g.renderSprite(float64(x), float64(y), 0, 0, 0, 1.0, g.colorScale(float64(x), float64(y)), 1.0, t.sprites[i], screen) - } - } - } - - for _, item := range g.level.items { - if item.health == 0 { - continue - } - - drawn += g.renderSprite(item.x, item.y, 0, 0, 0, 1.0, g.colorScale(item.x, item.y), 1.0, item.sprite, screen) - } - - if !g.gameWon { - drawCreeps() - } - for _, p := range g.projectiles { colorScale := p.colorScale if colorScale == 1 { - colorScale = g.colorScale(p.x, p.y) + colorScale = g.levelColorScale(p.x, p.y) } - drawn += g.renderSprite(p.x, p.y, 0, 0, p.angle, 1.0, colorScale, 1.0, imageAtlas[ImageBullet], screen) + alpha := 1.0 + if g.gameWon { + //alpha = g.minLevelColorScale + } + // TODO if colorscale and gamewon, alpha is colorscale + + drawn += g.renderSprite(p.x, p.y, 0, 0, p.angle, 1.0, colorScale, alpha, imageAtlas[ImageBullet], screen) } + return drawn +} + +func (g *game) drawPlayer(screen *ebiten.Image) int { + var drawn int repelTime := g.player.garlicUntil.Sub(time.Now()) if repelTime > 0 && repelTime < 7*time.Second { @@ -1155,6 +1131,11 @@ func (g *game) renderLevel(screen *ebiten.Image) int { drawn += g.renderSprite(g.player.x+0.25, g.player.y+0.25, -offset, -offset, 0, scale, 1.0, alpha, imageAtlas[ImageHolyWater], screen) } + var playerColorScale = g.levelColorScale(g.player.x, g.player.y) + if g.minPlayerColorScale != -1 { + playerColorScale = g.minPlayerColorScale + } + var weaponSprite *ebiten.Image playerSprite := playerSS.Frame1 @@ -1163,7 +1144,7 @@ func (g *game) renderLevel(screen *ebiten.Image) int { if g.player.weapon != nil { weaponSprite = g.player.weapon.spriteFlipped } - if g.player.angle > math.Pi/2 || g.player.angle < -1*math.Pi/2 { + if (g.player.angle > math.Pi/2 || g.player.angle < -1*math.Pi/2) && (g.gameOverTime.IsZero() || time.Since(g.gameOverTime) < 7*time.Second) { playerSprite = playerSS.Frame2 playerAngle = playerAngle - math.Pi mul = -1 @@ -1171,19 +1152,84 @@ func (g *game) renderLevel(screen *ebiten.Image) int { weaponSprite = g.player.weapon.sprite } } - drawn += g.renderSprite(g.player.x, g.player.y, 0, 0, playerAngle, 1.0, 1.0, 1.0, playerSprite, screen) + drawn += g.renderSprite(g.player.x, g.player.y, 0, 0, playerAngle, 1.0, playerColorScale, 1.0, playerSprite, screen) if g.player.weapon != nil { - drawn += g.renderSprite(g.player.x, g.player.y, 11*mul, 9, playerAngle, 1.0, 1.0, 1.0, weaponSprite, screen) + drawn += g.renderSprite(g.player.x, g.player.y, 11*mul, 9, playerAngle, 1.0, playerColorScale, 1.0, weaponSprite, screen) } if g.player.hasTorch { - drawn += g.renderSprite(g.player.x, g.player.y, -10*mul, 2, playerAngle, 1.0, 1.0, 1.0, sandstoneSS.TorchMulti, screen) + drawn += g.renderSprite(g.player.x, g.player.y, -10*mul, 2, playerAngle, 1.0, playerColorScale, 1.0, sandstoneSS.TorchMulti, screen) } flashDuration := 40 * time.Millisecond if g.player.weapon != nil && time.Since(g.player.weapon.lastFire) < flashDuration { - drawn += g.renderSprite(g.player.x, g.player.y, 39, -1, g.player.angle, 1.0, 1.0, 1.0, imageAtlas[ImageMuzzleFlash], screen) + drawn += g.renderSprite(g.player.x, g.player.y, 39, -1, g.player.angle, 1.0, playerColorScale, 1.0, imageAtlas[ImageMuzzleFlash], screen) } + return drawn +} + +// renderLevel draws the current Level on the screen. +func (g *game) renderLevel(screen *ebiten.Image) int { + var drawn int + + drawCreeps := func() { + for _, c := range g.level.creeps { + if c.health == 0 && c.creepType != TypeTorch { + continue + } + + a := 1.0 + if c.creepType == TypeSoul { + a = 0.35 + } + + drawn += g.renderSprite(c.x, c.y, 0, 0, c.angle, 1.0, g.levelColorScale(c.x, c.y), a, c.sprites[c.frame], screen) + if c.frames > 1 && time.Since(c.lastFrame) >= 75*time.Millisecond { + c.frame++ + if c.frame == c.frames { + c.frame = 0 + } + c.lastFrame = time.Now() + } + } + } + + if g.gameWon { + drawn += g.drawProjectiles(screen) + } + + var t *Tile + for y := 0; y < g.level.h; y++ { + for x := 0; x < g.level.w; x++ { + t = g.level.tiles[y][x] + if t == nil { + continue // No tile at this position. + } + + for i := range t.sprites { + drawn += g.renderSprite(float64(x), float64(y), 0, 0, 0, 1.0, g.levelColorScale(float64(x), float64(y)), 1.0, t.sprites[i], screen) + } + } + } + + for _, item := range g.level.items { + if item.health == 0 { + continue + } + + drawn += g.renderSprite(item.x, item.y, 0, 0, 0, 1.0, g.levelColorScale(item.x, item.y), 1.0, item.sprite, screen) + } + + if !g.gameWon { + drawCreeps() + } + + if !g.gameWon { + drawn += g.drawProjectiles(screen) + } + + drawn += g.drawPlayer(screen) + if g.gameWon { drawCreeps() } @@ -1334,9 +1380,7 @@ func (g *game) showWinScreen() { return } - // TODO make it a sunrise instead - - g.forceColorScale = 0.4 + g.minLevelColorScale = 0.4 g.gameWon = true g.gameOverTime = time.Now() @@ -1349,8 +1393,6 @@ func (g *game) showWinScreen() { g.level = newWinLevel(g.player) - // TODO move win screen background upward - g.winScreenBackground = ebiten.NewImage(g.w, g.h) g.winScreenBackground.Fill(colornames.Deepskyblue) @@ -1360,6 +1402,207 @@ func (g *game) showWinScreen() { g.winScreenSunY = float64(g.h/2) + float64(sunSize/2) + go func() { + p := g.player + l := g.level + + var stars []*projectile + + addStar := func() { + star := &projectile{ + x: p.x + (0.5-rand.Float64())*66, + y: p.y + (0.5-rand.Float64())*66, + colorScale: rand.Float64(), + } + g.projectiles = append(g.projectiles, star) + stars = append(stars, star) + } + + lastPlayerX := p.x + updateStars := func() { + if p.x == lastPlayerX { + return + } + + for _, star := range stars { + star.x = p.x - (lastPlayerX - star.x) + } + lastPlayerX = p.x + } + + // Add stars. + numStars := 666 + for i := 0; i < numStars; i++ { + addStar() + } + + // Walk away. + for i := 0; i < 36; i++ { + p.x += 0.05 + updateStars() + time.Sleep(time.Second / 144) + } + for i := 0; i < 288; i++ { + p.x += 0.05 * (float64(288-i) / 288) + updateStars() + + time.Sleep(time.Second / 144) + } + + // Turn around. + p.angle = math.Pi + time.Sleep(time.Millisecond * 1750) + + // Throw weapon. + weaponSprite := newCreep(TypeTorch, l, p) + weaponSprite.x, weaponSprite.y = p.x, p.y + weaponSprite.frames = 1 + weaponSprite.frame = 0 + weaponSprite.sprites = []*ebiten.Image{ + imageAtlas[ImageUzi], + } + + p.weapon = nil + l.creeps = append(l.creeps, weaponSprite) + + startX := 108 + + doorX := float64(startX) - 0.4 + + go func() { + for i := 0; i < 144*2; i++ { + if weaponSprite.x < doorX { + for i, c := range l.creeps { + if c == weaponSprite { + l.creeps = append(l.creeps[:i], l.creeps[i+1:]...) + } + } + return + } + + weaponSprite.x -= 0.05 + if i < 100 { + weaponSprite.y -= 0.005 * (float64(144-i) / 144) + } else { + weaponSprite.y += 0.01 * (float64(288-i) / 288) + } + weaponSprite.angle -= .1 + time.Sleep(time.Second / 144) + } + }() + + time.Sleep(time.Second / 2) + + // Throw torch. + torchSprite := newCreep(TypeTorch, l, p) + torchSprite.x, torchSprite.y = p.x, p.y + torchSprite.frames = 1 + torchSprite.frame = 0 + torchSprite.sprites = []*ebiten.Image{ + sandstoneSS.TorchMulti, + } + + p.hasTorch = false + l.creeps = append(l.creeps, torchSprite) + l.torches = append(l.torches, torchSprite) + l.bakePartialLightmap(int(torchSprite.x), int(torchSprite.y)) + + go func() { + lastTorchX := torchSprite.x + + for i := 0; i < 144*3; i++ { + if torchSprite.x < doorX { + for i, c := range l.creeps { + if c == torchSprite { + l.creeps = append(l.creeps[:i], l.creeps[i+1:]...) + l.torches = nil + } + } + } + + torchSprite.x -= 0.05 + if i < 100 { + torchSprite.y -= 0.005 * (float64(144-i) / 144) + } else { + torchSprite.y += 0.01 * (float64(288-i) / 288) + } + + if lastTorchX-torchSprite.x >= 0.1 { + l.bakePartialLightmap(int(torchSprite.x), int(torchSprite.y)) + lastTorchX = torchSprite.x + } + + torchSprite.angle -= .1 + time.Sleep(time.Second / 144) + } + }() + + // Walk away. + time.Sleep(time.Second) + + p.angle = 0 + for i := 0; i < 144; i++ { + p.x += 0.05 * (float64(i) / 144) + // Fade out stars. + for _, star := range stars { + star.colorScale -= 0.01 + if star.colorScale < 0 { + star.colorScale = 0 + } + } + updateStars() + time.Sleep(time.Second / 144) + } + + var removedExistingStars bool + + var addedStars bool + + for i := 0; i < 144*20; i++ { + if p.health > 0 { + // Game has restarted. + return + } + + if i > int(144*3.5) { + if !removedExistingStars { + // Remove existing stars. + stars = nil + g.projectiles = nil + + removedExistingStars = true + } + } + + if i > int(144*12) { + p.angle += 0.0025 * (float64((144*5)-i) / (144 * 5)) + + addStar() + addStar() + addStar() + addStar() + _ = addedStars + } + + p.x += 0.05 + updateStars() + + if i > 144*11 { + for _, star := range stars { + pct := float64((144*7)-i) / 144 * 7 + + star.x -= 0.05 * pct / 50 + + // Apply warp effect. + dx, dy := deltaXY(g.player.x, g.player.y, star.x, star.y) + star.x, star.y = star.x+(dx/100)*pct/1000, star.y+(dy/100)*pct/1000 + } + } + + time.Sleep(time.Second / 144) + } + }() + go func() { // Animate sunrise. go func() { @@ -1368,9 +1611,9 @@ func (g *game) showWinScreen() { g.winScreenSunY -= 0.035 if i > int(144*3.5) { - g.forceColorScale += 0.0005 - if g.forceColorScale > 1 { - g.forceColorScale = 1 + g.minLevelColorScale += 0.0005 + if g.minLevelColorScale > 1 { + g.minLevelColorScale = 1 } } @@ -1385,22 +1628,36 @@ func (g *game) showWinScreen() { time.Sleep(time.Second / 144) } - time.Sleep(5 * time.Second) + time.Sleep(4 * time.Second) // Fade out win screen. - for i := 0; i < 144; i++ { - g.winScreenColorScale -= 0.01 - g.forceColorScale -= 0.01 + for i := 0; i < 144*2; i++ { + g.winScreenColorScale -= 0.005 + g.minLevelColorScale -= 0.005 + if g.minLevelColorScale > 0.6 { + g.minPlayerColorScale = g.minLevelColorScale + } time.Sleep(time.Second / 144) } + g.Lock() + for y := 0; y < g.level.h; y++ { + for x := 0; x < g.level.w; x++ { + g.level.tiles[y][x].sprites = nil + } + } + g.Unlock() + + time.Sleep(6 * time.Second) + // Fade in game over screen. g.gameWon = false defer func() { - g.forceColorScale = -1 + g.minLevelColorScale = -1 + g.minPlayerColorScale = -1 g.updateCursor() }() @@ -1408,7 +1665,9 @@ func (g *game) showWinScreen() { if g.player.health > 0 { return } - g.forceColorScale = i + if i <= 1 { + g.minLevelColorScale = i + } time.Sleep(time.Second / 144) } }() diff --git a/level.go b/level.go index ef36881..528df67 100644 --- a/level.go +++ b/level.go @@ -4,6 +4,7 @@ import ( "fmt" "math" "math/rand" + "time" "github.com/Meshiest/go-dungeon/dungeon" ) @@ -29,7 +30,7 @@ type Level struct { enterX, enterY int exitX, exitY int - exitOpen bool + exitOpenTime time.Time requiredSouls int } diff --git a/win.go b/win.go index 7910a92..065e4fc 100644 --- a/win.go +++ b/win.go @@ -1,9 +1,7 @@ package main import ( - "math" "math/rand" - "time" "github.com/hajimehoshi/ebiten/v2" ) @@ -130,108 +128,5 @@ func newWinLevel(p *gamePlayer) *Level { p.angle = 0 p.x, p.y = doorX, float64(startY) - go func() { - // Walk away. - for i := 0; i < 36; i++ { - p.x += 0.05 - time.Sleep(time.Second / 144) - } - for i := 0; i < 288; i++ { - p.x += 0.05 * (float64(288-i) / 288) - time.Sleep(time.Second / 144) - } - - // Turn around. - p.angle = math.Pi - time.Sleep(time.Millisecond * 1750) - - // Throw weapon. - weaponSprite := newCreep(TypeTorch, l, p) - weaponSprite.x, weaponSprite.y = p.x, p.y-0.25 - weaponSprite.frames = 1 - weaponSprite.frame = 0 - weaponSprite.sprites = []*ebiten.Image{ - imageAtlas[ImageUzi], - } - - p.weapon = nil - l.creeps = append(l.creeps, weaponSprite) - - go func() { - for i := 0; i < 144*2; i++ { - if weaponSprite.x < doorX { - for i, c := range l.creeps { - if c == weaponSprite { - l.creeps = append(l.creeps[:i], l.creeps[i+1:]...) - } - } - return - } - - weaponSprite.x -= 0.05 - if i < 100 { - weaponSprite.y -= 0.005 * (float64(144-i) / 144) - } else { - weaponSprite.y += 0.01 * (float64(288-i) / 288) - } - weaponSprite.angle -= .1 - time.Sleep(time.Second / 144) - } - }() - - time.Sleep(time.Second / 2) - - // Throw torch. - torchSprite := newCreep(TypeTorch, l, p) - torchSprite.x, torchSprite.y = p.x, p.y-0.25 - torchSprite.frames = 1 - torchSprite.frame = 0 - torchSprite.sprites = []*ebiten.Image{ - sandstoneSS.TorchMulti, - } - - p.hasTorch = false - l.creeps = append(l.creeps, torchSprite) - - go func() { - for i := 0; i < 144*3; i++ { - if torchSprite.x < doorX { - for i, c := range l.creeps { - if c == torchSprite { - l.creeps = append(l.creeps[:i], l.creeps[i+1:]...) - } - } - } - - torchSprite.x -= 0.05 - if i < 100 { - torchSprite.y -= 0.005 * (float64(144-i) / 144) - } else { - torchSprite.y += 0.01 * (float64(288-i) / 288) - } - - torchSprite.angle -= .1 - time.Sleep(time.Second / 144) - } - }() - - // Walk away. - time.Sleep(time.Second) - - p.angle = 0 - for i := 0; i < 144; i++ { - p.x += 0.05 * (float64(i) / 144) - time.Sleep(time.Second / 144) - } - for i := 0; i < 144*15; i++ { - if p.health > 0 { - // Game has restarted. - return - } - p.x += 0.05 - time.Sleep(time.Second / 144) - } - }() - return l }