Wrap everything up

This commit is contained in:
Trevor Slocum 2021-11-01 12:58:54 -07:00
parent 4e849022ac
commit 5d87df3611
38 changed files with 450 additions and 163 deletions

BIN
assets/audio/pickup.wav Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 812 B

After

Width:  |  Height:  |  Size: 405 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 523 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 653 B

After

Width:  |  Height:  |  Size: 303 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 954 B

After

Width:  |  Height:  |  Size: 853 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 956 B

After

Width:  |  Height:  |  Size: 853 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 744 B

After

Width:  |  Height:  |  Size: 678 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 542 B

After

Width:  |  Height:  |  Size: 261 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 633 B

After

Width:  |  Height:  |  Size: 336 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 604 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 B

After

Width:  |  Height:  |  Size: 122 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 KiB

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 327 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 323 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 255 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 960 B

After

Width:  |  Height:  |  Size: 911 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -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

View File

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

View File

@ -4,5 +4,5 @@
package main
func parseFlags(g *game) {
// Do nothing
g.disableEsc = true
}

197
game.go
View File

@ -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 {

308
level.go
View File

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

View File

@ -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")

View File

@ -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.