Wrap everything up
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 812 B After Width: | Height: | Size: 405 B |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 523 B |
Before Width: | Height: | Size: 653 B After Width: | Height: | Size: 303 B |
Before Width: | Height: | Size: 954 B After Width: | Height: | Size: 853 B |
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 956 B After Width: | Height: | Size: 853 B |
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 744 B After Width: | Height: | Size: 678 B |
Before Width: | Height: | Size: 542 B After Width: | Height: | Size: 261 B |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 633 B After Width: | Height: | Size: 336 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 604 B |
Before Width: | Height: | Size: 157 B After Width: | Height: | Size: 122 B |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 247 KiB After Width: | Height: | Size: 247 KiB |
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 327 KiB After Width: | Height: | Size: 9.2 KiB |
Before Width: | Height: | Size: 323 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 255 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 960 B After Width: | Height: | Size: 911 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.4 KiB |
3
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
|
||||
|
|
6
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
|
||||
}
|
||||
|
||||
|
|
|
@ -4,5 +4,5 @@
|
|||
package main
|
||||
|
||||
func parseFlags(g *game) {
|
||||
// Do nothing
|
||||
g.disableEsc = true
|
||||
}
|
||||
|
|
197
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 {
|
||||
|
|
308
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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
9
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.
|
||||
|
|