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}