code wiki / _hdl_build / nx_game_agent.nx
nx_game_agent.nx source
↩ module page · 236 lines · 10065 B
1// nx_game_agent.nx -- the NPC/enemy BRAIN (ws=game-interact rung 2; the Palworld wilds half).
2// A wild roams its home range, PERCEIVES the player (radius + genuine Bresenham LOS through the shared
3// wall grid), CHASES via the certified pf_astar (real routing -- proven in-gate against a maze that
4// jams a greedy chaser), ATTACKS when adjacent, FLEES when weak, and can SUBMIT (the breeders hook).
5// Design split (single-responsibility): the brain DECIDES and EMITS EVENTS; the game RESOLVES them
6// (battle, capture, dialogue). No resolution logic lives here.
7// Species behaviour numbers are DATA ROWS (aggro radius, flee threshold), never inline constants --
8// tuning a species touches data, not logic. All integer, per-actor seeded, deterministic. LIB, no main.
9// license_tier: ORIGINAL
10import "nx_syscalls.nx"
11import "nx_game_actor.nx"
12import "nx_pathfind.nx"
13
14// ---- agent states (stored in A_STATE) ----
15const AG_IDLE: i64 = 0
16const AG_PATROL: i64 = 1
17const AG_CHASE: i64 = 2
18const AG_ATTACK: i64 = 3 // adjacent this tick; event emitted, game resolves
19const AG_FLEE: i64 = 4
20const AG_SUBMIT: i64 = 5 // out of the brain's hands (breeders lane owns it)
21const AG_DEAD: i64 = 6
22
23// ---- species behaviour table: DATA rows, stride 4 = [aggro_radius, flee_hp_permil, patrol_radius, speed_num]
24// speed_num: agent moves speed_num ticks of every 4 (integer duty cycle; 4 = every tick)
25const SP_STRIDE: i64 = 4
26func ag_species_default(tbl: *i64) -> i64 {
27 // row 0: docile scout -- short aggro, flees early, wide patrol, slow
28 tbl[0] = 4; tbl[1] = 500; tbl[2] = 5; tbl[3] = 2
29 // row 1: hunter -- long aggro, fights to 250 permil, tight patrol, normal
30 tbl[SP_STRIDE + 0] = 7; tbl[SP_STRIDE + 1] = 250; tbl[SP_STRIDE + 2] = 3; tbl[SP_STRIDE + 3] = 3
31 // row 2: guardian -- medium aggro, never flees, post-bound, fast
32 tbl[2*SP_STRIDE + 0] = 5; tbl[2*SP_STRIDE + 1] = 0; tbl[2*SP_STRIDE + 2] = 1; tbl[2*SP_STRIDE + 3] = 4
33 return 3
34}
35func ag_aggro(tbl: *i64, spec: i64) -> i64 { return tbl[spec*SP_STRIDE + 0] }
36func ag_fleehp(tbl: *i64, spec: i64) -> i64 { return tbl[spec*SP_STRIDE + 1] }
37func ag_patrolr(tbl: *i64, spec: i64) -> i64 { return tbl[spec*SP_STRIDE + 2] }
38func ag_speed(tbl: *i64, spec: i64) -> i64 { return tbl[spec*SP_STRIDE + 3] }
39
40// ---- event ring: [0]=count then rows stride 4 = [tick, kind, actor_handle, target_handle] ----
41const EV_STRIDE: i64 = 4
42const EV_ATTACK: i64 = 1
43const EV_SPOTTED: i64 = 2 // patrol->chase transition (the aggro moment; UI/audio hook)
44const EV_LOST: i64 = 3 // chase->patrol (player escaped)
45const EV_FLED: i64 = 4 // entered flee
46func ev_count(ev: *i64) -> i64 { return ev[0] }
47func ev_push(ev: *i64, cap: i64, tick: i64, kind: i64, a: i64, t: i64) -> i64 {
48 let n: i64 = ev[0]
49 if n >= cap { return 0-1 }
50 ev[1 + n*EV_STRIDE + 0] = tick
51 ev[1 + n*EV_STRIDE + 1] = kind
52 ev[1 + n*EV_STRIDE + 2] = a
53 ev[1 + n*EV_STRIDE + 3] = t
54 ev[0] = n + 1
55 return n
56}
57func ev_kind(ev: *i64, i: i64) -> i64 { return ev[1 + i*EV_STRIDE + 1] }
58func ev_actor(ev: *i64, i: i64) -> i64 { return ev[1 + i*EV_STRIDE + 2] }
59func ev_tick(ev: *i64, i: i64) -> i64 { return ev[1 + i*EV_STRIDE + 0] }
60
61// deterministic per-actor rng: fold the actor's seed with its tick counter
62func ag_rng(ar: *i64, hnd: i64) -> i64 {
63 var x: i64 = en_get(ar, hnd, A_SEED) + en_get(ar, hnd, A_TICK) * 2654435761
64 x = x ^ (x << 13); x = x ^ (x >> 7); x = x ^ (x << 17)
65 if x < 0 { x = 0 - x }
66 return x
67}
68
69// one greedy step toward (tx,ty) -- used for FLEE (away = toward the mirror point) and as the
70// no-path fallback. Tries the dominant axis first, then the other; respects all collision.
71func ag_step_toward(wd: *i64, ar: *i64, hnd: i64, tx: i64, ty: i64, out: *i64) -> i64 {
72 let x: i64 = en_get(ar, hnd, A_X)
73 let y: i64 = en_get(ar, hnd, A_Y)
74 var dx: i64 = 0
75 if tx > x { dx = 1 }
76 if tx < x { dx = 0-1 }
77 var dy: i64 = 0
78 if ty > y { dy = 1 }
79 if ty < y { dy = 0-1 }
80 var adx: i64 = tx - x
81 if adx < 0 { adx = 0 - adx }
82 var ady: i64 = ty - y
83 if ady < 0 { ady = 0 - ady }
84 if adx >= ady {
85 if dx != 0 { if ac_step(wd, ar, hnd, dx, 0, out) == AC_MOVED { return AC_MOVED } }
86 if dy != 0 { if ac_step(wd, ar, hnd, 0, dy, out) == AC_MOVED { return AC_MOVED } }
87 }
88 if adx < ady {
89 if dy != 0 { if ac_step(wd, ar, hnd, 0, dy, out) == AC_MOVED { return AC_MOVED } }
90 if dx != 0 { if ac_step(wd, ar, hnd, dx, 0, out) == AC_MOVED { return AC_MOVED } }
91 }
92 return AC_WALL
93}
94
95// scratch layout for the pathfind arrays: 6 arrays of w*h each, caller-owned
96func ag_scratch_words(w: i64, h: i64) -> i64 { return w*h*6 }
97
98// A* step: route from the agent to the target's cell, take the FIRST step of the path.
99// Falls back to greedy when no path (boxed in). Returns AC_MOVED/AC_WALL/AC_BUMP...
100func ag_step_astar(wd: *i64, ar: *i64, hnd: i64, tx: i64, ty: i64, scr: *i64, out: *i64) -> i64 {
101 let w: i64 = wd_w(wd)
102 let h: i64 = wd_h(wd)
103 let n: i64 = w*h
104 let x: i64 = en_get(ar, hnd, A_X)
105 let y: i64 = en_get(ar, hnd, A_Y)
106 let base: i64 = scr as i64
107 let g: *i64 = base as *i64
108 let f: *i64 = (base + n*8) as *i64
109 let cm: *i64 = (base + n*16) as *i64
110 let of: *i64 = (base + n*24) as *i64
111 let cl: *i64 = (base + n*32) as *i64
112 let pa: *i64 = (base + n*40) as *i64
113 // target cell itself is occupied by the target actor; route to it anyway by asking for the
114 // target's cell but stepping only the FIRST path cell (which is open or triggers a bump = engage).
115 var plen: i64 = 0-1
116 // pf_astar refuses an occupied-by-wall target only; occupancy is not walls, so this is fine.
117 plen = pf_astar(wd_grid(wd), w, h, x, y, tx, ty, g, f, cm, of, cl, pa)
118 if plen > 0 {
119 // pf_astar convention: path[0] = the START cell, path[1] = the first step (verified in source)
120 let cell: i64 = pa[1]
121 let cx: i64 = cell % w
122 let cy: i64 = cell / w
123 return ac_step(wd, ar, hnd, cx - x, cy - y, out)
124 }
125 return ag_step_toward(wd, ar, hnd, tx, ty, out)
126}
127
128// deterministic patrol: seeded wander biased back toward home when beyond patrol radius
129func ag_step_patrol(wd: *i64, ar: *i64, hnd: i64, tbl: *i64, out: *i64) -> i64 {
130 let spec: i64 = en_get(ar, hnd, A_SPEC)
131 let hx: i64 = en_get(ar, hnd, A_HOMEX)
132 let hy: i64 = en_get(ar, hnd, A_HOMEY)
133 let x: i64 = en_get(ar, hnd, A_X)
134 let y: i64 = en_get(ar, hnd, A_Y)
135 let pr: i64 = ag_patrolr(tbl, spec)
136 var ddx: i64 = x - hx
137 if ddx < 0 { ddx = 0 - ddx }
138 var ddy: i64 = y - hy
139 if ddy < 0 { ddy = 0 - ddy }
140 if ddx + ddy > pr { return ag_step_toward(wd, ar, hnd, hx, hy, out) }
141 let r: i64 = ag_rng(ar, hnd)
142 let d: i64 = r & 3
143 var dx: i64 = 0
144 var dy: i64 = 0
145 if d == 0 { dx = 1 }
146 if d == 1 { dx = 0-1 }
147 if d == 2 { dy = 1 }
148 if d == 3 { dy = 0-1 }
149 return ac_step(wd, ar, hnd, dx, dy, out)
150}
151
152// ---- THE BRAIN TICK. Decides + moves ONE agent; emits events; never resolves outcomes. ----
153func ag_tick(wd: *i64, ar: *i64, hnd: i64, hplayer: i64, tbl: *i64, scr: *i64,
154 ev: *i64, evcap: i64, tick: i64, out: *i64) -> i64 {
155 if en_valid(ar, hnd) == 0 { return AG_DEAD }
156 if en_valid(ar, hplayer) == 0 { return en_get(ar, hnd, A_STATE) }
157 en_set(ar, hnd, A_TICK, en_get(ar, hnd, A_TICK) + 1)
158 var st: i64 = en_get(ar, hnd, A_STATE)
159 if st == AG_SUBMIT { return st }
160 if st == AG_DEAD { return st }
161 if st == AG_IDLE { st = AG_PATROL }
162 let spec: i64 = en_get(ar, hnd, A_SPEC)
163
164 // duty cycle: a slow species thinks every tick but MOVES only speed_num of every 4
165 let duty: i64 = en_get(ar, hnd, A_TICK) % 4
166 var may_move: i64 = 0
167 if duty < ag_speed(tbl, spec) { may_move = 1 }
168
169 // ---- perception: radius AND genuine LOS (walls block sight) ----
170 let x: i64 = en_get(ar, hnd, A_X)
171 let y: i64 = en_get(ar, hnd, A_Y)
172 let px: i64 = en_get(ar, hplayer, A_X)
173 let py: i64 = en_get(ar, hplayer, A_Y)
174 let rr: i64 = ag_aggro(tbl, spec)
175 var sees: i64 = 0
176 if ac_dist2(ar, hnd, hplayer) <= rr*rr {
177 if ac_los(wd, x, y, px, py) == 1 { sees = 1 }
178 }
179
180 // ---- flee check first (survival outranks aggression) ----
181 let hp: i64 = en_get(ar, hnd, A_HP)
182 let mhp: i64 = en_get(ar, hnd, A_MAXHP)
183 let fleebar: i64 = ag_fleehp(tbl, spec)
184 var weak: i64 = 0
185 if mhp > 0 { if hp * 1000 / mhp < fleebar { weak = 1 } }
186 if weak == 1 {
187 if st != AG_FLEE { ev_push(ev, evcap, tick, EV_FLED, hnd, hplayer) }
188 st = AG_FLEE
189 }
190
191 if st == AG_FLEE {
192 en_set(ar, hnd, A_STATE, AG_FLEE)
193 if may_move == 1 {
194 // away = toward the mirror of the player across the agent
195 ag_step_toward(wd, ar, hnd, x + (x - px), y + (y - py), out)
196 }
197 return AG_FLEE
198 }
199
200 if st == AG_PATROL {
201 if sees == 1 {
202 ev_push(ev, evcap, tick, EV_SPOTTED, hnd, hplayer)
203 st = AG_CHASE
204 }
205 if st == AG_PATROL {
206 en_set(ar, hnd, A_STATE, AG_PATROL)
207 if may_move == 1 { ag_step_patrol(wd, ar, hnd, tbl, out) }
208 return AG_PATROL
209 }
210 }
211
212 // CHASE / ATTACK
213 if ac_adjacent(ar, hnd, hplayer) == 1 {
214 en_set(ar, hnd, A_STATE, AG_ATTACK)
215 ev_push(ev, evcap, tick, EV_ATTACK, hnd, hplayer)
216 return AG_ATTACK
217 }
218 en_set(ar, hnd, A_STATE, AG_CHASE)
219 if sees == 0 {
220 // lost sight THIS tick: drop back to patrol and say so (simple, deterministic)
221 ev_push(ev, evcap, tick, EV_LOST, hnd, hplayer)
222 en_set(ar, hnd, A_STATE, AG_PATROL)
223 return AG_PATROL
224 }
225 if may_move == 1 {
226 let rc: i64 = ag_step_astar(wd, ar, hnd, px, py, scr, out)
227 if rc == AC_BUMP {
228 if out[0] == hplayer {
229 en_set(ar, hnd, A_STATE, AG_ATTACK)
230 ev_push(ev, evcap, tick, EV_ATTACK, hnd, hplayer)
231 return AG_ATTACK
232 }
233 }
234 }
235 return AG_CHASE
236}