Simulated arrows. Why...

This commit is contained in:
Miroslav Vasilev 2024-06-10 23:20:38 +03:00
parent e8f3c6ca9e
commit 855fa8dfc1
12 changed files with 246 additions and 85 deletions

View file

@ -38,3 +38,20 @@ func (p *Path) Next() (current Position, hasNext bool) {
return p.CurrentPosition(), true
}
func LinePath(from, to Position) *Path {
points := make([]Position, 0)
n := float64(from.Distance(to))
for step := 0.0; step <= n; step += 1.0 {
t := 0.0
if n != 0 {
t = step / n
}
points = append(points, LerpPositions(from, to, t))
}
return CreatePath(from, to, points)
}

View file

@ -35,7 +35,7 @@ func FindPath(from Position, to Position, maxDistance int, isPassable func(x, y
iteration++
if iteration >= maxDistance {
return nil
break
}
if len(openList) == 0 {

View file

@ -173,3 +173,14 @@ func AbsInt(val int) int {
}
return val
}
func Lerp(start, end float64, t float64) float64 {
return start*(1.0-t) + end*t
}
func LerpPositions(p0, p1 Position, t float64) Position {
return PositionAt(
int(math.Round(Lerp(float64(p0.x), float64(p1.x), t))),
int(math.Round(Lerp(float64(p0.y), float64(p1.y), t))),
)
}

View file

@ -87,6 +87,11 @@ type Entity_DropTableComponent struct {
DropTable *LootTable
}
type Entity_ProjectileComponent struct {
Source Entity
Path *engine.Path
}
type Entity interface {
UniqueId() uuid.UUID
@ -99,6 +104,7 @@ type Entity interface {
Stats() *Entity_StatsHolderComponent
HealthData() *Entity_HealthComponent
DropTable() *Entity_DropTableComponent
Projectile() *Entity_ProjectileComponent
}
type BaseEntity struct {
@ -113,6 +119,7 @@ type BaseEntity struct {
stats *Entity_StatsHolderComponent
damageable *Entity_HealthComponent
dropTable *Entity_DropTableComponent
projectile *Entity_ProjectileComponent
}
func (be *BaseEntity) UniqueId() uuid.UUID {
@ -155,6 +162,10 @@ func (be *BaseEntity) DropTable() *Entity_DropTableComponent {
return be.dropTable
}
func (be *BaseEntity) Projectile() *Entity_ProjectileComponent {
return be.projectile
}
func CreateEntity(components ...func(*BaseEntity)) *BaseEntity {
e := &BaseEntity{
id: uuid.New(),
@ -250,3 +261,12 @@ func WithDropTable(table map[int]ItemSupplier) func(e *BaseEntity) {
}
}
}
func WithProjectileData(source Entity, path *engine.Path) func(e *BaseEntity) {
return func(e *BaseEntity) {
e.projectile = &Entity_ProjectileComponent{
Source: source,
Path: path,
}
}
}

View file

@ -5,11 +5,86 @@ import (
"mvvasilev/last_light/engine"
)
// func ProjectileBehavior() func(npc Entity) (complete bool, requeue bool) {
// return func(npc Entity) (complete bool, requeue bool) {
type ProjectileSprite rune
// }
// }
//
// \ | /
//
// ─ + ─
//
// / | \
const (
ProjectileSprite_NorthSouth ProjectileSprite = '|'
ProjectileSprite_EastWest = '─'
ProjectileSprite_NorthEastSouthWest = '/'
ProjectileSprite_NorthWestSouthEast = '\\'
)
func ProjectileBehavior(eventLog *engine.GameEventLog, dungeon *Dungeon) func(npc Entity) (complete bool, requeue bool) {
return func(npc Entity) (complete bool, requeue bool) {
hasNext := ProjectileFollowPathNext(npc, eventLog, dungeon)
return !hasNext, false
}
}
func ProjectileFollowPathNext(npc Entity, eventLog *engine.GameEventLog, dungeon *Dungeon) (hasNext bool) {
projectileData := npc.Projectile()
positionData := npc.Positioned()
if projectileData == nil || positionData == nil {
return false
}
path := projectileData.Path
next, hasNext := path.Next()
nextTile := dungeon.CurrentLevel().TileAt(next.XY())
nextTileEntityData := nextTile.Entity()
dungeon.CurrentLevel().DropEntity(npc.UniqueId())
positionData.Position = next
// The next tile is impassable ( wall, void, etc. ) and contains no entity to damage
// This is the end of the path
if nextTileEntityData == nil && !nextTile.Passable() {
return false
}
// Otherwise, if the tile is passible, but also the end of the path, stop here and despawn the projectile
if nextTileEntityData == nil && next == projectileData.Path.To() {
return false
}
// The next tile contains an entity, do damage to it if we have damage data
if nextTileEntityData != nil {
// The arrow strikes against its master, but to no avail, for I decree it to be illegal
if nextTileEntityData.Entity == projectileData.Source {
return
}
// Futher I decree, that should the arrow striketh at thyself, it shall be blocked from doing so
if nextTileEntityData.Entity == npc {
return
}
if projectileData.Source == nil {
return false
}
ExecuteAttack(eventLog, projectileData.Source, nextTileEntityData.Entity)
return false
}
dungeon.CurrentLevel().AddEntity(npc)
return
}
func HostileNPCBehavior(eventLog *engine.GameEventLog, dungeon *Dungeon, player *Player) func(npc Entity) (complete bool, requeue bool) {
return func(npc Entity) (complete bool, requeue bool) {
@ -121,6 +196,10 @@ func WithinHitRange(pos engine.Position, otherPos engine.Position) bool {
func ExecuteAttack(eventLog *engine.GameEventLog, attacker, victim Entity) {
hit, precision, evasion, dmg, dmgType := CalculateAttack(attacker, victim)
if attacker.Projectile() != nil {
attacker = attacker.Projectile().Source
}
attackerName := "Unknown"
if attacker.Named() != nil {
@ -138,6 +217,10 @@ func ExecuteAttack(eventLog *engine.GameEventLog, attacker, victim Entity) {
return
}
if victim.HealthData() == nil {
return
}
victim.HealthData().Health -= dmg
if victim.HealthData().Health <= 0 {

View file

@ -12,10 +12,13 @@ const (
ImpClaws specialItemType = 100_000 + iota
)
func Entity_ArrowProjectile(startX, startY int, targetX, targetY int) Entity {
func Entity_ArrowProjectile(source Entity, path *engine.Path, eventLog *engine.GameEventLog, dungeon *Dungeon) Entity {
return CreateEntity(
WithName("Arrow"),
WithPosition(engine.PositionAt(startX, startY)),
WithPosition(path.From()),
WithPresentation('?', tcell.StyleDefault),
WithProjectileData(source, path),
WithBehavior(1, ProjectileBehavior(eventLog, dungeon)),
)
}

View file

@ -9,6 +9,7 @@ import (
type Player struct {
Entity
inLookState bool
skipNextTurn bool
}
@ -64,3 +65,11 @@ func (p *Player) SkipNextTurn(skip bool) {
func (p *Player) IsNextTurnSkipped() bool {
return p.skipNextTurn
}
func (p *Player) IsInLookState() bool {
return p.inLookState
}
func (p *Player) SetInLookState(lookState bool) {
p.inLookState = lookState
}

View file

@ -90,7 +90,7 @@ func (d *Dungeon) HasNextLevel() bool {
type DungeonLevel struct {
ground Map
entitiesByPosition map[engine.Position][]Entity
entitiesByPosition map[engine.Position]Entity
entities map[uuid.UUID]Entity
}
@ -142,7 +142,7 @@ func CreateDungeonLevel(width, height int, dungeonType DungeonType) (dLevel *Dun
dLevel = &DungeonLevel{
ground: groundLevel,
entities: map[uuid.UUID]Entity{},
entitiesByPosition: map[engine.Position][]Entity{},
entitiesByPosition: map[engine.Position]Entity{},
}
if groundLevel.Rooms() == nil {
@ -238,11 +238,7 @@ func (d *DungeonLevel) AddEntity(entity Entity) {
d.entities[entity.UniqueId()] = entity
if entity.Positioned() != nil {
if d.entitiesByPosition[entity.Positioned().Position] == nil {
d.entitiesByPosition[entity.Positioned().Position] = []Entity{entity}
} else {
d.entitiesByPosition[entity.Positioned().Position] = append(d.entitiesByPosition[entity.Positioned().Position], entity)
}
d.entitiesByPosition[entity.Positioned().Position] = entity
}
}
@ -258,9 +254,7 @@ func (d *DungeonLevel) MoveEntityTo(uuid uuid.UUID, x, y int) {
ent.Positioned().Position = engine.PositionAt(x, y)
if d.entitiesByPosition[ent.Positioned().Position] == nil {
d.entitiesByPosition[ent.Positioned().Position] = []Entity{ent}
} else {
d.entitiesByPosition[ent.Positioned().Position] = append(d.entitiesByPosition[ent.Positioned().Position], ent)
d.entitiesByPosition[ent.Positioned().Position] = ent
}
}
@ -303,7 +297,7 @@ func (d *DungeonLevel) TileAt(x, y int) Tile {
tile := Map_TileAt(d.ground, x, y)
if entity != nil {
return CreateTileFromPrototype(tile, Tile_WithEntities(entity))
return CreateTileFromPrototype(tile, Tile_WithEntity(entity))
}
return tile
@ -316,14 +310,14 @@ func (d *DungeonLevel) IsTilePassable(x, y int) bool {
tile := d.TileAt(x, y)
if tile.Entities() != nil {
if tile.Entity() != nil {
return false
}
return tile.Passable()
}
func (d *DungeonLevel) EntitiesAt(x, y int) (e []Entity) {
func (d *DungeonLevel) EntityAt(x, y int) (e Entity) {
return d.entitiesByPosition[engine.PositionAt(x, y)]
}

View file

@ -1,10 +1,7 @@
package model
import (
"slices"
"github.com/gdamore/tcell/v2"
"github.com/google/uuid"
)
type Material uint
@ -26,7 +23,7 @@ type Tile_ItemComponent struct {
}
type Tile_EntityComponent struct {
Entities []Entity
Entity Entity
}
type Tile interface {
@ -40,9 +37,9 @@ type Tile interface {
RemoveItem()
WithItem(item Item)
Entities() *Tile_EntityComponent
RemoveEntity(uuid uuid.UUID)
AddEntity(entity Entity)
Entity() *Tile_EntityComponent
RemoveEntity()
WithEntity(entity Entity)
}
type BaseTile struct {
@ -52,8 +49,8 @@ type BaseTile struct {
material Material
passable, opaque, transparent bool
item *Tile_ItemComponent
entities *Tile_EntityComponent
item *Tile_ItemComponent
entity *Tile_EntityComponent
}
func CreateTileFromPrototype(prototype Tile, components ...func(*BaseTile)) Tile {
@ -121,46 +118,24 @@ func (t *BaseTile) WithItem(item Item) {
}
}
func (t *BaseTile) Entities() *Tile_EntityComponent {
return t.entities
func (t *BaseTile) Entity() *Tile_EntityComponent {
return t.entity
}
func (t *BaseTile) RemoveEntity(uuid uuid.UUID) {
if t.entities == nil {
return
func (t *BaseTile) WithEntity(entity Entity) {
t.entity = &Tile_EntityComponent{
Entity: entity,
}
t.entities.Entities = slices.DeleteFunc(t.entities.Entities, func(e Entity) bool { return e.UniqueId() == uuid })
}
func (t *BaseTile) AddEntity(entity Entity) {
if t.entities == nil {
t.entities = &Tile_EntityComponent{
Entities: []Entity{
entity,
},
}
return
}
t.entities.Entities = append(t.entities.Entities, entity)
func (t *BaseTile) RemoveEntity() {
t.entity = nil
}
func Tile_WithEntity(entity Entity) func(*BaseTile) {
return func(bt *BaseTile) {
bt.entities = &Tile_EntityComponent{
Entities: []Entity{
entity,
},
}
}
}
func Tile_WithEntities(entities []Entity) func(*BaseTile) {
return func(bt *BaseTile) {
bt.entities = &Tile_EntityComponent{
Entities: entities,
bt.entity = &Tile_EntityComponent{
Entity: entity,
}
}
}

View file

@ -5,6 +5,7 @@ import (
"mvvasilev/last_light/engine"
"mvvasilev/last_light/game/model"
"mvvasilev/last_light/game/systems"
"mvvasilev/last_light/game/ui"
"time"
"github.com/gdamore/tcell/v2"
@ -23,9 +24,14 @@ type LookState struct {
player *model.Player
dungeon *model.Dungeon
showLog bool
uiEventLog *ui.UIEventLog
showCursor bool
cursorPos engine.Position
lastCursorBlinkTime time.Time
nextGameState GameState
}
func CreateLookState(prevState GameState, eventLog *engine.GameEventLog, dungeon *model.Dungeon, inputSystem *systems.InputSystem, turnSystem *systems.TurnSystem, player *model.Player) *LookState {
@ -38,6 +44,8 @@ func CreateLookState(prevState GameState, eventLog *engine.GameEventLog, dungeon
eventLog: eventLog,
cursorPos: engine.PositionAt(0, 0),
lastCursorBlinkTime: time.Now(),
showLog: true,
uiEventLog: ui.CreateUIEventLog(0, 17, 80, 7, eventLog, tcell.StyleDefault),
}
}
@ -46,6 +54,8 @@ func (ls *LookState) InputContext() systems.InputContext {
}
func (ls *LookState) OnTick(dt int64) GameState {
ls.nextGameState = ls
switch ls.inputSystem.NextAction() {
case systems.InputAction_Move_North:
ls.cursorPos = ls.cursorPos.WithOffset(model.MovementDirectionOffset(model.North))
@ -55,15 +65,17 @@ func (ls *LookState) OnTick(dt int64) GameState {
ls.cursorPos = ls.cursorPos.WithOffset(model.MovementDirectionOffset(model.East))
case systems.InputAction_Move_West:
ls.cursorPos = ls.cursorPos.WithOffset(model.MovementDirectionOffset(model.West))
case systems.InputAction_OpenLogs:
ls.showLog = !ls.showLog
case systems.InputAction_Describe:
ls.Describe()
case systems.InputAction_Shoot:
ls.ShootEquippedWeapon()
case systems.InputAction_Menu_Exit:
return ls.prevState
ls.nextGameState = ls.prevState
}
return ls
return ls.nextGameState
}
func (ls *LookState) ShootEquippedWeapon() {
@ -90,15 +102,49 @@ func (ls *LookState) ShootEquippedWeapon() {
}
// TODO: Projectiles
dX, dY := ls.lookCursorCoordsToDungeonCoords()
distance := engine.PositionAt(dX, dY).Distance(ls.player.Position())
if distance >= 12 {
ls.eventLog.Log("Can't see in the dark that far")
return
}
path := engine.LinePath(
ls.player.Position(),
engine.PositionAt(dX, dY),
)
if path == nil {
ls.eventLog.Log("Can't shoot there, something is in the way")
return
}
projectile := model.Entity_ArrowProjectile(ls.player, path, ls.eventLog, ls.dungeon)
ls.turnSystem.Schedule(
projectile.Behavior().Speed,
projectile.Behavior().Behavior,
)
ls.player.SkipNextTurn(true)
ls.turnSystem.NextTurn()
ls.nextGameState = ls.prevState
}
func (ls *LookState) Describe() {
dX, dY := ls.lookCursorCoordsToDungeonCoords()
distance := engine.PositionAt(dX, dY).Distance(ls.player.Position())
if distance >= 12 {
ls.eventLog.Log("Can't see in the dark that far")
return
}
isVisibleFromPlayer, lastTile := model.HasLineOfSight(ls.dungeon, ls.player.Position(), engine.PositionAt(dX, dY))
if !isVisibleFromPlayer {
@ -110,10 +156,10 @@ func (ls *LookState) Describe() {
tile := ls.dungeon.CurrentLevel().TileAt(dX, dY)
entities := tile.Entities()
entity := tile.Entity()
if entities != nil {
ls.DescribeEntities(entities.Entities)
if entity != nil {
ls.DescribeEntity(entity.Entity)
return
}
@ -131,27 +177,25 @@ func (ls *LookState) Describe() {
ls.eventLog.Log(fmt.Sprintf("%s: %s", materialName, materialDesc))
}
func (ls *LookState) DescribeEntities(entities []model.Entity) {
if entities == nil {
func (ls *LookState) DescribeEntity(entity model.Entity) {
if entity == nil {
return
}
for _, entity := range entities {
if entity == ls.player {
ls.eventLog.Log("You")
if entity == ls.player {
ls.eventLog.Log("You")
continue
}
return
}
if entity.Named() == nil {
continue
}
if entity.Named() == nil {
return
}
if entity.Described() != nil {
ls.eventLog.Log(fmt.Sprintf("%s: %s", entity.Named().Name, entity.Described().Description))
} else {
ls.eventLog.Log(entity.Named().Name)
}
if entity.Described() != nil {
ls.eventLog.Log(fmt.Sprintf("%s: %s", entity.Named().Name, entity.Described().Description))
} else {
ls.eventLog.Log(entity.Named().Name)
}
}
@ -209,5 +253,9 @@ func (ls *LookState) CollectDrawables() []engine.Drawable {
}
}))
if ls.showLog {
drawables = append(drawables, ls.uiEventLog)
}
return drawables
}

View file

@ -64,7 +64,7 @@ func CreatePlayingState(turnSystem *systems.TurnSystem, inputSystem *systems.Inp
case systems.InputAction_OpenInventory:
s.nextGameState = CreateInventoryScreenState(s.eventLog, s.dungeon, s.inputSystem, s.turnSystem, s.player, s)
case systems.InputAction_EnterLookMode:
s.viewShortLogs = !s.viewShortLogs
s.viewShortLogs = false
s.nextGameState = CreateLookState(s, s.eventLog, s.dungeon, s.inputSystem, s.turnSystem, s.player)
case systems.InputAction_PickUpItem:
complete = PickUpItemUnderPlayer(s.eventLog, s.dungeon, s.player)
@ -162,7 +162,7 @@ func (ps *PlayingState) MovePlayer(direction model.Direction) (success bool) {
newPlayerPos := ps.player.Position().WithOffset(model.MovementDirectionOffset(direction))
ent := ps.dungeon.CurrentLevel().EntitiesAt(newPlayerPos.XY())[0]
ent := ps.dungeon.CurrentLevel().EntityAt(newPlayerPos.XY())
// We are moving into an entity with health data. Attack it.
if ent != nil && ent.HealthData() != nil {
@ -334,8 +334,8 @@ func (ps *PlayingState) CollectDrawables() []engine.Drawable {
if tile != nil {
if tile.Entities() != nil {
return tile.Entities().Entities[0].Presentable().Rune, tile.Entities().Entities[0].Presentable().Style
if tile.Entity() != nil {
return tile.Entity().Entity.Presentable().Rune, tile.Entity().Entity.Presentable().Style
}
if tile.Item() != nil {

View file

@ -93,6 +93,7 @@ func CreateInputSystemWithDefaultBindings() *InputSystem {
InputKeyOf(InputContext_Look, 0, tcell.KeyRight, 0): InputAction_Move_East,
InputKeyOf(InputContext_Look, 0, tcell.KeyRune, 'd'): InputAction_Describe,
InputKeyOf(InputContext_Look, 0, tcell.KeyRune, 'a'): InputAction_Shoot,
InputKeyOf(InputContext_Look, 0, tcell.KeyRune, 'l'): InputAction_OpenLogs,
InputKeyOf(InputContext_Look, 0, tcell.KeyESC, 0): InputAction_Menu_Exit,
},
}