2024-04-27 22:32:05 +03:00
package state
import (
2024-06-01 11:20:51 +03:00
"fmt"
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"
2024-04-27 22:32:05 +03:00
"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
2024-06-06 23:28:06 +03:00
player * model . Player
someNPC 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-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-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 ( )
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-06-06 23:17:22 +03:00
s . dungeon = model . CreateDungeon ( mapSize . Width ( ) , mapSize . Height ( ) , 1 )
2024-04-27 22:32:05 +03:00
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 ( 10 , func ( ) ( complete bool , requeue bool ) {
requeue = true
complete = false
2024-06-06 23:17:22 +03:00
if s . player . HealthData ( ) . IsDead {
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_PickUpItem :
2024-05-30 23:39:54 +03:00
s . PickUpItemUnderPlayer ( )
complete = true
2024-06-06 23:17:22 +03:00
case systems . InputAction_Interact :
2024-05-30 23:39:54 +03:00
s . InteractBelowPlayer ( )
complete = true
2024-06-06 23:17:22 +03:00
case systems . InputAction_OpenLogs :
2024-05-30 23:39:54 +03:00
s . viewShortLogs = ! s . viewShortLogs
2024-06-06 23:17:22 +03:00
case systems . InputAction_MovePlayer_East :
s . MovePlayer ( model . East )
2024-05-30 23:39:54 +03:00
complete = true
2024-06-06 23:17:22 +03:00
case systems . InputAction_MovePlayer_West :
s . MovePlayer ( model . West )
2024-05-30 23:39:54 +03:00
complete = true
2024-06-06 23:17:22 +03:00
case systems . InputAction_MovePlayer_North :
s . MovePlayer ( model . North )
2024-05-30 23:39:54 +03:00
complete = true
2024-06-06 23:17:22 +03:00
case systems . InputAction_MovePlayer_South :
s . MovePlayer ( model . South )
2024-05-30 23:39:54 +03:00
complete = true
default :
}
return
} )
2024-06-06 23:17:22 +03:00
s . someNPC = model . CreateEntity (
model . WithPosition ( s . dungeon . CurrentLevel ( ) . Ground ( ) . NextLevelStaircase ( ) . Position ) ,
model . WithName ( "NPC" ) ,
model . WithPresentation ( 'n' , tcell . StyleDefault ) ,
model . WithStats ( model . RandomStats ( 21 , 1 , 20 , [ ] model . Stat { model . Stat_Attributes_Strength , model . Stat_Attributes_Constitution , model . Stat_Attributes_Intelligence , model . Stat_Attributes_Dexterity } ) ) ,
model . WithHealthData ( 20 , 20 , false ) ,
2024-06-01 11:20:51 +03:00
)
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 )
2024-06-06 23:17:22 +03:00
s . healthBar = ui . CreateHealthBar ( 68 , 0 , 12 , 3 , s . player , tcell . StyleDefault )
2024-04-27 22:32:05 +03:00
2024-06-01 11:20:51 +03:00
s . dungeon . CurrentLevel ( ) . AddEntity ( s . player )
s . dungeon . CurrentLevel ( ) . AddEntity ( s . someNPC )
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-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 ) ,
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-06-06 23:17:22 +03:00
func ( s * PlayingState ) InputContext ( ) systems . InputContext {
return systems . InputContext_Play
2024-04-27 22:32:05 +03:00
}
2024-06-06 23:17:22 +03:00
func ( ps * PlayingState ) MovePlayer ( direction model . Direction ) {
if direction == model . DirectionNone {
2024-04-27 22:32:05 +03:00
return
}
2024-06-06 23:17:22 +03:00
newPlayerPos := ps . player . Position ( ) . WithOffset ( model . MovementDirectionOffset ( direction ) )
ent := ps . dungeon . CurrentLevel ( ) . EntityAt ( newPlayerPos . XY ( ) )
// 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
}
ExecuteAttack ( ps . eventLog , ps . player , ent )
return
}
2024-04-27 22:32:05 +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 ( ) )
2024-04-27 22:32:05 +03:00
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 ) )
2024-04-27 22:32:05 +03:00
}
2024-06-06 23:17:22 +03:00
}
2024-04-27 22:32:05 +03:00
2024-06-06 23:28:06 +03:00
func ExecuteAttack ( eventLog * engine . GameEventLog , attacker , victim model . Entity ) {
2024-06-06 23:17:22 +03:00
hit , precision , evasion , dmg , dmgType := CalculateAttack ( attacker , victim )
2024-06-01 11:20:51 +03:00
2024-06-06 23:17:22 +03:00
attackerName := "Unknown"
2024-06-01 11:20:51 +03:00
2024-06-06 23:17:22 +03:00
if attacker . Named ( ) != nil {
attackerName = attacker . Named ( ) . Name
}
2024-06-01 11:20:51 +03:00
2024-06-06 23:17:22 +03:00
victimName := "Unknown"
if victim . Named ( ) != nil {
victimName = victim . Named ( ) . Name
}
if ! hit {
eventLog . Log ( fmt . Sprintf ( "%s attacked %s, but missed ( %v Evasion vs %v Precision)" , attackerName , victimName , evasion , precision ) )
return
}
victim . HealthData ( ) . Health -= dmg
if victim . HealthData ( ) . Health <= 0 {
victim . HealthData ( ) . IsDead = true
eventLog . Log ( fmt . Sprintf ( "%s attacked %s, and was victorious ( %v Evasion vs %v Precision)" , attackerName , victimName , evasion , precision ) )
return
}
eventLog . Log ( fmt . Sprintf ( "%s attacked %s, and hit for %v %v damage" , attackerName , victimName , dmg , model . DamageTypeName ( dmgType ) ) )
}
2024-06-06 23:28:06 +03:00
func CalculateAttack ( attacker , victim model . Entity ) ( hit bool , precisionRoll , evasionRoll int , damage int , damageType model . DamageType ) {
2024-06-06 23:17:22 +03:00
if attacker . Equipped ( ) != nil && attacker . Equipped ( ) . Inventory . AtSlot ( model . EquippedSlotDominantHand ) != nil {
weapon := attacker . Equipped ( ) . Inventory . AtSlot ( model . EquippedSlotDominantHand )
return model . PhysicalWeaponAttack ( attacker , weapon , victim )
} else {
return model . UnarmedAttack ( attacker , victim )
2024-06-01 11:20:51 +03:00
}
2024-04-27 22:32:05 +03:00
}
2024-05-12 23:22:39 +03:00
func ( ps * PlayingState ) InteractBelowPlayer ( ) {
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
}
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
}
}
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
}
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
}
2024-06-06 23:17:22 +03:00
if item . Named ( ) != nil {
itemName := item . Named ( ) . Name
ps . eventLog . Log ( "You picked up " + itemName )
} else {
ps . eventLog . Log ( "You picked up an item" )
}
2024-05-30 23:39:54 +03:00
}
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-06-01 11:20:51 +03:00
func ( ps * PlayingState ) PlayerWithinHitRange ( pos engine . Position ) bool {
return pos . WithOffset ( - 1 , 0 ) == ps . player . Position ( ) || pos . WithOffset ( + 1 , 0 ) == ps . player . Position ( ) || pos . WithOffset ( 0 , - 1 ) == ps . player . Position ( ) || pos . WithOffset ( 0 , + 1 ) == ps . player . Position ( )
}
2024-05-12 23:22:39 +03:00
func ( ps * PlayingState ) CalcPathToPlayerAndMove ( ) {
2024-06-06 23:17:22 +03:00
if ps . someNPC . HealthData ( ) . IsDead {
ps . dungeon . CurrentLevel ( ) . DropEntity ( ps . someNPC . UniqueId ( ) )
return
}
2024-05-30 23:39:54 +03:00
playerVisibleAndInRange := false
2024-06-06 23:17:22 +03:00
if ps . someNPC . Positioned ( ) . Position . Distance ( ps . player . Position ( ) ) < 20 && ps . HasLineOfSight ( ps . someNPC . Positioned ( ) . Position , ps . player . Position ( ) ) {
2024-05-30 23:39:54 +03:00
playerVisibleAndInRange = true
}
if ! playerVisibleAndInRange {
2024-06-06 23:17:22 +03:00
randomMove := model . Direction ( engine . RandInt ( int ( model . DirectionNone ) , int ( model . East ) ) )
2024-05-30 23:39:54 +03:00
2024-06-06 23:17:22 +03:00
nextPos := ps . someNPC . Positioned ( ) . Position
2024-05-30 23:39:54 +03:00
switch randomMove {
2024-06-06 23:17:22 +03:00
case model . North :
2024-05-30 23:39:54 +03:00
nextPos = nextPos . WithOffset ( 0 , - 1 )
2024-06-06 23:17:22 +03:00
case model . South :
2024-05-30 23:39:54 +03:00
nextPos = nextPos . WithOffset ( 0 , + 1 )
2024-06-06 23:17:22 +03:00
case model . West :
2024-05-30 23:39:54 +03:00
nextPos = nextPos . WithOffset ( - 1 , 0 )
2024-06-06 23:17:22 +03:00
case model . East :
2024-05-30 23:39:54 +03:00
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-06-06 23:17:22 +03:00
if ps . PlayerWithinHitRange ( ps . someNPC . Positioned ( ) . Position ) {
ExecuteAttack ( ps . eventLog , ps . someNPC , ps . player )
2024-06-01 11:20:51 +03:00
}
2024-05-12 23:22:39 +03:00
pathToPlayer := engine . FindPath (
2024-06-06 23:17:22 +03:00
ps . someNPC . Positioned ( ) . Position ,
2024-05-12 23:22:39 +03:00
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 (
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 ) ]
2024-04-27 22:32:05 +03:00
if tile != nil {
2024-06-06 23:17:22 +03:00
if tile . Entity ( ) != nil {
return tile . Entity ( ) . Entity . Presentable ( ) . Rune , tile . Entity ( ) . Entity . Presentable ( ) . Style
}
if tile . Item ( ) != nil {
return tile . Item ( ) . Item . TileIcon ( ) , tile . Item ( ) . Item . Style ( )
}
return tile . DefaultPresentation ( )
2024-04-27 22:32:05 +03:00
}
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-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
}