2024-04-27 22:32:05 +03:00
package state
import (
2024-05-06 20:43:35 +03:00
"mvvasilev/last_light/engine"
2024-05-30 23:39:54 +03:00
"mvvasilev/last_light/game/input"
"mvvasilev/last_light/game/npc"
2024-05-21 23:08:51 +03:00
"mvvasilev/last_light/game/player"
2024-05-30 23:39:54 +03:00
"mvvasilev/last_light/game/rpg"
"mvvasilev/last_light/game/turns"
2024-05-12 23:22:39 +03:00
"mvvasilev/last_light/game/ui"
2024-05-06 18:59:14 +03:00
"mvvasilev/last_light/game/world"
2024-04-27 22:32:05 +03:00
"github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/views"
)
type PlayingState struct {
2024-05-30 23:39:54 +03:00
turnSystem * turns . TurnSystem
inputSystem * input . InputSystem
2024-05-21 23:08:51 +03:00
player * player . Player
2024-05-30 23:39:54 +03:00
someNPC * npc . BasicNPC
eventLog * engine . GameEventLog
uiEventLog * ui . UIEventLog
healthBar * ui . UIHealthBar
2024-05-12 23:22:39 +03:00
dungeon * world . Dungeon
2024-04-27 22:32:05 +03:00
2024-05-06 20:43:35 +03:00
viewport * engine . Viewport
2024-04-27 22:32:05 +03:00
2024-05-30 23:39:54 +03:00
viewShortLogs bool
2024-05-12 23:22:39 +03:00
nextGameState GameState
2024-04-27 22:32:05 +03:00
}
2024-05-30 23:39:54 +03:00
func CreatePlayingState ( turnSystem * turns . TurnSystem , inputSystem * input . InputSystem ) * PlayingState {
turnSystem . Clear ( )
2024-04-27 22:32:05 +03:00
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-04-27 22:32:05 +03:00
2024-05-12 23:22:39 +03:00
s . dungeon = world . CreateDungeon ( mapSize . Width ( ) , mapSize . Height ( ) , 1 )
2024-04-27 22:32:05 +03:00
2024-05-21 23:08:51 +03:00
s . player = player . CreatePlayer ( s . dungeon . CurrentLevel ( ) . PlayerSpawnPoint ( ) . XY ( ) )
2024-05-30 23:39:54 +03:00
s . player . Heal ( rpg . BaseMaxHealth ( s . player ) )
s . turnSystem . Schedule ( 10 , func ( ) ( complete bool , requeue bool ) {
requeue = true
complete = false
switch inputSystem . NextAction ( ) {
case input . InputAction_PauseGame :
s . nextGameState = PauseGame ( s , s . turnSystem , s . inputSystem )
case input . InputAction_OpenInventory :
s . nextGameState = CreateInventoryScreenState ( s . inputSystem , s . turnSystem , s . player , s )
case input . InputAction_PickUpItem :
s . PickUpItemUnderPlayer ( )
complete = true
case input . InputAction_Interact :
s . InteractBelowPlayer ( )
complete = true
case input . InputAction_OpenLogs :
s . viewShortLogs = ! s . viewShortLogs
case input . InputAction_MovePlayer_East :
s . MovePlayer ( npc . East )
complete = true
case input . InputAction_MovePlayer_West :
s . MovePlayer ( npc . West )
complete = true
case input . InputAction_MovePlayer_North :
s . MovePlayer ( npc . North )
complete = true
case input . InputAction_MovePlayer_South :
s . MovePlayer ( npc . South )
complete = true
default :
}
return
} )
s . someNPC = npc . CreateNPC ( s . dungeon . CurrentLevel ( ) . NextLevelStaircase ( ) )
2024-05-06 18:59:14 +03:00
2024-05-30 23:39:54 +03:00
s . turnSystem . Schedule ( 20 , func ( ) ( complete bool , requeue bool ) {
s . CalcPathToPlayerAndMove ( )
return true , true
} )
s . eventLog = engine . CreateGameEventLog ( 100 )
s . uiEventLog = ui . CreateUIEventLog ( 0 , 17 , 80 , 7 , s . eventLog , tcell . StyleDefault )
s . healthBar = ui . CreateHealthBar ( 68 , 0 , 12 , 3 , s . player . CurrentHealth ( ) , rpg . BaseMaxHealth ( s . player ) , tcell . StyleDefault )
2024-04-27 22:32:05 +03:00
2024-05-12 23:22:39 +03:00
s . dungeon . CurrentLevel ( ) . AddEntity ( s . player , '@' , tcell . StyleDefault )
s . dungeon . CurrentLevel ( ) . AddEntity ( s . someNPC , 'N' , tcell . StyleDefault )
2024-04-27 22:32:05 +03:00
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-05-12 23:22:39 +03:00
s . dungeon . CurrentLevel ( ) . PlayerSpawnPoint ( ) ,
2024-05-06 21:47:20 +03:00
engine . SizeOf ( 80 , 24 ) ,
2024-04-27 22:32:05 +03:00
tcell . StyleDefault ,
)
2024-05-12 23:22:39 +03:00
s . nextGameState = s
2024-04-27 22:32:05 +03:00
return s
}
2024-05-30 23:39:54 +03:00
func ( s * PlayingState ) InputContext ( ) input . Context {
return input . InputContext_Play
2024-04-27 22:32:05 +03:00
}
2024-05-30 23:39:54 +03:00
func ( ps * PlayingState ) MovePlayer ( direction npc . Direction ) {
if direction == npc . DirectionNone {
2024-04-27 22:32:05 +03:00
return
}
2024-05-30 23:39:54 +03:00
newPlayerPos := ps . player . Position ( ) . WithOffset ( npc . MovementDirectionOffset ( direction ) )
2024-04-27 22:32:05 +03:00
2024-05-12 23:22:39 +03:00
if ps . dungeon . CurrentLevel ( ) . IsTilePassable ( newPlayerPos . XY ( ) ) {
2024-05-30 23:39:54 +03:00
dx , dy := npc . MovementDirectionOffset ( direction )
2024-05-12 23:22:39 +03:00
ps . dungeon . CurrentLevel ( ) . MoveEntity ( ps . player . UniqueId ( ) , dx , dy )
2024-04-27 22:32:05 +03:00
ps . viewport . SetCenter ( ps . player . Position ( ) )
}
2024-05-30 23:39:54 +03:00
ps . eventLog . Log ( "You moved " + npc . DirectionName ( direction ) )
2024-04-27 22:32:05 +03:00
}
2024-05-12 23:22:39 +03:00
func ( ps * PlayingState ) InteractBelowPlayer ( ) {
playerPos := ps . player . Position ( )
if playerPos == ps . dungeon . CurrentLevel ( ) . NextLevelStaircase ( ) {
ps . SwitchToNextLevel ( )
return
}
if playerPos == ps . dungeon . CurrentLevel ( ) . PreviousLevelStaircase ( ) {
ps . SwitchToPreviousLevel ( )
return
}
}
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 ( )
ps . player . MoveTo ( ps . dungeon . CurrentLevel ( ) . PlayerSpawnPoint ( ) )
ps . viewport = engine . CreateViewport (
engine . PositionAt ( 0 , 0 ) ,
ps . dungeon . CurrentLevel ( ) . PlayerSpawnPoint ( ) ,
engine . SizeOf ( 80 , 24 ) ,
tcell . StyleDefault ,
)
ps . dungeon . CurrentLevel ( ) . AddEntity ( ps . player , '@' , tcell . StyleDefault )
}
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 ( )
ps . player . MoveTo ( ps . dungeon . CurrentLevel ( ) . NextLevelStaircase ( ) )
ps . viewport = engine . CreateViewport (
engine . PositionAt ( 0 , 0 ) ,
ps . dungeon . CurrentLevel ( ) . NextLevelStaircase ( ) ,
engine . SizeOf ( 80 , 24 ) ,
tcell . StyleDefault ,
)
ps . dungeon . CurrentLevel ( ) . AddEntity ( ps . player , '@' , tcell . StyleDefault )
}
2024-05-03 13:46:32 +03:00
func ( ps * PlayingState ) PickUpItemUnderPlayer ( ) {
pos := ps . player . Position ( )
2024-05-12 23:22:39 +03:00
item := ps . dungeon . CurrentLevel ( ) . RemoveItemAt ( pos . XY ( ) )
if item == nil {
return
}
2024-05-21 23:08:51 +03:00
success := ps . player . Inventory ( ) . Push ( item )
2024-05-03 13:46:32 +03:00
2024-05-12 23:22:39 +03:00
if ! success {
2024-05-21 23:08:51 +03:00
ps . dungeon . CurrentLevel ( ) . SetItemAt ( pos . X ( ) , pos . Y ( ) , item )
2024-05-30 23:39:54 +03:00
return
}
itemName , _ := item . Name ( )
ps . eventLog . Log ( "You picked up " + itemName )
}
func ( ps * PlayingState ) HasLineOfSight ( start , end engine . Position ) bool {
positions := engine . CastRay ( start , end )
for _ , p := range positions {
if ps . dungeon . CurrentLevel ( ) . IsGroundTileOpaque ( p . XY ( ) ) {
return false
}
2024-05-12 23:22:39 +03:00
}
2024-05-30 23:39:54 +03:00
return true
2024-05-12 23:22:39 +03:00
}
2024-05-03 13:46:32 +03:00
2024-05-12 23:22:39 +03:00
func ( ps * PlayingState ) CalcPathToPlayerAndMove ( ) {
2024-05-30 23:39:54 +03:00
playerVisibleAndInRange := false
if ps . someNPC . Position ( ) . Distance ( ps . player . Position ( ) ) < 20 && ps . HasLineOfSight ( ps . someNPC . Position ( ) , ps . player . Position ( ) ) {
playerVisibleAndInRange = true
}
if ! playerVisibleAndInRange {
randomMove := npc . Direction ( engine . RandInt ( int ( npc . DirectionNone ) , int ( npc . East ) ) )
nextPos := ps . someNPC . Position ( )
switch randomMove {
case npc . North :
nextPos = nextPos . WithOffset ( 0 , - 1 )
case npc . South :
nextPos = nextPos . WithOffset ( 0 , + 1 )
case npc . West :
nextPos = nextPos . WithOffset ( - 1 , 0 )
case npc . East :
nextPos = nextPos . WithOffset ( + 1 , 0 )
default :
return
}
if ps . dungeon . CurrentLevel ( ) . IsTilePassable ( nextPos . XY ( ) ) {
ps . dungeon . CurrentLevel ( ) . MoveEntityTo (
ps . someNPC . UniqueId ( ) ,
nextPos . X ( ) ,
nextPos . Y ( ) ,
)
}
2024-05-13 01:01:39 +03:00
2024-05-03 13:46:32 +03:00
return
}
2024-05-12 23:22:39 +03:00
pathToPlayer := engine . FindPath (
ps . someNPC . Position ( ) ,
ps . player . Position ( ) ,
func ( x , y int ) bool {
if x == ps . player . Position ( ) . X ( ) && y == ps . player . Position ( ) . Y ( ) {
return true
}
2024-05-03 13:46:32 +03:00
2024-05-12 23:22:39 +03:00
return ps . dungeon . CurrentLevel ( ) . IsTilePassable ( x , y )
} ,
)
2024-05-03 13:46:32 +03:00
2024-05-12 23:22:39 +03:00
nextPos , hasNext := pathToPlayer . Next ( )
if ! hasNext {
2024-05-03 13:46:32 +03:00
return
}
2024-05-12 23:22:39 +03:00
if nextPos . Equals ( ps . player . Position ( ) ) {
return
}
ps . dungeon . CurrentLevel ( ) . MoveEntityTo ( ps . someNPC . UniqueId ( ) , nextPos . X ( ) , nextPos . Y ( ) )
2024-05-03 13:46:32 +03:00
}
2024-05-30 23:39:54 +03:00
func ( ps * PlayingState ) OnTick ( dt int64 ) ( nextState GameState ) {
ps . nextGameState = ps
2024-04-27 22:32:05 +03:00
2024-05-30 23:39:54 +03:00
ps . turnSystem . NextTurn ( )
2024-05-12 23:22:39 +03:00
return ps . nextGameState
2024-04-27 22:32:05 +03:00
}
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 (
func ( x , y int ) world . Tile {
ps . dungeon . CurrentLevel ( ) . Flatten ( ) . MarkExplored ( x , y )
return ps . dungeon . CurrentLevel ( ) . TileAt ( x , y )
} ,
func ( x , y int ) bool { return ps . dungeon . CurrentLevel ( ) . Flatten ( ) . IsInBounds ( x , y ) } ,
func ( x , y int ) bool { return ps . dungeon . CurrentLevel ( ) . Flatten ( ) . TileAt ( x , y ) . Opaque ( ) } ,
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 ) ]
2024-04-27 22:32:05 +03:00
if tile != nil {
return tile . Presentation ( )
}
2024-05-21 23:08:51 +03:00
explored := ps . dungeon . CurrentLevel ( ) . Flatten ( ) . ExploredTileAt ( x , y )
if explored != nil {
return explored . Presentation ( )
}
2024-05-03 13:46:32 +03:00
return ' ' , tcell . StyleDefault
2024-04-27 22:32:05 +03:00
} )
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
2024-04-27 22:32:05 +03:00
}