ludic migrate state packages <every example program> packages/ludic.lab/example/plate.ludic 1804 vars into 126 states, 64 into lets; 23498 edits in 460 files Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
119 lines
5.9 KiB
Text
119 lines
5.9 KiB
Text
# npcai_demo.ludic — the ludic.npcai stack (#61) exercised headlessly and
|
|
# deterministically, driving a body built from the ludic.shooter controller: the
|
|
# AI never moves the body directly, it writes the same intent fields the player
|
|
# controllers read, so the shooter's weapon/aim fire for an enemy unchanged.
|
|
#
|
|
# Proves: perception (spot a hostile), FSM chase (move toward), FSM attack (auto-aim
|
|
# + fire -> damage), utility flee (move away at low hp), and a companion follower.
|
|
#
|
|
# A full run prints: 1 1 1 1 1 1 1 1
|
|
#
|
|
# Build (from the repo root):
|
|
# LUDIC_MODULES=packages ludicc --headless examples/games/npcai_demo.ludic
|
|
program NpcAiDemo {
|
|
import "ludic.npcai/npcai.ludic"
|
|
import "ludic.shooter/shooter.ludic"
|
|
|
|
property Position { x: int = 0, y: int = 0 }
|
|
property Body {
|
|
vx: fixed = 0.0, vy: fixed = 0.0, gravity: fixed = 0.0, max_fall: fixed = 0.0,
|
|
rx: fixed = 0.0, ry: fixed = 0.0, policy: int = 1,
|
|
on_ground: int = 0, hit_wall: int = 0, hit_ceiling: int = 0
|
|
}
|
|
property Collider {
|
|
w: int = 0, h: int = 0, offx: int = 0, offy: int = 0,
|
|
is_trigger: int = 0, one_way: int = 0, layer: int = 0, mask: int = 0,
|
|
hit: int = 0, entered: int = 0, exited: int = 0
|
|
}
|
|
|
|
model Enemy { Position, Body, Collider, TopDown, Weapon, Faction, Stats, Vision, Memory, Brain }
|
|
model Ally { Position, Body, Collider, TopDown, Faction, Stats, Brain, Follower }
|
|
model Dummy { Position, Collider, Faction, Stats }
|
|
@OnSpawn(Enemy) handler EInit { }
|
|
@OnSpawn(Ally) handler AInit { }
|
|
@OnSpawn(Dummy) handler DInit { }
|
|
|
|
state NpcAiDemoState {
|
|
spotted: int = 0
|
|
}
|
|
@On(TargetSpotted) handler OnSpot(npc_ai_demo_st: mut NpcAiDemoState) { npc_ai_demo_st.spotted += 1 }
|
|
|
|
function tick_n(n: int) -> void { var i = 0; while i < n { tick_fixed(); i += 1 } }
|
|
function bi(b: bool) -> int { if b { return 1 }; return 0 }
|
|
function gx(e: int) -> int { let P = World.prop_id("Position"); return World.get(e, P, World.field_id(P, "x")) }
|
|
function gy(e: int) -> int { let P = World.prop_id("Position"); return World.get(e, P, World.field_id(P, "y")) }
|
|
function hp_of(e: int) -> int { let P = World.prop_id("Stats"); return World.get(e, P, World.field_id(P, "hp")) }
|
|
function set_pos(e: int, x: int, y: int) -> void {
|
|
let P = World.prop_id("Position")
|
|
World.set(e, P, World.field_id(P, "x"), x); World.set(e, P, World.field_id(P, "y"), y)
|
|
}
|
|
function set_hp(e: int, h: int) -> void { let P = World.prop_id("Stats"); World.set(e, P, World.field_id(P, "hp"), h) }
|
|
# the dummy target = Stats-carrying entity without a Brain.
|
|
function find_dummy() -> int {
|
|
let PS = World.prop_id("Stats")
|
|
let PBr = World.prop_id("Brain")
|
|
var e = World.query_next(PS, 0)
|
|
while e >= 0 { if World.has(e, PBr) == 0 { return e }; e = World.query_next(PS, e + 1) }
|
|
return -1
|
|
}
|
|
|
|
entry (npc_ai_demo_st: NpcAiDemoState) {
|
|
let PBr = World.prop_id("Brain")
|
|
|
|
Weapon.def("blaster", 4, 10, 10, 0, 1, 0)
|
|
|
|
# enemy (faction 2) auto-aims at the nearest enemy (aim_mode 3) and hunts with an
|
|
# FSM. dummy player (faction 1) sits 50px to the right.
|
|
spawn Enemy { Position { x: 100, y: 100 }, Collider { w: 12, h: 12 },
|
|
TopDown { move_speed: 3, aim_mode: 3 }, Weapon { def_id: 0 }, Faction { id: 2 },
|
|
Stats { hp: 40, max_hp: 40 }, Vision { range: 200, scan: 2 },
|
|
Brain { model: 0, attack_range: 60, flee_pct: 40 } }
|
|
let enemy = World.query_next(PBr, 0)
|
|
spawn Dummy { Position { x: 150, y: 100 }, Collider { w: 16, h: 16 }, Faction { id: 1 }, Stats { hp: 100, max_hp: 100 } }
|
|
let dummy = find_dummy()
|
|
|
|
# ---- A. perception: the enemy spots the hostile dummy -------------------
|
|
tick_n(4)
|
|
print(bi(npc_ai_demo_st.spotted >= 1)) # 1 — TargetSpotted fired
|
|
print(bi(Ai.target(enemy) == dummy)) # 1 — remembers the right target
|
|
|
|
# ---- B. FSM attack: in range -> auto-aim + fire -> dummy takes damage ---
|
|
let dhp0 = hp_of(dummy)
|
|
tick_n(40)
|
|
print(bi(Ai.state(enemy) == 2)) # 1 — attack state (dummy within 60px)
|
|
print(bi(hp_of(dummy) < dhp0)) # 1 — the AI's weapon hit the player
|
|
|
|
# ---- C. FSM chase: move the dummy away (still within vision) -> advance -
|
|
set_pos(dummy, 220, 100) # 120px: within range 200, beyond attack 60
|
|
tick_n(4) # re-perceive + begin chasing
|
|
print(bi(Ai.state(enemy) == 1)) # 1 — chase state (still out of attack range)
|
|
let ex0 = gx(enemy)
|
|
tick_n(30)
|
|
print(bi(gx(enemy) > ex0)) # 1 — advanced toward the target
|
|
|
|
# ---- D. utility flee: switch model + drop hp -> retreat ----------------
|
|
set_pos(enemy, 200, 100)
|
|
set_pos(dummy, 320, 100) # dummy to the right
|
|
Ai.set_model(enemy, 1) # utility
|
|
set_hp(enemy, 4) # 10% of 40 < flee_pct 40
|
|
tick_n(4)
|
|
let fx0 = gx(enemy)
|
|
tick_n(20)
|
|
print(bi(gx(enemy) < fx0)) # 1 — fled left, away from the target
|
|
|
|
# ---- E. companion follower advances toward its leader ------------------
|
|
spawn Ally { Position { x: 40, y: 40 }, Collider { w: 12, h: 12 },
|
|
TopDown { move_speed: 3 }, Faction { id: 2 }, Stats { hp: 30, max_hp: 30 },
|
|
Brain { model: 0 }, Follower { leader: -1, distance: 24 } }
|
|
# the ally is the Brain+Follower entity that isn't the enemy
|
|
var ally = World.query_next(PBr, 0)
|
|
while (ally >= 0) and (World.has(ally, World.prop_id("Follower")) == 0) { ally = World.query_next(PBr, ally + 1) }
|
|
Follower.set(ally, enemy, 24, 0) # follow the enemy as leader
|
|
set_pos(enemy, 300, 300) # leader far away
|
|
let ax0 = gx(ally)
|
|
tick_n(20)
|
|
print(bi(gx(ally) > ax0)) # 1 — companion moved toward the leader
|
|
|
|
quit()
|
|
}
|
|
}
|