diff --git a/assets/audio/pickup.wav b/assets/audio/pickup.wav new file mode 100644 index 0000000..e09db30 Binary files /dev/null and b/assets/audio/pickup.wav differ diff --git a/assets/creeps/bat/bite.png b/assets/creeps/bat/bite.png index 462f055..322fb50 100644 Binary files a/assets/creeps/bat/bite.png and b/assets/creeps/bat/bite.png differ diff --git a/assets/creeps/bat/fly.png b/assets/creeps/bat/fly.png index bd5eb2c..4077ab0 100644 Binary files a/assets/creeps/bat/fly.png and b/assets/creeps/bat/fly.png differ diff --git a/assets/creeps/bat/formation.png b/assets/creeps/bat/formation.png index 7d9a9d9..9f9e372 100644 Binary files a/assets/creeps/bat/formation.png and b/assets/creeps/bat/formation.png differ diff --git a/assets/creeps/bat/hit-and-death.png b/assets/creeps/bat/hit-and-death.png index 120c54a..154b98e 100644 Binary files a/assets/creeps/bat/hit-and-death.png and b/assets/creeps/bat/hit-and-death.png differ diff --git a/assets/creeps/bat/idle-to-fly.png b/assets/creeps/bat/idle-to-fly.png index 863d221..e21e2d9 100644 Binary files a/assets/creeps/bat/idle-to-fly.png and b/assets/creeps/bat/idle-to-fly.png differ diff --git a/assets/creeps/bat/idle.png b/assets/creeps/bat/idle.png index e23ef7b..b2c3c48 100644 Binary files a/assets/creeps/bat/idle.png and b/assets/creeps/bat/idle.png differ diff --git a/assets/creeps/ghost/ghost1.png b/assets/creeps/ghost/ghost1.png index 456da04..9554d27 100644 Binary files a/assets/creeps/ghost/ghost1.png and b/assets/creeps/ghost/ghost1.png differ diff --git a/assets/creeps/ghost/ghost1r.png b/assets/creeps/ghost/ghost1r.png index 4d01379..ab6cf88 100644 Binary files a/assets/creeps/ghost/ghost1r.png and b/assets/creeps/ghost/ghost1r.png differ diff --git a/assets/creeps/ghost/ghost2.png b/assets/creeps/ghost/ghost2.png index c0272dd..50579cb 100644 Binary files a/assets/creeps/ghost/ghost2.png and b/assets/creeps/ghost/ghost2.png differ diff --git a/assets/creeps/ghost/ghost2r.png b/assets/creeps/ghost/ghost2r.png index b6f5c6f..a0a2276 100644 Binary files a/assets/creeps/ghost/ghost2r.png and b/assets/creeps/ghost/ghost2r.png differ diff --git a/assets/creeps/vampire/vampire1.png b/assets/creeps/vampire/vampire1.png index 6caf2ab..59b338a 100644 Binary files a/assets/creeps/vampire/vampire1.png and b/assets/creeps/vampire/vampire1.png differ diff --git a/assets/creeps/vampire/vampire2.png b/assets/creeps/vampire/vampire2.png index 5162eee..b413fc6 100644 Binary files a/assets/creeps/vampire/vampire2.png and b/assets/creeps/vampire/vampire2.png differ diff --git a/assets/creeps/vampire/vampire3.png b/assets/creeps/vampire/vampire3.png index 8aae6a9..71ebbb3 100644 Binary files a/assets/creeps/vampire/vampire3.png and b/assets/creeps/vampire/vampire3.png differ diff --git a/assets/items/garlic.png b/assets/items/garlic.png index 8e16da9..03f33a2 100644 Binary files a/assets/items/garlic.png and b/assets/items/garlic.png differ diff --git a/assets/items/holy-water.png b/assets/items/holy-water.png index 7b28c95..8f0458b 100644 Binary files a/assets/items/holy-water.png and b/assets/items/holy-water.png differ diff --git a/assets/ojas-dungeon/GOLD BAR AND COPPER BAR-sheet.png b/assets/ojas-dungeon/GOLD BAR AND COPPER BAR-sheet.png index 0e0d77a..faed7f5 100644 Binary files a/assets/ojas-dungeon/GOLD BAR AND COPPER BAR-sheet.png and b/assets/ojas-dungeon/GOLD BAR AND COPPER BAR-sheet.png differ diff --git a/assets/ojas-dungeon/character-run.png b/assets/ojas-dungeon/character-run.png index 1ca9b11..8d65ac9 100644 Binary files a/assets/ojas-dungeon/character-run.png and b/assets/ojas-dungeon/character-run.png differ diff --git a/assets/ojas-dungeon/character.png b/assets/ojas-dungeon/character.png index f2ee59d..a90db13 100644 Binary files a/assets/ojas-dungeon/character.png and b/assets/ojas-dungeon/character.png differ diff --git a/assets/ojas-dungeon/charecter-attack.png b/assets/ojas-dungeon/charecter-attack.png index e942cce..7102b10 100644 Binary files a/assets/ojas-dungeon/charecter-attack.png and b/assets/ojas-dungeon/charecter-attack.png differ diff --git a/assets/ojas-dungeon/crate.png b/assets/ojas-dungeon/crate.png index 39dd473..718f26e 100644 Binary files a/assets/ojas-dungeon/crate.png and b/assets/ojas-dungeon/crate.png differ diff --git a/assets/ojas-dungeon/dungeon-tileset-1.png b/assets/ojas-dungeon/dungeon-tileset-1.png index cc8ae6c..d153e6d 100644 Binary files a/assets/ojas-dungeon/dungeon-tileset-1.png and b/assets/ojas-dungeon/dungeon-tileset-1.png differ diff --git a/assets/sandstone-dungeon/!Package Preview.png b/assets/sandstone-dungeon/!Package Preview.png index c25f672..dc25f5e 100644 Binary files a/assets/sandstone-dungeon/!Package Preview.png and b/assets/sandstone-dungeon/!Package Preview.png differ diff --git a/assets/sandstone-dungeon/Characters-part-1.png b/assets/sandstone-dungeon/Characters-part-1.png index 73c82a5..8a85b51 100644 Binary files a/assets/sandstone-dungeon/Characters-part-1.png and b/assets/sandstone-dungeon/Characters-part-1.png differ diff --git a/assets/sandstone-dungeon/Characters-part-2.png b/assets/sandstone-dungeon/Characters-part-2.png index a06420e..cff2b17 100644 Binary files a/assets/sandstone-dungeon/Characters-part-2.png and b/assets/sandstone-dungeon/Characters-part-2.png differ diff --git a/assets/sandstone-dungeon/Tiles-Door-packs.png b/assets/sandstone-dungeon/Tiles-Door-packs.png index a8293c3..b477174 100644 Binary files a/assets/sandstone-dungeon/Tiles-Door-packs.png and b/assets/sandstone-dungeon/Tiles-Door-packs.png differ diff --git a/assets/sandstone-dungeon/Tiles-Items-pack.png b/assets/sandstone-dungeon/Tiles-Items-pack.png index 16c9615..3a50b88 100644 Binary files a/assets/sandstone-dungeon/Tiles-Items-pack.png and b/assets/sandstone-dungeon/Tiles-Items-pack.png differ diff --git a/assets/sandstone-dungeon/Tiles-Props-pack.png b/assets/sandstone-dungeon/Tiles-Props-pack.png index fb2b338..be176ce 100644 Binary files a/assets/sandstone-dungeon/Tiles-Props-pack.png and b/assets/sandstone-dungeon/Tiles-Props-pack.png differ diff --git a/assets/sandstone-dungeon/Tiles-Sandstone-Dungeons.png b/assets/sandstone-dungeon/Tiles-Sandstone-Dungeons.png index 740e4ae..c45f80f 100644 Binary files a/assets/sandstone-dungeon/Tiles-Sandstone-Dungeons.png and b/assets/sandstone-dungeon/Tiles-Sandstone-Dungeons.png differ diff --git a/assets/ui/heart.png b/assets/ui/heart.png index bf8ea79..459bb7d 100644 Binary files a/assets/ui/heart.png and b/assets/ui/heart.png differ diff --git a/assets/weapons/uzi.png b/assets/weapons/uzi.png index 2c72390..dbaf221 100644 Binary files a/assets/weapons/uzi.png and b/assets/weapons/uzi.png differ diff --git a/audio.go b/audio.go index 765082a..8af45f0 100644 --- a/audio.go +++ b/audio.go @@ -14,8 +14,8 @@ const ( SoundBat SoundPlayerHurt SoundPlayerDie + SoundPickup SoundMunch - SoundGib ) var soundMap = map[int]string{ @@ -25,6 +25,7 @@ var soundMap = map[int]string{ SoundBat: "assets/audio/bat.wav", SoundPlayerHurt: "assets/audio/playerhurt.wav", SoundPlayerDie: "assets/audio/playerdie.wav", + SoundPickup: "assets/audio/pickup.wav", SoundMunch: "assets/audio/munch.wav", } var soundAtlas [][]*audio.Player diff --git a/creep.go b/creep.go index 8b0ce80..790107a 100644 --- a/creep.go +++ b/creep.go @@ -109,6 +109,7 @@ func newCreep(creepType int, l *Level, p *gamePlayer) *gameCreep { } func (c *gameCreep) queueNextAction() { + c.tick = 0 if c.creepType == TypeBat { c.nextAction = 288 + rand.Intn(288) return @@ -308,8 +309,10 @@ func (c *gameCreep) Update() { c.x, c.y = x, y } else if c.level.isFloor(x, c.y) { c.x = x + c.moveY *= -1 } else if c.level.isFloor(c.x, y) { c.y = y + c.moveX *= -1 } else { c.nextAction = 0 return @@ -322,8 +325,9 @@ func (c *gameCreep) Update() { } } + // Avoid garlic. for _, item := range c.level.items { - if item.health == 0 { + if item.health == 0 || item.itemType != itemTypeGarlic { continue } diff --git a/flags_web.go b/flags_web.go index 9d0fcbf..eca2841 100644 --- a/flags_web.go +++ b/flags_web.go @@ -4,5 +4,5 @@ package main func parseFlags(g *game) { - // Do nothing + g.disableEsc = true } diff --git a/game.go b/game.go index fc762d9..eb09978 100644 --- a/game.go +++ b/game.go @@ -29,19 +29,21 @@ var colorBlood = color.RGBA{102, 0, 0, 255} const ( gunshotVolume = 0.2 vampireDieVolume = 0.15 - batDieVolume = 1.5 + batVolume = 1.0 playerHurtVolume = 0.4 playerDieVolume = 1.6 - munchVolume = 0.8 + pickupVolume = 0.8 + munchVolume = 0.6 - spawnGarlic = 6 + spawnGarlic = 3 - garlicActiveTime = 7 * time.Second - holyWaterActiveTime = time.Second + garlicActiveTime = 7 * time.Second batSoundDelay = 250 * time.Millisecond screenPadding = 33 + + startingHealth = 3 ) var startButtons = []ebiten.StandardGamepadButton{ @@ -72,6 +74,8 @@ type projectile struct { colorScale float64 } +var blackSquare = ebiten.NewImage(32, 32) + // game is an isometric demo game. type game struct { w, h int @@ -119,6 +123,8 @@ type game struct { minLevelColorScale float64 minPlayerColorScale float64 + disableEsc bool + godMode bool noclipMode bool muteAudio bool @@ -157,6 +163,8 @@ func NewGame() (*game, error) { return nil, err } + blackSquare.Fill(color.Black) + return g, nil } @@ -235,7 +243,7 @@ func (g *game) generateLevel() error { // Position player. if g.levelNum > 1 { - g.player.x, g.player.y = float64(g.level.enterX), float64(g.level.enterY)+1 + g.player.x, g.player.y = float64(g.level.enterX)+0.5, float64(g.level.enterY)-0.5 } else { for { g.player.x, g.player.y = float64(rand.Intn(g.level.w)), float64(rand.Intn(g.level.h)) @@ -247,7 +255,7 @@ func (g *game) generateLevel() error { // Spawn items. g.level.items = nil - for i := 0; i < spawnGarlic; i++ { + for i := 0; i < spawnGarlic*g.levelNum; i++ { itemType := itemTypeGarlic c := g.newItem(itemType) g.level.items = append(g.level.items, c) @@ -268,12 +276,12 @@ func (g *game) generateLevel() error { } g.level.items = append(g.level.items, item) - // Spawn creeps. + // Spawn starting creeps. spawnAmount := 66 if g.levelNum == 2 { - spawnAmount = 666 + spawnAmount = 133 } else if g.levelNum == 3 { - spawnAmount = 1111 + spawnAmount = 333 } for i := 0; i < spawnAmount; i++ { g.level.addCreep(TypeVampire) @@ -313,7 +321,7 @@ func (g *game) reset() error { g.player.soulsRescued = 0 // Reset player health. - g.player.health = 3 + g.player.health = startingHealth return nil } @@ -372,11 +380,36 @@ func (g *game) checkLevelComplete() { } 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) - g.level.tiles[g.level.exitY][g.level.exitX].AddSprite(sandstoneSS.DoorOpen) + // TODO preserve existing floor sprite + + t := g.level.tiles[g.level.exitY][g.level.exitX] + t.sprites = nil + t.AddSprite(sandstoneSS.FloorA) + t.AddSprite(sandstoneSS.TopDoorOpenTL) + + t = g.level.tiles[g.level.exitY][g.level.exitX+1] + t.sprites = nil + t.AddSprite(sandstoneSS.FloorA) + t.AddSprite(sandstoneSS.TopDoorOpenTR) + + t = g.level.tiles[g.level.exitY+1][g.level.exitX] + t.sprites = nil + t.AddSprite(sandstoneSS.FloorA) + t.AddSprite(sandstoneSS.TopDoorOpenBL) + + t = g.level.tiles[g.level.exitY+1][g.level.exitX+1] + t.sprites = nil + t.AddSprite(sandstoneSS.FloorA) + t.AddSprite(sandstoneSS.TopDoorOpenBR) + + for i := 1; i < 3; i++ { + t = g.level.tiles[g.level.exitY-i][g.level.exitX] + t.forceColorScale = 0 + + t = g.level.tiles[g.level.exitY-i][g.level.exitX+1] + t.forceColorScale = 0 + } - // TODO widen doorway // TODO add trigger entity or hardcode check } @@ -387,7 +420,7 @@ func (g *game) Update() error { gamepadDeadZone := 0.1 - if ebiten.IsKeyPressed(ebiten.KeyEscape) || ebiten.IsWindowBeingClosed() { + if (!g.disableEsc && ebiten.IsKeyPressed(ebiten.KeyEscape)) || ebiten.IsWindowBeingClosed() { g.exit() return nil } @@ -490,7 +523,7 @@ func (g *game) Update() error { g.handlePlayerDeath() } } else if c.creepType == TypeBat && (dx <= 12 && dy <= 7) && rand.Intn(166) == 6 && time.Since(g.lastBatSound) >= batSoundDelay { - g.playSound(SoundBat, batDieVolume) + g.playSound(SoundBat, batVolume) g.lastBatSound = time.Now() } @@ -578,7 +611,7 @@ func (g *game) Update() error { g.playSound(SoundMunch, munchVolume) g.player.garlicUntil = time.Now().Add(garlicActiveTime) } else if item.itemType == itemTypeHolyWater { - // TODO g.playSound(SoundItemPickup, pickupVolume) + g.playSound(SoundPickup, pickupVolume) g.player.health++ } } @@ -687,7 +720,7 @@ UPDATEPROJECTILES: if g.tick%200 == 0 { removed = 0 for i, creep := range g.level.creeps { - if creep.health != 0 || creep.creepType == TypeTorch { + if creep.health != 0 || creep.creepType == TypeTorch || creep.creepType == TypeSoul { continue } @@ -698,41 +731,64 @@ UPDATEPROJECTILES: } // Spawn garlic. - if g.tick%(144*45) == 0 || rand.Intn(666) == 0 { + if (g.tick > 0 && g.tick%(144*45) == 0) || rand.Intn(6666) == 0 { item := g.newItem(itemTypeGarlic) g.level.items = append(g.level.items, item) + SPAWNGARLIC: + for i := 0; i < 5; i++ { + for _, levelItem := range g.level.items { + if levelItem != item && item.itemType == itemTypeGarlic { + dx, dy := deltaXY(item.x, item.y, levelItem.x, levelItem.y) + if dx < 21 || dy < 21 { + item.x, item.y = g.level.newSpawnLocation() + continue SPAWNGARLIC + } + } + } + break + } + if g.debugMode { g.flashMessage("SPAWN GARLIC") } } // Spawn holy water. - if g.tick%(144*120) == 0 || rand.Intn(666) == 0 { + if g.tick%(144*30) == 0 || rand.Intn(6666) == 0 { item := g.newItem(itemTypeHolyWater) g.level.items = append(g.level.items, item) + SPAWNHOLYWATER: + for i := 0; i < 5; i++ { + for _, levelItem := range g.level.items { + if levelItem != item && item.itemType == itemTypeHolyWater { + dx, dy := deltaXY(item.x, item.y, levelItem.x, levelItem.y) + if dx < 21 || dy < 21 { + item.x, item.y = g.level.newSpawnLocation() + continue SPAWNHOLYWATER + } + } + } + break + } + if g.debugMode { g.flashMessage("SPAWN HOLY WATER") } } - maxCreeps := 666 + maxCreeps := 333 if g.levelNum == 2 { - maxCreeps = 1999 + maxCreeps = 666 } else if g.levelNum == 3 { - maxCreeps = 3333 + maxCreeps = 999 } if len(g.level.creeps) < maxCreeps { // Spawn vampires. if g.tick%144 == 0 { - spawnAmount := rand.Intn(26 + (g.tick / (144 * 3))) - minCreeps := 0 - if g.levelNum == 2 { - minCreeps = 500 - } else if g.levelNum == 3 { - minCreeps = 1000 - } + spawnAmount := rand.Intn(1 + (g.tick / (144 * 9))) + minCreeps := g.level.requiredSouls * 2 if len(g.level.creeps) < minCreeps { spawnAmount *= 4 } @@ -745,7 +801,7 @@ UPDATEPROJECTILES: } // Spawn bats. - if g.tick%144 == 0 { + if g.tick%(144*(4-g.levelNum)) == 0 { spawnAmount := g.tick / 288 if spawnAmount < 1 { spawnAmount = 1 @@ -791,9 +847,6 @@ UPDATEPROJECTILES: if ebiten.IsKeyPressed(ebiten.KeyControl) { spawnAmount := 13 switch { - case inpututil.IsKeyJustPressed(ebiten.KeyE): - g.player.x, g.player.y = float64(g.level.exitX), float64(g.level.exitY+1) - g.flashMessage("WARPED TO EXIT") case inpututil.IsKeyJustPressed(ebiten.KeyF): g.fullBrightMode = !g.fullBrightMode if g.fullBrightMode { @@ -868,6 +921,15 @@ UPDATEPROJECTILES: case ebiten.IsKeyPressed(ebiten.KeyShift) && inpututil.IsKeyJustPressed(ebiten.KeyEqual): g.showWinScreen() g.flashMessage("WARPED TO WIN SCREEN") + case inpututil.IsKeyJustPressed(ebiten.KeyMinus): + if g.player.soulsRescued < g.level.requiredSouls { + g.player.soulsRescued = g.level.requiredSouls + g.checkLevelComplete() + g.flashMessage("SKIPPED SOUL COLLECTION") + } else { + g.player.x, g.player.y = float64(g.level.exitX)+0.5, float64(g.level.exitY+2) + g.flashMessage("WARPED TO EXIT") + } case inpututil.IsKeyJustPressed(ebiten.KeyEqual): err := g.nextLevel() if err != nil { @@ -877,6 +939,19 @@ UPDATEPROJECTILES: } } + // Check if player is exiting level. + if !g.level.exitOpenTime.IsZero() { + exitThreshold := 1.1 + dx1, dy1 := deltaXY(g.player.x, g.player.y, float64(g.level.exitX), float64(g.level.exitY)) + dx2, dy2 := deltaXY(g.player.x, g.player.y, float64(g.level.exitX+1), float64(g.level.exitY)) + if (dx1 <= exitThreshold && dy1 <= exitThreshold) || (dx2 <= exitThreshold && dy2 <= exitThreshold) { + err := g.nextLevel() + if err != nil { + return err + } + } + } + g.tick++ return nil } @@ -971,7 +1046,7 @@ func (g *game) Draw(screen *ebiten.Image) { if g.gameOverTime.IsZero() { // Draw health. - healthScale := 1.3 + healthScale := 1.5 heartSpace := int(32 * healthScale) heartY := float64(g.h - screenPadding - heartSpace) for i := 0; i < g.player.health; i++ { @@ -981,19 +1056,19 @@ func (g *game) Draw(screen *ebiten.Image) { screen.DrawImage(imageAtlas[ImageHeart], g.op) } - scale := 4.0 + scale := 5.0 soulsY := float64(g.h-int(scale*14)) - screenPadding if g.level.exitOpenTime.IsZero() { // Draw souls. soulsLabel := fmt.Sprintf("%d", g.level.requiredSouls-g.player.soulsRescued) - soulImgSize := 50.0 + soulImgSize := 46.0 - soulsX := float64(g.w-screenPadding) - (float64((len(soulsLabel)) * 4 * 6)) - soulImgSize + soulsX := float64(g.w-screenPadding) - (float64((len(soulsLabel)) * int(scale) * 6)) - 2 - soulImgSize soulImgScale := 1.5 g.op.GeoM.Reset() - g.op.GeoM.Translate((soulsX+soulImgSize)/soulImgScale, (soulsY+9)/soulImgScale) + g.op.GeoM.Translate((float64(g.w-screenPadding)-soulImgSize)/soulImgScale, (soulsY+19)/soulImgScale) g.op.GeoM.Scale(soulImgScale, soulImgScale) screen.DrawImage(ojasDungeonSS.Soul1, g.op) @@ -1001,7 +1076,7 @@ func (g *game) Draw(screen *ebiten.Image) { } else { // Draw exit message. if time.Since(g.level.exitOpenTime).Milliseconds()%2000 < 1500 { - g.drawCenteredText(screen, 0, soulsY, scale, 1.0, "EXIT OPEN") + g.drawText(screen, float64(g.w-screenPadding)-(float64(9)*scale*6), soulsY, scale, 1.0, "EXIT OPEN") } } } @@ -1103,10 +1178,15 @@ func (g *game) levelColorScale(x, y float64) float64 { if t == nil { return 0 } + tileV := t.colorScale s := math.Min(1, v+tileV) + if t.forceColorScale != 0 { + return t.forceColorScale + } + return s } @@ -1217,6 +1297,7 @@ func (g *game) renderLevel(screen *ebiten.Image) int { } } + // Render top tiles. var t *Tile for y := 0; y < g.level.h; y++ { for x := 0; x < g.level.w; x++ { @@ -1253,6 +1334,38 @@ func (g *game) renderLevel(screen *ebiten.Image) int { drawCreeps() } + // Render side and bottom walls a second time. + if g.level.sideWalls != nil { + for y := 0; y < g.level.h; y++ { + for x := 0; x < g.level.w; x++ { + t = g.level.sideWalls[y][x] + if t == nil { + continue // No tile at this position. + } + + drawn += g.renderSprite(float64(x), float64(y), 0, 0, 0, 1.0, 1.0, 1.0, blackSquare, screen) + } + } + } + if g.level.otherWalls != nil { + for y := 0; y < g.level.h; y++ { + for x := 0; x < g.level.w; x++ { + t = g.level.otherWalls[y][x] + if t == nil { + t = g.level.tiles[y][x] + if t == nil || len(t.sprites) == 0 { + drawn += g.renderSprite(float64(x), float64(y), 0, 0, 0, 1.0, 1.0, 1.0, blackSquare, screen) + } + 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) + } + } + } + } + return drawn } @@ -1320,7 +1433,7 @@ func (g *game) hurtCreep(c *gameCreep, damage int) error { /* if c.creepType == TypeBat { dieSound = SoundBat - volume = batDieVolume + volume = batVolume } else { dieSound = SoundVampireDie1 if rand.Intn(2) == 1 { diff --git a/level.go b/level.go index 5e92374..71bf9fb 100644 --- a/level.go +++ b/level.go @@ -20,6 +20,10 @@ type Level struct { tiles [][]*Tile // (Y,X) array of tiles tileSize int + topWalls [][]*Tile + sideWalls [][]*Tile + otherWalls [][]*Tile + items []*gameItem creeps []*gameCreep @@ -37,81 +41,30 @@ type Level struct { requiredSouls int } -// Tile returns the tile at the provided coordinates, or nil. -func (l *Level) Tile(x, y int) *Tile { - if x >= 0 && y >= 0 && x < l.w && y < l.h { - return l.tiles[y][x] - } - return nil -} - -// Size returns the size of the Level. -func (l *Level) Size() (width, height int) { - return l.w, l.h -} - -func (l *Level) isFloor(x float64, y float64) bool { - t := l.Tile(int(math.Floor(x+.5)), int(math.Floor(y+.5))) - if t == nil { - return false - } - if !t.floor { - return false - } - return true -} - -func (l *Level) newSpawnLocation() (float64, float64) { -SPAWNLOCATION: - for { - x := float64(1 + rand.Intn(l.w-2)) - y := float64(1 + rand.Intn(l.h-2)) - - if !l.isFloor(x, y) { - continue - } - - // Too close to player. - playerSafeSpace := 18.0 - dx, dy := deltaXY(x, y, l.player.x, l.player.y) - if dx <= playerSafeSpace && dy <= playerSafeSpace { - continue - } - - // Too close to garlic or holy water. - garlicSafeSpace := 2.0 - for _, item := range l.items { - if item.health == 0 { - continue - } - - dx, dy = deltaXY(x, y, item.x, item.y) - if dx <= garlicSafeSpace && dy <= garlicSafeSpace { - continue SPAWNLOCATION - } - } - - return x, y - } - -} - // NewLevel returns a new randomly generated Level. func NewLevel(levelNum int, p *gamePlayer) (*Level, error) { - div := 4 - levelNum + levelSize := 100 + if levelNum == 2 { + levelSize = 108 + } else if levelNum == 3 { + levelSize = 116 + } else if levelSize == 4 { + levelSize = 256 + } + // Note: Level size must be divisible by the dungeon scale (4). l := &Level{ num: levelNum, - w: 336 / div, - h: 336 / div, + w: levelSize, + h: levelSize, tileSize: 32, player: p, } - l.requiredSouls = 66 + l.requiredSouls = 33 if levelNum == 2 { - l.requiredSouls = 666 + l.requiredSouls = 66 } else if levelNum == 3 { - l.requiredSouls = 6666 + l.requiredSouls = 99 } var err error @@ -120,10 +73,12 @@ func NewLevel(levelNum int, p *gamePlayer) (*Level, error) { return nil, fmt.Errorf("failed to load embedded spritesheet: %s", err) } - rooms := 33 - /*if multiplier == 2 { - rooms = 66 - }*/ + rooms := 13 + if levelNum == 2 { + rooms = 26 + } else if levelNum == 3 { + rooms = 33 + } d := dungeon.NewDungeon(l.w/dungeonScale, rooms) dungeonFloor := 1 l.tiles = make([][]*Tile, l.h) @@ -164,7 +119,18 @@ func NewLevel(levelNum int, p *gamePlayer) (*Level, error) { return t.floor } + l.topWalls = make([][]*Tile, l.h) + l.sideWalls = make([][]*Tile, l.h) + l.otherWalls = make([][]*Tile, l.h) + for y := 0; y < l.h; y++ { + l.topWalls[y] = make([]*Tile, l.w) + l.sideWalls[y] = make([]*Tile, l.w) + l.otherWalls[y] = make([]*Tile, l.w) + } + + // Entrance and exit candidates. var topWalls [][2]int + var bottomWalls [][2]int // Add walls. for x := 0; x < l.w; x++ { @@ -213,47 +179,145 @@ func NewLevel(levelNum int, p *gamePlayer) (*Level, error) { l.torches = append(l.torches, c) } else { neighbor.AddSprite(sandstoneSS.WallTop) - topWalls = append(topWalls, [2]int{nx, ny}) + + farRight := floorTile(nx+2, ny) + farBottomRight := floorTile(nx+2, ny+1) + if bottomRight && farBottomRight && !right && !farRight && y > 2 { + topWalls = append(topWalls, [2]int{nx, ny}) + } } + + l.topWalls[ny][nx] = neighbor case spriteLeft: if spriteBottom { neighbor.AddSprite(sandstoneSS.WallBottom) } neighbor.AddSprite(sandstoneSS.WallLeft) + + l.sideWalls[ny][nx] = neighbor + l.otherWalls[ny][nx] = neighbor case spriteRight: if spriteBottom { neighbor.AddSprite(sandstoneSS.WallBottom) } neighbor.AddSprite(sandstoneSS.WallRight) + + l.sideWalls[ny][nx] = neighbor + l.otherWalls[ny][nx] = neighbor case spriteBottomLeft: neighbor.AddSprite(sandstoneSS.WallBottomLeft) + + l.sideWalls[ny][nx] = neighbor + l.otherWalls[ny][nx] = neighbor case spriteBottomRight: neighbor.AddSprite(sandstoneSS.WallBottomRight) + + l.sideWalls[ny][nx] = neighbor + l.otherWalls[ny][nx] = neighbor case spriteBottom: neighbor.AddSprite(sandstoneSS.WallBottom) + + l.otherWalls[ny][nx] = neighbor + + farRight := floorTile(nx+2, ny) + farTopRight := floorTile(nx+2, ny-1) + if topRight && farTopRight && !right && !farRight && ny < l.h-3 { + bottomWalls = append(bottomWalls, [2]int{nx, ny}) + } } } } } - entrance := topWalls[rand.Intn(len(topWalls))] - exit := entrance - for exit == entrance { - exit = topWalls[rand.Intn(len(topWalls))] + for { + entrance := bottomWalls[rand.Intn(len(bottomWalls))] + l.enterX, l.enterY = entrance[0], entrance[1] + + exit := topWalls[rand.Intn(len(topWalls))] + l.exitX, l.exitY = exit[0], exit[1] + + dx, dy := deltaXY(float64(l.enterX), float64(l.enterY), float64(l.exitX), float64(l.exitY)) + if dy >= 8 || dx >= 6 { + break + } } - l.enterX, l.enterY = entrance[0], entrance[1] - l.exitX, l.exitY = exit[0], exit[1] + fadeA := 0.15 + fadeB := 0.1 + // Add entrance. if levelNum > 1 { - l.Tile(l.enterX, l.enterY).sprites = nil - l.Tile(l.enterX, l.enterY).AddSprite(sandstoneSS.FloorA) - l.Tile(l.enterX, l.enterY).AddSprite(sandstoneSS.DoorClosed) + t := l.Tile(l.enterX, l.enterY) + t.sprites = nil + t.AddSprite(sandstoneSS.FloorA) + t.AddSprite(sandstoneSS.BottomDoorClosedL) + t.AddSprite(sandstoneSS.WallLeft) + + t = l.Tile(l.enterX+1, l.enterY) + t.sprites = nil + t.AddSprite(sandstoneSS.FloorA) + t.AddSprite(sandstoneSS.BottomDoorClosedR) + t.AddSprite(sandstoneSS.WallRight) + + // Add fading entrance hall. + for i := 1; i < 3; i++ { + colorScale := fadeA + if i == 2 { + colorScale = fadeB + } + + t = l.Tile(l.enterX, l.enterY+i) + if t != nil { + t.AddSprite(sandstoneSS.FloorA) + t.AddSprite(sandstoneSS.WallLeft) + t.forceColorScale = colorScale + } + + t = l.Tile(l.enterX+1, l.enterY+i) + if t != nil { + t.AddSprite(sandstoneSS.FloorA) + t.AddSprite(sandstoneSS.WallRight) + t.forceColorScale = colorScale + } + } } - l.Tile(l.exitX, l.exitY).sprites = nil - l.Tile(l.exitX, l.exitY).AddSprite(sandstoneSS.FloorA) - l.Tile(l.exitX, l.exitY).AddSprite(sandstoneSS.DoorClosed) + // Add exit. + t := l.Tile(l.exitX, l.exitY) + t.sprites = nil + t.AddSprite(sandstoneSS.FloorA) + t.AddSprite(sandstoneSS.TopDoorClosedL) + + t = l.Tile(l.exitX+1, l.exitY) + t.sprites = nil + t.AddSprite(sandstoneSS.FloorA) + t.AddSprite(sandstoneSS.TopDoorClosedR) + + // Add fading exit hall. + for i := 1; i < 3; i++ { + colorScale := fadeA + if i == 2 { + colorScale = fadeB + } + + t = l.Tile(l.exitX, l.exitY-i) + if t != nil { + t.AddSprite(sandstoneSS.FloorA) + t.AddSprite(sandstoneSS.WallLeft) + t.forceColorScale = colorScale + } + + t = l.Tile(l.exitX+1, l.exitY-i) + if t != nil { + t.AddSprite(sandstoneSS.FloorA) + t.AddSprite(sandstoneSS.WallRight) + t.forceColorScale = colorScale + } + } + + // TODO make it more obvious players should enter it (arrow on first level?) + + // TODO two frame sprite arrow animation // TODO special door for final exit @@ -262,17 +326,85 @@ func NewLevel(levelNum int, p *gamePlayer) (*Level, error) { return l, nil } +// Tile returns the tile at the provided coordinates, or nil. +func (l *Level) Tile(x, y int) *Tile { + if x >= 0 && y >= 0 && x < l.w && y < l.h { + return l.tiles[y][x] + } + return nil +} + +// Size returns the size of the Level. +func (l *Level) Size() (width, height int) { + return l.w, l.h +} + +func (l *Level) isFloor(x float64, y float64) bool { + t := l.Tile(int(math.Floor(x+.5)), int(math.Floor(y+.5))) + if t == nil { + return false + } + if !t.floor { + return false + } + return true +} + +func (l *Level) newSpawnLocation() (float64, float64) { +SPAWNLOCATION: + for { + x := float64(1 + rand.Intn(l.w-2)) + y := float64(1 + rand.Intn(l.h-2)) + + if !l.isFloor(x, y) { + continue + } + + // Too close to player. + playerSafeSpace := 11.0 + dx, dy := deltaXY(x, y, l.player.x, l.player.y) + if dx <= playerSafeSpace && dy <= playerSafeSpace { + continue + } + + // Too close to entrance. + exitSafeSpace := 9.0 + dx, dy = deltaXY(x, y, float64(l.enterX), float64(l.enterY)) + if dx <= exitSafeSpace && dy <= exitSafeSpace { + continue + } + + // Too close to garlic or holy water. + garlicSafeSpace := 2.0 + for _, item := range l.items { + if item.health == 0 { + continue + } + + dx, dy = deltaXY(x, y, item.x, item.y) + if dx <= garlicSafeSpace && dy <= garlicSafeSpace { + continue SPAWNLOCATION + } + } + + return x, y + } + +} + func (l *Level) bakeLightmap() { for x := 0; x < l.w; x++ { for y := 0; y < l.h; y++ { t := l.tiles[y][x] - v := 0.0 - for _, torch := range l.torches { - if torch.health == 0 { - continue + v := t.forceColorScale + if v == 0 { + for _, torch := range l.torches { + if torch.health == 0 { + continue + } + torchV := colorScaleValue(float64(x), float64(y), torch.x, torch.y) + v += torchV } - torchV := colorScaleValue(float64(x), float64(y), torch.x, torch.y) - v += torchV } t.colorScale = v } diff --git a/ss_environment.go b/ss_environment.go index 7295118..9f1c7aa 100644 --- a/ss_environment.go +++ b/ss_environment.go @@ -11,30 +11,40 @@ var sandstoneSS *EnvironmentSpriteSheet // EnvironmentSpriteSheet represents a collection of sprite images. type EnvironmentSpriteSheet struct { - FloorA *ebiten.Image - FloorB *ebiten.Image - FloorC *ebiten.Image - WallTop *ebiten.Image - WallBottom *ebiten.Image - WallBottomLeft *ebiten.Image - WallBottomRight *ebiten.Image - WallLeft *ebiten.Image - WallRight *ebiten.Image - WallTopLeft *ebiten.Image - WallTopRight *ebiten.Image - WallPillar *ebiten.Image - TorchTop1 *ebiten.Image - TorchTop2 *ebiten.Image - TorchTop3 *ebiten.Image - TorchTop4 *ebiten.Image - TorchTop5 *ebiten.Image - TorchTop6 *ebiten.Image - TorchTop7 *ebiten.Image - TorchTop8 *ebiten.Image - TorchTop9 *ebiten.Image - TorchMulti *ebiten.Image - DoorOpen *ebiten.Image - DoorClosed *ebiten.Image + FloorA *ebiten.Image + FloorB *ebiten.Image + FloorC *ebiten.Image + WallTop *ebiten.Image + WallBottom *ebiten.Image + WallBottomLeft *ebiten.Image + WallBottomRight *ebiten.Image + WallLeft *ebiten.Image + WallRight *ebiten.Image + WallTopLeft *ebiten.Image + WallTopRight *ebiten.Image + WallPillar *ebiten.Image + TorchTop1 *ebiten.Image + TorchTop2 *ebiten.Image + TorchTop3 *ebiten.Image + TorchTop4 *ebiten.Image + TorchTop5 *ebiten.Image + TorchTop6 *ebiten.Image + TorchTop7 *ebiten.Image + TorchTop8 *ebiten.Image + TorchTop9 *ebiten.Image + TorchMulti *ebiten.Image + TopDoorClosedL *ebiten.Image + TopDoorClosedR *ebiten.Image + TopDoorOpenTL *ebiten.Image + TopDoorOpenTR *ebiten.Image + TopDoorOpenBL *ebiten.Image + TopDoorOpenBR *ebiten.Image + BottomDoorClosedL *ebiten.Image + BottomDoorClosedR *ebiten.Image + BottomDoorOpenTL *ebiten.Image + BottomDoorOpenTR *ebiten.Image + BottomDoorOpenBL *ebiten.Image + BottomDoorOpenBR *ebiten.Image } // LoadEnvironmentSpriteSheet loads the embedded EnvironmentSpriteSheet. @@ -71,8 +81,34 @@ func LoadEnvironmentSpriteSheet() (*EnvironmentSpriteSheet, error) { s.WallTopLeft = dungeonSpriteAt(2, 3) s.WallTopRight = dungeonSpriteAt(0, 3) s.WallPillar = dungeonSpriteAt(8, 4) - s.DoorOpen = dungeonSpriteAt(3, 3) - s.DoorClosed = dungeonSpriteAt(3, 2) + + // Door sprites + doorFile, err := assetsFS.Open("assets/sandstone-dungeon/Tiles-Door-packs.png") + if err != nil { + return nil, err + } + defer doorFile.Close() + doorImg, _, err := image.Decode(doorFile) + if err != nil { + return nil, err + } + doorSheet := ebiten.NewImageFromImage(doorImg) + // doorSpriteAt returns a sprite at the provided coordinates. + doorSpriteAt := func(x, y int) *ebiten.Image { + return doorSheet.SubImage(image.Rect(x*tileSize, (y+1)*tileSize, (x+1)*tileSize, y*tileSize)).(*ebiten.Image) + } + s.TopDoorClosedL = doorSpriteAt(5, 0) + s.TopDoorClosedR = doorSpriteAt(6, 0) + s.TopDoorOpenTL = doorSpriteAt(5, 3) + s.TopDoorOpenTR = doorSpriteAt(6, 3) + s.TopDoorOpenBL = doorSpriteAt(5, 4) + s.TopDoorOpenBR = doorSpriteAt(6, 4) + s.BottomDoorClosedL = dungeonSpriteAt(4, 0) + s.BottomDoorClosedR = dungeonSpriteAt(5, 0) + s.BottomDoorOpenTL = doorSpriteAt(5, 3) + s.BottomDoorOpenTR = doorSpriteAt(6, 3) + s.BottomDoorOpenBL = doorSpriteAt(5, 4) + s.BottomDoorOpenBR = doorSpriteAt(6, 4) // Prop sprites propFile, err := assetsFS.Open("assets/sandstone-dungeon/Tiles-Props-pack.png") diff --git a/tile.go b/tile.go index 10e2608..2447252 100644 --- a/tile.go +++ b/tile.go @@ -7,10 +7,11 @@ import ( // Tile represents a space with an x,y coordinate within a Level. Any number of // sprites may be added to a Tile. type Tile struct { - sprites []*ebiten.Image - floor bool - wall bool - colorScale float64 // Minimum color scale (brightness) + sprites []*ebiten.Image + floor bool + wall bool + colorScale float64 // Minimum color scale (brightness) + forceColorScale float64 // Override lightmap value } // AddSprite adds a sprite to the Tile.