last_light/game/state/playing_state.go

370 lines
9.6 KiB
Go
Raw Normal View History

package state
import (
"math/rand"
2024-05-06 20:43:35 +03:00
"mvvasilev/last_light/engine"
2024-06-06 23:17:22 +03:00
"mvvasilev/last_light/game/model"
"mvvasilev/last_light/game/systems"
2024-05-12 23:22:39 +03:00
"mvvasilev/last_light/game/ui"
"github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/views"
)
type PlayingState struct {
2024-06-06 23:17:22 +03:00
turnSystem *systems.TurnSystem
inputSystem *systems.InputSystem
2024-05-30 23:39:54 +03:00
player *model.Player
npcs []model.Entity
2024-05-30 23:39:54 +03:00
eventLog *engine.GameEventLog
uiEventLog *ui.UIEventLog
healthBar *ui.UIHealthBar
2024-05-12 23:22:39 +03:00
2024-06-06 23:17:22 +03:00
dungeon *model.Dungeon
2024-05-06 20:43:35 +03:00
viewport *engine.Viewport
2024-05-30 23:39:54 +03:00
viewShortLogs bool
2024-05-12 23:22:39 +03:00
nextGameState GameState
}
2024-06-06 23:17:22 +03:00
func CreatePlayingState(turnSystem *systems.TurnSystem, inputSystem *systems.InputSystem, playerStats map[model.Stat]int) *PlayingState {
2024-05-30 23:39:54 +03:00
turnSystem.Clear()
s := new(PlayingState)
2024-05-30 23:39:54 +03:00
s.turnSystem = turnSystem
s.inputSystem = inputSystem
2024-05-06 21:47:20 +03:00
mapSize := engine.SizeOf(128, 128)
2024-06-06 23:17:22 +03:00
s.dungeon = model.CreateDungeon(mapSize.Width(), mapSize.Height(), 1)
2024-06-06 23:28:06 +03:00
s.player = model.CreatePlayer(
2024-06-06 23:17:22 +03:00
s.dungeon.CurrentLevel().Ground().PlayerSpawnPoint().Position.X(),
s.dungeon.CurrentLevel().Ground().PlayerSpawnPoint().Position.Y(),
2024-05-31 23:37:06 +03:00
playerStats,
)
2024-05-30 23:39:54 +03:00
s.turnSystem.Schedule(s.player.DefaultSpeed().Speed, func() (complete bool, requeue bool) {
2024-05-30 23:39:54 +03:00
requeue = true
complete = false
if s.player.HealthData().Health <= 0 || s.player.HealthData().IsDead {
2024-06-06 23:17:22 +03:00
s.nextGameState = CreateGameOverState(inputSystem)
}
2024-05-30 23:39:54 +03:00
switch inputSystem.NextAction() {
2024-06-06 23:17:22 +03:00
case systems.InputAction_PauseGame:
2024-05-30 23:39:54 +03:00
s.nextGameState = PauseGame(s, s.turnSystem, s.inputSystem)
2024-06-06 23:17:22 +03:00
case systems.InputAction_OpenInventory:
s.nextGameState = CreateInventoryScreenState(s.eventLog, s.dungeon, s.inputSystem, s.turnSystem, s.player, s)
case systems.InputAction_EnterLookMode:
2024-06-10 23:20:38 +03:00
s.viewShortLogs = false
s.nextGameState = CreateLookState(s, s.eventLog, s.dungeon, s.inputSystem, s.turnSystem, s.player)
2024-06-06 23:17:22 +03:00
case systems.InputAction_PickUpItem:
complete = PickUpItemUnderPlayer(s.eventLog, s.dungeon, s.player)
2024-06-06 23:17:22 +03:00
case systems.InputAction_Interact:
complete = s.InteractBelowPlayer()
2024-06-06 23:17:22 +03:00
case systems.InputAction_OpenLogs:
2024-05-30 23:39:54 +03:00
s.viewShortLogs = !s.viewShortLogs
case systems.InputAction_Move_East:
complete = s.MovePlayer(model.East)
case systems.InputAction_Move_West:
complete = s.MovePlayer(model.West)
case systems.InputAction_Move_North:
complete = s.MovePlayer(model.North)
case systems.InputAction_Move_South:
complete = s.MovePlayer(model.South)
2024-05-30 23:39:54 +03:00
default:
}
if s.player.IsNextTurnSkipped() {
s.player.SkipNextTurn(false)
complete = true
}
2024-05-30 23:39:54 +03:00
return
})
s.eventLog = engine.CreateGameEventLog(100)
s.uiEventLog = ui.CreateUIEventLog(0, 17, 80, 7, s.eventLog, tcell.StyleDefault)
2024-06-06 23:17:22 +03:00
s.healthBar = ui.CreateHealthBar(68, 0, 12, 3, s.player, tcell.StyleDefault)
2024-06-01 11:20:51 +03:00
s.dungeon.CurrentLevel().AddEntity(s.player)
entityTable := model.CreateEntityTable()
entityTable.Add(1, func(x, y int) model.Entity {
return model.Entity_Imp(x, y, model.HostileNPCBehavior(s.eventLog, s.dungeon, s.player))
})
entityTable.Add(1, func(x, y int) model.Entity {
return model.Entity_SkeletalKnight(x, y, model.HostileNPCBehavior(s.eventLog, s.dungeon, s.player))
})
entityTable.Add(1, func(x, y int) model.Entity {
return model.Entity_SkeletalWarrior(x, y, model.HostileNPCBehavior(s.eventLog, s.dungeon, s.player))
})
s.npcs = SpawnNPCs(s.dungeon, 7, entityTable)
for _, npc := range s.npcs {
if npc.Behavior() != nil {
speed := npc.Behavior().Speed
s.turnSystem.Schedule(speed, npc.Behavior().Behavior)
}
}
2024-05-06 20:43:35 +03:00
s.viewport = engine.CreateViewport(
2024-05-06 21:47:20 +03:00
engine.PositionAt(0, 0),
2024-06-06 23:17:22 +03:00
s.dungeon.CurrentLevel().Ground().PlayerSpawnPoint().Position,
2024-05-06 21:47:20 +03:00
engine.SizeOf(80, 24),
tcell.StyleDefault,
)
2024-05-12 23:22:39 +03:00
s.nextGameState = s
return s
}
2024-06-06 23:17:22 +03:00
func (s *PlayingState) InputContext() systems.InputContext {
return systems.InputContext_Play
}
func SpawnNPCs(dungeon *model.Dungeon, number int, genTable *model.EntityTable) []model.Entity {
rooms := dungeon.CurrentLevel().Ground().Rooms().Rooms
entities := make([]model.Entity, 0, number)
for range number {
r := rooms[rand.Intn(len(rooms))]
x, y := engine.RandInt(r.Position().X()+1, r.Position().X()+r.Size().Width()-1), engine.RandInt(r.Position().Y()+1, r.Position().Y()+r.Size().Height()-1)
entity := genTable.Generate(x, y)
entities = append(entities, entity)
dungeon.CurrentLevel().AddEntity(entity)
}
return entities
}
func (ps *PlayingState) MovePlayer(direction model.Direction) (success bool) {
2024-06-06 23:17:22 +03:00
if direction == model.DirectionNone {
return true
}
2024-06-06 23:17:22 +03:00
newPlayerPos := ps.player.Position().WithOffset(model.MovementDirectionOffset(direction))
2024-06-10 23:20:38 +03:00
ent := ps.dungeon.CurrentLevel().EntityAt(newPlayerPos.XY())
2024-06-06 23:17:22 +03:00
// We are moving into an entity with health data. Attack it.
if ent != nil && ent.HealthData() != nil {
if ent.HealthData().IsDead {
// TODO: If the entity is dead, the player should be able to move through it.
return false
2024-06-06 23:17:22 +03:00
}
model.ExecuteAttack(ps.eventLog, ps.player, ent)
2024-06-06 23:17:22 +03:00
return true
2024-06-06 23:17:22 +03:00
}
2024-05-12 23:22:39 +03:00
if ps.dungeon.CurrentLevel().IsTilePassable(newPlayerPos.XY()) {
2024-06-06 23:17:22 +03:00
ps.dungeon.CurrentLevel().MoveEntityTo(ps.player.UniqueId(), newPlayerPos.X(), newPlayerPos.Y())
ps.viewport.SetCenter(ps.player.Position())
2024-06-01 11:20:51 +03:00
2024-06-06 23:17:22 +03:00
ps.eventLog.Log("You moved " + model.DirectionName(direction))
return true
} else {
ps.eventLog.Log("You bump into an impassable object")
return false
}
2024-06-06 23:17:22 +03:00
}
func (ps *PlayingState) InteractBelowPlayer() (success bool) {
2024-05-12 23:22:39 +03:00
playerPos := ps.player.Position()
2024-06-06 23:17:22 +03:00
if playerPos == ps.dungeon.CurrentLevel().Ground().NextLevelStaircase().Position {
2024-05-12 23:22:39 +03:00
ps.SwitchToNextLevel()
return true
2024-05-12 23:22:39 +03:00
}
2024-06-06 23:17:22 +03:00
if playerPos == ps.dungeon.CurrentLevel().Ground().PreviousLevelStaircase().Position {
2024-05-12 23:22:39 +03:00
ps.SwitchToPreviousLevel()
return true
2024-05-12 23:22:39 +03:00
}
ps.eventLog.Log("There is nothing to interact with here")
return false
2024-05-12 23:22:39 +03:00
}
func (ps *PlayingState) SwitchToNextLevel() {
if !ps.dungeon.HasNextLevel() {
ps.nextGameState = CreateDialogState(
2024-05-30 23:39:54 +03:00
ps.inputSystem,
ps.turnSystem,
2024-05-12 23:22:39 +03:00
ui.CreateOkDialog(
"The Unknown Depths",
"The staircases descent down to the lower levels is seemingly blocked by multiple large boulders. They appear immovable.",
"Continue",
40,
func() {
ps.nextGameState = ps
},
),
ps,
)
return
}
ps.dungeon.CurrentLevel().DropEntity(ps.player.UniqueId())
ps.dungeon.MoveToNextLevel()
2024-06-06 23:17:22 +03:00
ps.player.Positioned().Position = ps.dungeon.CurrentLevel().Ground().PlayerSpawnPoint().Position
2024-05-12 23:22:39 +03:00
ps.viewport = engine.CreateViewport(
engine.PositionAt(0, 0),
2024-06-06 23:17:22 +03:00
ps.dungeon.CurrentLevel().Ground().PlayerSpawnPoint().Position,
2024-05-12 23:22:39 +03:00
engine.SizeOf(80, 24),
tcell.StyleDefault,
)
2024-06-01 11:20:51 +03:00
ps.dungeon.CurrentLevel().AddEntity(ps.player)
2024-05-12 23:22:39 +03:00
}
func (ps *PlayingState) SwitchToPreviousLevel() {
if !ps.dungeon.HasPreviousLevel() {
ps.nextGameState = CreateDialogState(
2024-05-30 23:39:54 +03:00
ps.inputSystem,
ps.turnSystem,
2024-05-12 23:22:39 +03:00
ui.CreateOkDialog(
"The Surface",
"You feel the gentle, yet chilling breeze of the surface make its way through the weaving cavern tunnels, the very same you had to make your way through to get where you are. There is nothing above that you need. Find the last light, or die trying.",
"Continue",
40,
func() {
ps.nextGameState = ps
},
),
ps,
)
return
}
ps.dungeon.CurrentLevel().DropEntity(ps.player.UniqueId())
ps.dungeon.MoveToPreviousLevel()
2024-06-06 23:17:22 +03:00
ps.player.Positioned().Position = ps.dungeon.CurrentLevel().Ground().NextLevelStaircase().Position
2024-05-12 23:22:39 +03:00
ps.viewport = engine.CreateViewport(
engine.PositionAt(0, 0),
2024-06-06 23:17:22 +03:00
ps.dungeon.CurrentLevel().Ground().NextLevelStaircase().Position,
2024-05-12 23:22:39 +03:00
engine.SizeOf(80, 24),
tcell.StyleDefault,
)
2024-06-01 11:20:51 +03:00
ps.dungeon.CurrentLevel().AddEntity(ps.player)
2024-05-12 23:22:39 +03:00
}
func PickUpItemUnderPlayer(eventLog *engine.GameEventLog, dungeon *model.Dungeon, player *model.Player) (success bool) {
pos := player.Position()
item := dungeon.CurrentLevel().RemoveItemAt(pos.XY())
2024-05-12 23:22:39 +03:00
if item == nil {
eventLog.Log("There is no item to pick up here")
return false
2024-05-12 23:22:39 +03:00
}
success = player.Inventory().Push(item)
2024-05-03 13:46:32 +03:00
2024-05-12 23:22:39 +03:00
if !success {
eventLog.Log("Unable to pick up item")
dungeon.CurrentLevel().SetItemAt(pos.X(), pos.Y(), item)
2024-05-30 23:39:54 +03:00
return
}
2024-06-06 23:17:22 +03:00
if item.Named() != nil {
itemName := item.Named().Name
eventLog.Log("You picked up " + itemName)
2024-06-06 23:17:22 +03:00
} else {
eventLog.Log("You picked up an item")
2024-06-06 23:17:22 +03:00
}
return true
2024-05-30 23:39:54 +03:00
}
func (ps *PlayingState) OnTick(dt int64) (nextState GameState) {
ps.nextGameState = ps
2024-05-30 23:39:54 +03:00
ps.turnSystem.NextTurn()
2024-05-12 23:22:39 +03:00
return ps.nextGameState
}
2024-05-06 20:43:35 +03:00
func (ps *PlayingState) CollectDrawables() []engine.Drawable {
2024-05-30 23:39:54 +03:00
mainCameraDrawingInstructions := engine.CreateDrawingInstructions(func(v views.View) {
2024-05-21 23:08:51 +03:00
visibilityMap := engine.ComputeFOV(
2024-06-06 23:28:06 +03:00
func(x, y int) model.Tile {
2024-06-06 23:17:22 +03:00
model.Map_MarkExplored(ps.dungeon.CurrentLevel().Ground(), x, y)
2024-05-21 23:08:51 +03:00
return ps.dungeon.CurrentLevel().TileAt(x, y)
},
2024-06-06 23:17:22 +03:00
func(x, y int) bool { return model.Map_IsInBounds(ps.dungeon.CurrentLevel().Ground(), x, y) },
func(x, y int) bool { return ps.dungeon.CurrentLevel().TileAt(x, y).Opaque() },
2024-05-21 23:08:51 +03:00
ps.player.Position().X(), ps.player.Position().Y(),
13,
)
2024-05-03 13:46:32 +03:00
ps.viewport.DrawFromProvider(v, func(x, y int) (rune, tcell.Style) {
2024-05-21 23:08:51 +03:00
tile := visibilityMap[engine.PositionAt(x, y)]
if tile != nil {
2024-06-10 23:20:38 +03:00
if tile.Entity() != nil {
return tile.Entity().Entity.Presentable().Rune, tile.Entity().Entity.Presentable().Style
2024-06-06 23:17:22 +03:00
}
if tile.Item() != nil {
return tile.Item().Item.TileIcon(), tile.Item().Item.Style()
}
return tile.DefaultPresentation()
}
2024-06-06 23:17:22 +03:00
explored := model.Map_ExploredTileAt(ps.dungeon.CurrentLevel().Ground(), x, y)
2024-05-21 23:08:51 +03:00
if explored != nil {
2024-06-06 23:17:22 +03:00
return explored.DefaultPresentation()
2024-05-21 23:08:51 +03:00
}
2024-05-03 13:46:32 +03:00
return ' ', tcell.StyleDefault
})
2024-05-30 23:39:54 +03:00
})
drawables := []engine.Drawable{}
drawables = append(drawables, mainCameraDrawingInstructions)
if ps.viewShortLogs {
drawables = append(drawables, ps.uiEventLog)
}
drawables = append(drawables, ps.healthBar)
return drawables
}