quake2ts 0.0.74 → 0.0.75

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3333,704 +3333,849 @@ function registerLightSpawns(registry) {
3333
3333
  });
3334
3334
  }
3335
3335
 
3336
- // src/entities/spawn.ts
3337
- var FIELD_LOOKUP = new Map(
3338
- ENTITY_FIELD_METADATA.map((field) => [field.name, field])
3339
- );
3340
- function parseVec3(text) {
3341
- const parts = text.trim().split(/\s+/);
3342
- const [x = 0, y = 0, z = 0] = parts.map((part) => Number.parseFloat(part)).map((value) => Number.isNaN(value) ? 0 : value);
3343
- return { x, y, z };
3336
+ // src/ai/constants.ts
3337
+ var RANGE_MELEE = 20;
3338
+ var RANGE_NEAR = 440;
3339
+ var RANGE_MID = 940;
3340
+ var FL_NOTARGET = 1 << 5;
3341
+ var FL_NOVISIBLE = 1 << 24;
3342
+ var SPAWNFLAG_MONSTER_AMBUSH = 1 << 0;
3343
+ var AIFlags = /* @__PURE__ */ ((AIFlags2) => {
3344
+ AIFlags2[AIFlags2["StandGround"] = 1] = "StandGround";
3345
+ AIFlags2[AIFlags2["TempStandGround"] = 2] = "TempStandGround";
3346
+ AIFlags2[AIFlags2["SoundTarget"] = 4] = "SoundTarget";
3347
+ AIFlags2[AIFlags2["LostSight"] = 8] = "LostSight";
3348
+ AIFlags2[AIFlags2["PursuitLastSeen"] = 16] = "PursuitLastSeen";
3349
+ AIFlags2[AIFlags2["PursueNext"] = 32] = "PursueNext";
3350
+ AIFlags2[AIFlags2["PursueTemp"] = 64] = "PursueTemp";
3351
+ AIFlags2[AIFlags2["HoldFrame"] = 128] = "HoldFrame";
3352
+ AIFlags2[AIFlags2["GoodGuy"] = 256] = "GoodGuy";
3353
+ AIFlags2[AIFlags2["Brutal"] = 512] = "Brutal";
3354
+ AIFlags2[AIFlags2["NoStep"] = 1024] = "NoStep";
3355
+ AIFlags2[AIFlags2["Ducked"] = 2048] = "Ducked";
3356
+ AIFlags2[AIFlags2["CombatPoint"] = 4096] = "CombatPoint";
3357
+ AIFlags2[AIFlags2["Medic"] = 8192] = "Medic";
3358
+ AIFlags2[AIFlags2["Resurrecting"] = 16384] = "Resurrecting";
3359
+ AIFlags2[AIFlags2["Pathing"] = 1073741824] = "Pathing";
3360
+ return AIFlags2;
3361
+ })(AIFlags || {});
3362
+ var TraceMask = /* @__PURE__ */ ((TraceMask2) => {
3363
+ TraceMask2[TraceMask2["Opaque"] = 1] = "Opaque";
3364
+ TraceMask2[TraceMask2["Window"] = 2] = "Window";
3365
+ return TraceMask2;
3366
+ })(TraceMask || {});
3367
+
3368
+ // src/ai/movement.ts
3369
+ function yawVector(yawDegrees, distance2) {
3370
+ if (distance2 === 0) {
3371
+ return { x: 0, y: 0, z: 0 };
3372
+ }
3373
+ const radians = degToRad(yawDegrees);
3374
+ return {
3375
+ x: Math.cos(radians) * distance2,
3376
+ y: Math.sin(radians) * distance2,
3377
+ z: 0
3378
+ };
3344
3379
  }
3345
- function parseBoolean(text) {
3346
- const normalized = text.trim().toLowerCase();
3347
- return normalized === "1" || normalized === "true" || normalized === "yes";
3380
+ function walkMove(self, yawDegrees, distance2) {
3381
+ const delta = yawVector(yawDegrees, distance2);
3382
+ const origin = self.origin;
3383
+ origin.x += delta.x;
3384
+ origin.y += delta.y;
3385
+ origin.z += delta.z;
3386
+ return true;
3348
3387
  }
3349
- function parseValue(type, value) {
3350
- switch (type) {
3351
- case "int":
3352
- return Number.parseInt(value, 10) || 0;
3353
- case "float":
3354
- return Number.parseFloat(value) || 0;
3355
- case "boolean":
3356
- return parseBoolean(value);
3357
- case "vec3":
3358
- return parseVec3(value);
3359
- case "string":
3360
- return value;
3361
- case "entity":
3362
- case "callback":
3363
- return void 0;
3364
- default:
3365
- return value;
3388
+ function changeYaw(self, deltaSeconds) {
3389
+ const current = angleMod(self.angles.y);
3390
+ const ideal = self.ideal_yaw;
3391
+ if (current === ideal) {
3392
+ self.angles.y = current;
3393
+ return;
3366
3394
  }
3367
- }
3368
- function applyEntityKeyValues(entity, values) {
3369
- if ("angle" in values && !("angles" in values)) {
3370
- entity.angles = { x: 0, y: Number.parseFloat(values.angle) || 0, z: 0 };
3395
+ const speed = self.yaw_speed * deltaSeconds * 10;
3396
+ let move = ideal - current;
3397
+ if (ideal > current) {
3398
+ if (move >= 180) move -= 360;
3399
+ } else if (move <= -180) {
3400
+ move += 360;
3371
3401
  }
3372
- for (const [key, rawValue] of Object.entries(values)) {
3373
- if (key.startsWith("_")) {
3374
- continue;
3375
- }
3376
- const descriptor = FIELD_LOOKUP.get(key);
3377
- if (!descriptor) {
3378
- continue;
3379
- }
3380
- const parsed = parseValue(descriptor.type, rawValue);
3381
- if (parsed !== void 0) {
3382
- entity[descriptor.name] = parsed;
3383
- }
3402
+ if (move > speed) move = speed;
3403
+ else if (move < -speed) move = -speed;
3404
+ self.angles.y = angleMod(current + move);
3405
+ }
3406
+ function facingIdeal(self) {
3407
+ const delta = angleMod(self.angles.y - self.ideal_yaw);
3408
+ const hasPathing = (self.monsterinfo.aiflags & 1073741824 /* Pathing */) !== 0;
3409
+ if (hasPathing) {
3410
+ return !(delta > 5 && delta < 355);
3384
3411
  }
3385
- entity.size = {
3386
- x: entity.maxs.x - entity.mins.x,
3387
- y: entity.maxs.y - entity.mins.y,
3388
- z: entity.maxs.z - entity.mins.z
3412
+ return !(delta > 45 && delta < 315);
3413
+ }
3414
+ function ai_move(self, distance2) {
3415
+ walkMove(self, self.angles.y, distance2);
3416
+ }
3417
+ function setIdealYawTowards(self, target) {
3418
+ if (!target) return;
3419
+ const toTarget = {
3420
+ x: target.origin.x - self.origin.x,
3421
+ y: target.origin.y - self.origin.y,
3422
+ z: target.origin.z - self.origin.z
3389
3423
  };
3424
+ self.ideal_yaw = vectorToYaw(toTarget);
3390
3425
  }
3391
- function parseQuoted(text, start) {
3392
- let index = start;
3393
- let result = "";
3394
- while (index < text.length) {
3395
- const char = text[index];
3396
- if (char === '"') {
3397
- return { value: result, nextIndex: index + 1 };
3398
- }
3399
- result += char;
3400
- index += 1;
3401
- }
3402
- throw new Error("Unterminated quoted string in entity lump");
3426
+ function ai_stand(self, deltaSeconds) {
3427
+ changeYaw(self, deltaSeconds);
3403
3428
  }
3404
- function consumeWhitespace(text, start) {
3405
- let index = start;
3406
- while (index < text.length && /\s/.test(text[index] ?? "")) {
3407
- index += 1;
3429
+ function ai_walk(self, distance2, deltaSeconds) {
3430
+ setIdealYawTowards(self, self.goalentity);
3431
+ changeYaw(self, deltaSeconds);
3432
+ if (distance2 !== 0) {
3433
+ walkMove(self, self.angles.y, distance2);
3408
3434
  }
3409
- return index;
3410
3435
  }
3411
- function parseToken(text, start) {
3412
- const index = consumeWhitespace(text, start);
3413
- if (index >= text.length) {
3414
- return { token: null, nextIndex: index };
3415
- }
3416
- const current = text[index];
3417
- if (current === "{" || current === "}") {
3418
- return { token: current, nextIndex: index + 1 };
3419
- }
3420
- if (current !== '"') {
3421
- throw new Error(`Unexpected token in entity lump: ${current}`);
3436
+ function ai_turn(self, distance2, deltaSeconds) {
3437
+ if (distance2 !== 0) {
3438
+ walkMove(self, self.angles.y, distance2);
3422
3439
  }
3423
- const quoted = parseQuoted(text, index + 1);
3424
- return { token: quoted.value, nextIndex: quoted.nextIndex };
3440
+ changeYaw(self, deltaSeconds);
3425
3441
  }
3426
- function parseEntityLump(text) {
3427
- const entities = [];
3428
- let index = 0;
3429
- while (index < text.length) {
3430
- const open = parseToken(text, index);
3431
- index = open.nextIndex;
3432
- if (open.token === null) {
3433
- break;
3434
- }
3435
- if (open.token !== "{") {
3436
- throw new Error("Expected { at start of entity definition");
3437
- }
3438
- const entity = {};
3439
- while (true) {
3440
- const keyToken = parseToken(text, index);
3441
- index = keyToken.nextIndex;
3442
- if (keyToken.token === null) {
3443
- throw new Error("EOF reached while parsing entity");
3444
- }
3445
- if (keyToken.token === "}") {
3446
- break;
3447
- }
3448
- const valueToken = parseToken(text, index);
3449
- index = valueToken.nextIndex;
3450
- if (valueToken.token === null || valueToken.token === "{" || valueToken.token === "}") {
3451
- throw new Error("Malformed entity key/value pair");
3452
- }
3453
- if (!keyToken.token.startsWith("_")) {
3454
- entity[keyToken.token] = valueToken.token;
3455
- }
3456
- }
3457
- entities.push(entity);
3442
+ function ai_run(self, distance2, deltaSeconds) {
3443
+ setIdealYawTowards(self, self.enemy ?? self.goalentity);
3444
+ changeYaw(self, deltaSeconds);
3445
+ if (distance2 !== 0) {
3446
+ walkMove(self, self.angles.y, distance2);
3458
3447
  }
3459
- return entities;
3460
3448
  }
3461
- var SpawnRegistry = class {
3462
- constructor() {
3463
- this.registry = /* @__PURE__ */ new Map();
3464
- }
3465
- register(classname, spawn) {
3466
- this.registry.set(classname, spawn);
3449
+ function ai_face(self, enemy, distance2, deltaSeconds) {
3450
+ if (enemy) {
3451
+ setIdealYawTowards(self, enemy);
3467
3452
  }
3468
- get(classname) {
3469
- return this.registry.get(classname);
3453
+ changeYaw(self, deltaSeconds);
3454
+ if (distance2 !== 0) {
3455
+ walkMove(self, self.angles.y, distance2);
3470
3456
  }
3471
- };
3472
- function defaultWarn(message) {
3473
- void message;
3474
3457
  }
3475
- function spawnEntityFromDictionary(dictionary, options) {
3476
- const warn = options.onWarning ?? defaultWarn;
3477
- const classname = dictionary.classname;
3478
- if (!classname) {
3479
- warn("Encountered entity with no classname");
3480
- return null;
3458
+ function ai_charge(self, distance2, deltaSeconds) {
3459
+ setIdealYawTowards(self, self.enemy);
3460
+ changeYaw(self, deltaSeconds);
3461
+ if (distance2 !== 0) {
3462
+ walkMove(self, self.angles.y, distance2);
3481
3463
  }
3482
- const isWorld = classname === "worldspawn";
3483
- const entity = isWorld ? options.entities.world : options.entities.spawn();
3484
- applyEntityKeyValues(entity, dictionary);
3485
- const context = {
3486
- keyValues: dictionary,
3487
- entities: options.entities,
3488
- warn,
3489
- free(target) {
3490
- options.entities.freeImmediate(target);
3464
+ }
3465
+
3466
+ // src/ai/perception.ts
3467
+ var RangeCategory = /* @__PURE__ */ ((RangeCategory2) => {
3468
+ RangeCategory2["Melee"] = "melee";
3469
+ RangeCategory2["Near"] = "near";
3470
+ RangeCategory2["Mid"] = "mid";
3471
+ RangeCategory2["Far"] = "far";
3472
+ return RangeCategory2;
3473
+ })(RangeCategory || {});
3474
+ function absBounds(entity) {
3475
+ return {
3476
+ mins: {
3477
+ x: entity.origin.x + entity.mins.x,
3478
+ y: entity.origin.y + entity.mins.y,
3479
+ z: entity.origin.z + entity.mins.z
3480
+ },
3481
+ maxs: {
3482
+ x: entity.origin.x + entity.maxs.x,
3483
+ y: entity.origin.y + entity.maxs.y,
3484
+ z: entity.origin.z + entity.maxs.z
3491
3485
  }
3492
3486
  };
3493
- const spawnFunc = options.registry.get(classname);
3494
- if (!spawnFunc) {
3495
- warn(`${classname} does not have a spawn function`);
3496
- if (!isWorld) {
3497
- options.entities.freeImmediate(entity);
3498
- }
3499
- return null;
3487
+ }
3488
+ function rangeTo(self, other) {
3489
+ const a = absBounds(self);
3490
+ const b = absBounds(other);
3491
+ const distanceSquared = distanceBetweenBoxesSquared(a.mins, a.maxs, b.mins, b.maxs);
3492
+ return Math.sqrt(distanceSquared);
3493
+ }
3494
+ function classifyRange(distance2) {
3495
+ if (distance2 <= RANGE_MELEE) {
3496
+ return "melee" /* Melee */;
3500
3497
  }
3501
- spawnFunc(entity, context);
3502
- if (!entity.inUse) {
3503
- return null;
3498
+ if (distance2 <= RANGE_NEAR) {
3499
+ return "near" /* Near */;
3504
3500
  }
3505
- options.entities.finalizeSpawn(entity);
3506
- return entity;
3501
+ if (distance2 <= RANGE_MID) {
3502
+ return "mid" /* Mid */;
3503
+ }
3504
+ return "far" /* Far */;
3507
3505
  }
3508
- function spawnEntitiesFromText(text, options) {
3509
- const parsed = parseEntityLump(text);
3510
- const spawned = [];
3511
- for (const dictionary of parsed) {
3512
- const entity = spawnEntityFromDictionary(dictionary, options);
3513
- if (entity) {
3514
- spawned.push(entity);
3515
- }
3506
+ function infront(self, other) {
3507
+ const { forward } = angleVectors(self.angles);
3508
+ const direction = normalizeVec3(subtractVec3(other.origin, self.origin));
3509
+ const dot = dotVec3(direction, forward);
3510
+ if ((self.spawnflags & SPAWNFLAG_MONSTER_AMBUSH) !== 0 && self.trail_time === 0 && self.enemy === null) {
3511
+ return dot > 0.15;
3516
3512
  }
3517
- return spawned;
3513
+ return dot > -0.3;
3518
3514
  }
3519
- function findPlayerStart(entities) {
3520
- return entities.find(
3521
- (entity) => entity.classname === "info_player_start"
3522
- );
3515
+ function visible(self, other, trace, options) {
3516
+ if ((other.flags & FL_NOVISIBLE) !== 0) {
3517
+ return false;
3518
+ }
3519
+ const start = { x: self.origin.x, y: self.origin.y, z: self.origin.z + self.viewheight };
3520
+ const end = { x: other.origin.x, y: other.origin.y, z: other.origin.z + other.viewheight };
3521
+ const mask = options?.throughGlass ? 1 /* Opaque */ : 1 /* Opaque */ | 2 /* Window */;
3522
+ const result = trace(start, end, self, mask);
3523
+ return result.fraction === 1 || result.entity === other;
3523
3524
  }
3524
- function registerDefaultSpawns(game, registry) {
3525
- registry.register("worldspawn", (entity) => {
3526
- entity.movetype = 2 /* Push */;
3527
- entity.solid = 3 /* Bsp */;
3528
- entity.modelindex = entity.modelindex || 1;
3529
- });
3530
- registry.register("info_player_start", () => {
3531
- });
3532
- registry.register("info_player_deathmatch", () => {
3533
- });
3534
- registry.register("info_player_coop", () => {
3535
- });
3536
- registry.register("info_null", (entity, context) => {
3537
- context.free(entity);
3538
- });
3539
- registry.register("info_notnull", () => {
3540
- });
3541
- registry.register("info_teleport_destination", () => {
3542
- });
3543
- registerTriggerSpawns(registry);
3544
- registerTargetSpawns(registry);
3545
- registerMiscSpawns(registry);
3546
- registerItemSpawns(game, registry);
3547
- registerFuncSpawns(registry);
3548
- registerPathSpawns(registry);
3549
- registerLightSpawns(registry);
3525
+
3526
+ // src/ai/targeting.ts
3527
+ function setIdealYawTowards2(self, other) {
3528
+ const delta = {
3529
+ x: other.origin.x - self.origin.x,
3530
+ y: other.origin.y - self.origin.y,
3531
+ z: other.origin.z - self.origin.z
3532
+ };
3533
+ self.ideal_yaw = vectorToYaw(delta);
3550
3534
  }
3551
- function createDefaultSpawnRegistry(game) {
3552
- const registry = new SpawnRegistry();
3553
- registerDefaultSpawns(game, registry);
3554
- return registry;
3535
+ function faceYawInstantly(self) {
3536
+ self.angles.y = angleMod(self.ideal_yaw);
3555
3537
  }
3556
-
3557
- // src/entities/callbacks.ts
3558
- function createCallbackRegistry() {
3559
- return /* @__PURE__ */ new Map();
3538
+ function huntTarget(self, level) {
3539
+ if (!self.enemy) return;
3540
+ self.goalentity = self.enemy;
3541
+ setIdealYawTowards2(self, self.enemy);
3542
+ faceYawInstantly(self);
3543
+ if ((self.monsterinfo.aiflags & 1 /* StandGround */) !== 0) {
3544
+ self.monsterinfo.stand?.(self);
3545
+ } else {
3546
+ self.monsterinfo.run?.(self);
3547
+ self.attack_finished_time = level.timeSeconds + 1;
3548
+ }
3560
3549
  }
3561
- function registerCallback(registry, name, fn) {
3562
- if (registry.has(name)) {
3550
+ function foundTarget(self, level, options) {
3551
+ if (!self.enemy) return;
3552
+ if ((self.enemy.svflags & 8 /* Player */) !== 0) {
3553
+ level.sightEntity = self;
3554
+ level.sightEntityFrame = level.frameNumber;
3555
+ self.light_level = 128;
3556
+ }
3557
+ self.show_hostile = level.timeSeconds + 1;
3558
+ const lastSighting = self.monsterinfo.last_sighting;
3559
+ lastSighting.x = self.enemy.origin.x;
3560
+ lastSighting.y = self.enemy.origin.y;
3561
+ lastSighting.z = self.enemy.origin.z;
3562
+ self.trail_time = level.timeSeconds;
3563
+ self.monsterinfo.trail_time = level.timeSeconds;
3564
+ if (!self.combattarget) {
3565
+ huntTarget(self, level);
3563
3566
  return;
3564
3567
  }
3565
- registry.set(name, fn);
3568
+ const pickTarget = options?.pickTarget;
3569
+ const movetarget = pickTarget?.(self.combattarget) ?? self.enemy;
3570
+ self.goalentity = movetarget;
3571
+ self.movetarget = movetarget;
3572
+ self.combattarget = void 0;
3573
+ self.monsterinfo.aiflags |= 4096 /* CombatPoint */;
3574
+ if (self.movetarget) {
3575
+ self.movetarget.targetname = void 0;
3576
+ }
3577
+ self.monsterinfo.pausetime = 0;
3578
+ self.monsterinfo.run?.(self);
3566
3579
  }
3567
-
3568
- // src/loop.ts
3569
- var orderedStageNames = [
3570
- "prep",
3571
- "simulate",
3572
- "finish"
3573
- ];
3574
- var GameFrameLoop = class {
3575
- constructor(initialStages) {
3576
- this.timeMs = 0;
3577
- this.frame = 0;
3578
- this.stageHandlers = {
3579
- prep: [],
3580
- simulate: [],
3581
- finish: []
3582
- };
3583
- this.stageCounts = {
3584
- prep: 0,
3585
- simulate: 0,
3586
- finish: 0
3587
- };
3588
- this.stageCompactionNeeded = {
3589
- prep: false,
3590
- simulate: false,
3591
- finish: false
3592
- };
3593
- if (initialStages) {
3594
- for (const stageName of orderedStageNames) {
3595
- const handler = initialStages[stageName];
3596
- if (handler) {
3597
- this.addStage(stageName, handler);
3598
- }
3599
- }
3580
+ function classifyClientVisibility(self, other, level, trace) {
3581
+ const distance2 = rangeTo(self, other);
3582
+ const range = classifyRange(distance2);
3583
+ if (range === "far" /* Far */) return false;
3584
+ if (other.light_level <= 5) return false;
3585
+ if (!visible(self, other, trace, { throughGlass: false })) return false;
3586
+ if (range === "near" /* Near */) {
3587
+ return level.timeSeconds <= other.show_hostile || infront(self, other);
3588
+ }
3589
+ if (range === "mid" /* Mid */) {
3590
+ return infront(self, other);
3591
+ }
3592
+ return true;
3593
+ }
3594
+ function updateSoundChase(self, client, level, hearability, trace) {
3595
+ if ((self.spawnflags & SPAWNFLAG_MONSTER_AMBUSH) !== 0) {
3596
+ if (!visible(self, client, trace)) return false;
3597
+ } else if (hearability.canHear && !hearability.canHear(self, client)) {
3598
+ return false;
3599
+ }
3600
+ const delta = subtractVec3(client.origin, self.origin);
3601
+ if (lengthVec3(delta) > 1e3) return false;
3602
+ if (hearability.areasConnected && !hearability.areasConnected(self, client)) return false;
3603
+ self.ideal_yaw = vectorToYaw(delta);
3604
+ faceYawInstantly(self);
3605
+ self.monsterinfo.aiflags |= 4 /* SoundTarget */;
3606
+ self.enemy = client;
3607
+ return true;
3608
+ }
3609
+ function chooseCandidate(self, level) {
3610
+ if (level.sightEntity && level.sightEntityFrame >= level.frameNumber - 1 && (self.spawnflags & SPAWNFLAG_MONSTER_AMBUSH) === 0) {
3611
+ if (level.sightEntity.enemy !== self.enemy) {
3612
+ return { candidate: level.sightEntity, heardit: false };
3600
3613
  }
3614
+ return { candidate: null, heardit: false };
3601
3615
  }
3602
- addStage(stage, handler) {
3603
- const handlers = this.stageHandlers[stage];
3604
- handlers.push(handler);
3605
- this.stageCounts[stage] += 1;
3606
- return () => {
3607
- const index = handlers.indexOf(handler);
3608
- if (index >= 0 && handlers[index]) {
3609
- handlers[index] = void 0;
3610
- this.stageCounts[stage] -= 1;
3611
- this.stageCompactionNeeded[stage] = true;
3612
- }
3613
- };
3616
+ if (level.soundEntity && level.soundEntityFrame >= level.frameNumber - 1) {
3617
+ return { candidate: level.soundEntity, heardit: true };
3614
3618
  }
3615
- reset(startTimeMs) {
3616
- this.timeMs = startTimeMs;
3617
- this.frame = 0;
3619
+ if (!self.enemy && level.sound2Entity && level.sound2EntityFrame >= level.frameNumber - 1 && (self.spawnflags & SPAWNFLAG_MONSTER_AMBUSH) === 0) {
3620
+ return { candidate: level.sound2Entity, heardit: true };
3618
3621
  }
3619
- advance(step) {
3620
- const previousTimeMs = this.timeMs;
3621
- this.timeMs = previousTimeMs + step.deltaMs;
3622
- this.frame = step.frame;
3623
- const context = {
3624
- ...step,
3625
- timeMs: this.timeMs,
3626
- previousTimeMs,
3627
- deltaSeconds: step.deltaMs / 1e3
3628
- };
3629
- this.runStage("prep", context);
3630
- if (this.stageCounts.simulate === 0) {
3631
- throw new Error("GameFrameLoop requires at least one simulate stage");
3632
- }
3633
- this.runStage("simulate", context);
3634
- this.runStage("finish", context);
3635
- return context;
3622
+ if (level.sightClient) {
3623
+ return { candidate: level.sightClient, heardit: false };
3636
3624
  }
3637
- runStage(stage, context) {
3638
- const handlers = this.stageHandlers[stage];
3639
- for (let i = 0; i < handlers.length; i += 1) {
3640
- const handler = handlers[i];
3641
- if (!handler) {
3642
- continue;
3643
- }
3644
- handler(context);
3645
- }
3646
- if (this.stageCompactionNeeded[stage]) {
3647
- this.compactStageHandlers(stage);
3648
- }
3625
+ return { candidate: null, heardit: false };
3626
+ }
3627
+ function rejectNotargetEntity(client) {
3628
+ if ((client.flags & FL_NOTARGET) !== 0) return true;
3629
+ if ((client.svflags & 4 /* Monster */) !== 0 && client.enemy) {
3630
+ return (client.enemy.flags & FL_NOTARGET) !== 0;
3649
3631
  }
3650
- compactStageHandlers(stage) {
3651
- const handlers = this.stageHandlers[stage];
3652
- let writeIndex = 0;
3653
- for (let readIndex = 0; readIndex < handlers.length; readIndex += 1) {
3654
- const handler = handlers[readIndex];
3655
- if (handler) {
3656
- handlers[writeIndex] = handler;
3657
- writeIndex += 1;
3658
- }
3632
+ if (client.enemy && (client.enemy.flags & FL_NOTARGET) !== 0) return true;
3633
+ return false;
3634
+ }
3635
+ function findTarget(self, level, trace, hearability = {}) {
3636
+ if ((self.monsterinfo.aiflags & 256 /* GoodGuy */) !== 0) {
3637
+ if (self.goalentity?.classname === "target_actor") {
3638
+ return false;
3659
3639
  }
3660
- handlers.length = writeIndex;
3661
- this.stageCompactionNeeded[stage] = false;
3640
+ return false;
3662
3641
  }
3663
- get time() {
3664
- return this.timeMs;
3642
+ if ((self.monsterinfo.aiflags & 4096 /* CombatPoint */) !== 0) {
3643
+ return false;
3665
3644
  }
3666
- get frameNumber() {
3667
- return this.frame;
3645
+ const { candidate, heardit } = chooseCandidate(self, level);
3646
+ if (!candidate || !candidate.inUse) return false;
3647
+ if (candidate === self.enemy) return true;
3648
+ if (rejectNotargetEntity(candidate)) return false;
3649
+ if (!heardit) {
3650
+ if (!classifyClientVisibility(self, candidate, level, trace)) return false;
3651
+ self.monsterinfo.aiflags &= ~4 /* SoundTarget */;
3652
+ self.enemy = candidate;
3653
+ } else if (!updateSoundChase(self, candidate, level, hearability, trace)) {
3654
+ return false;
3668
3655
  }
3669
- };
3656
+ foundTarget(self, level);
3657
+ if ((self.monsterinfo.aiflags & 4 /* SoundTarget */) === 0) {
3658
+ self.monsterinfo.sight?.(self, self.enemy);
3659
+ }
3660
+ return true;
3661
+ }
3670
3662
 
3671
- // src/level.ts
3672
- var ZERO_STATE = {
3673
- frameNumber: 0,
3674
- timeSeconds: 0,
3675
- previousTimeSeconds: 0,
3676
- deltaSeconds: 0
3677
- };
3678
- var LevelClock = class {
3679
- constructor() {
3680
- this.state = ZERO_STATE;
3663
+ // src/ai/monster.ts
3664
+ function M_MoveFrame(self) {
3665
+ const move = self.monsterinfo.current_move;
3666
+ if (!move) {
3667
+ return;
3681
3668
  }
3682
- start(startTimeMs) {
3683
- const startSeconds = startTimeMs / 1e3;
3684
- this.state = {
3685
- frameNumber: 0,
3686
- timeSeconds: startSeconds,
3687
- previousTimeSeconds: startSeconds,
3688
- deltaSeconds: 0
3689
- };
3669
+ if (self.frame < move.firstframe || self.frame > move.lastframe) {
3670
+ self.monsterinfo.aiflags &= ~128 /* HoldFrame */;
3671
+ self.frame = move.firstframe;
3690
3672
  }
3691
- tick(context) {
3692
- this.state = {
3693
- frameNumber: context.frame,
3694
- timeSeconds: context.timeMs / 1e3,
3695
- previousTimeSeconds: context.previousTimeMs / 1e3,
3696
- deltaSeconds: context.deltaSeconds
3697
- };
3698
- return this.state;
3673
+ if ((self.monsterinfo.aiflags & 128 /* HoldFrame */) !== 0) {
3674
+ return;
3699
3675
  }
3700
- get current() {
3701
- return this.state;
3676
+ const index = self.frame - move.firstframe;
3677
+ const frame = move.frames[index];
3678
+ if (frame.ai) {
3679
+ frame.ai(self, frame.dist);
3702
3680
  }
3703
- restore(state) {
3704
- this.state = { ...state };
3681
+ if (frame.think) {
3682
+ frame.think(self);
3705
3683
  }
3706
- };
3707
-
3708
- // src/ai/constants.ts
3709
- var RANGE_MELEE = 20;
3710
- var RANGE_NEAR = 440;
3711
- var RANGE_MID = 940;
3712
- var FL_NOTARGET = 1 << 5;
3713
- var FL_NOVISIBLE = 1 << 24;
3714
- var SPAWNFLAG_MONSTER_AMBUSH = 1 << 0;
3715
- var AIFlags = /* @__PURE__ */ ((AIFlags2) => {
3716
- AIFlags2[AIFlags2["StandGround"] = 1] = "StandGround";
3717
- AIFlags2[AIFlags2["TempStandGround"] = 2] = "TempStandGround";
3718
- AIFlags2[AIFlags2["SoundTarget"] = 4] = "SoundTarget";
3719
- AIFlags2[AIFlags2["LostSight"] = 8] = "LostSight";
3720
- AIFlags2[AIFlags2["PursuitLastSeen"] = 16] = "PursuitLastSeen";
3721
- AIFlags2[AIFlags2["PursueNext"] = 32] = "PursueNext";
3722
- AIFlags2[AIFlags2["PursueTemp"] = 64] = "PursueTemp";
3723
- AIFlags2[AIFlags2["HoldFrame"] = 128] = "HoldFrame";
3724
- AIFlags2[AIFlags2["GoodGuy"] = 256] = "GoodGuy";
3725
- AIFlags2[AIFlags2["Brutal"] = 512] = "Brutal";
3726
- AIFlags2[AIFlags2["NoStep"] = 1024] = "NoStep";
3727
- AIFlags2[AIFlags2["Ducked"] = 2048] = "Ducked";
3728
- AIFlags2[AIFlags2["CombatPoint"] = 4096] = "CombatPoint";
3729
- AIFlags2[AIFlags2["Medic"] = 8192] = "Medic";
3730
- AIFlags2[AIFlags2["Resurrecting"] = 16384] = "Resurrecting";
3731
- AIFlags2[AIFlags2["Pathing"] = 1073741824] = "Pathing";
3732
- return AIFlags2;
3733
- })(AIFlags || {});
3734
- var TraceMask = /* @__PURE__ */ ((TraceMask2) => {
3735
- TraceMask2[TraceMask2["Opaque"] = 1] = "Opaque";
3736
- TraceMask2[TraceMask2["Window"] = 2] = "Window";
3737
- return TraceMask2;
3738
- })(TraceMask || {});
3684
+ if (!self.inUse) {
3685
+ return;
3686
+ }
3687
+ self.frame++;
3688
+ if (self.frame > move.lastframe) {
3689
+ if (move.endfunc) {
3690
+ move.endfunc(self);
3691
+ if (self.monsterinfo.current_move !== move) {
3692
+ return;
3693
+ }
3694
+ }
3695
+ }
3696
+ }
3697
+ function monster_think(self, context) {
3698
+ M_MoveFrame(self);
3699
+ const time = context && typeof context.timeSeconds === "number" ? context.timeSeconds : self.nextthink;
3700
+ self.nextthink = time + 0.1;
3701
+ }
3739
3702
 
3740
- // src/ai/movement.ts
3741
- function yawVector(yawDegrees, distance2) {
3742
- if (distance2 === 0) {
3743
- return { x: 0, y: 0, z: 0 };
3703
+ // src/entities/monsters/soldier.ts
3704
+ var MONSTER_TICK = 0.1;
3705
+ function monster_ai_stand(self, dist) {
3706
+ ai_stand(self, MONSTER_TICK);
3707
+ }
3708
+ function monster_ai_walk(self, dist) {
3709
+ ai_walk(self, dist, MONSTER_TICK);
3710
+ }
3711
+ function monster_ai_run(self, dist) {
3712
+ ai_run(self, dist, MONSTER_TICK);
3713
+ }
3714
+ function monster_ai_charge(self, dist) {
3715
+ ai_charge(self, dist, MONSTER_TICK);
3716
+ }
3717
+ var stand_move;
3718
+ var walk_move;
3719
+ var run_move;
3720
+ var attack_move;
3721
+ function soldier_stand(self) {
3722
+ self.monsterinfo.current_move = stand_move;
3723
+ }
3724
+ function soldier_walk(self) {
3725
+ self.monsterinfo.current_move = walk_move;
3726
+ }
3727
+ function soldier_run(self) {
3728
+ if (self.enemy && self.enemy.health > 0) {
3729
+ self.monsterinfo.current_move = run_move;
3730
+ } else {
3731
+ self.monsterinfo.current_move = stand_move;
3744
3732
  }
3745
- const radians = degToRad(yawDegrees);
3746
- return {
3747
- x: Math.cos(radians) * distance2,
3748
- y: Math.sin(radians) * distance2,
3749
- z: 0
3733
+ }
3734
+ function soldier_attack(self) {
3735
+ self.monsterinfo.current_move = attack_move;
3736
+ }
3737
+ function soldier_fire(self) {
3738
+ }
3739
+ var stand_frames = Array.from({ length: 30 }, () => ({
3740
+ ai: monster_ai_stand,
3741
+ dist: 0
3742
+ }));
3743
+ stand_move = {
3744
+ firstframe: 0,
3745
+ lastframe: 29,
3746
+ frames: stand_frames,
3747
+ endfunc: soldier_stand
3748
+ };
3749
+ var walk_frames = Array.from({ length: 40 }, () => ({
3750
+ ai: monster_ai_walk,
3751
+ dist: 2
3752
+ }));
3753
+ walk_move = {
3754
+ firstframe: 30,
3755
+ lastframe: 69,
3756
+ frames: walk_frames,
3757
+ endfunc: soldier_walk
3758
+ };
3759
+ var run_frames = Array.from({ length: 20 }, () => ({
3760
+ ai: monster_ai_run,
3761
+ dist: 10
3762
+ }));
3763
+ run_move = {
3764
+ firstframe: 70,
3765
+ lastframe: 89,
3766
+ frames: run_frames,
3767
+ endfunc: soldier_run
3768
+ };
3769
+ var attack_frames = Array.from({ length: 10 }, (_, i) => ({
3770
+ ai: monster_ai_charge,
3771
+ dist: 0,
3772
+ think: i === 5 ? soldier_fire : null
3773
+ }));
3774
+ attack_move = {
3775
+ firstframe: 90,
3776
+ lastframe: 99,
3777
+ frames: attack_frames,
3778
+ endfunc: soldier_run
3779
+ };
3780
+ function SP_monster_soldier(self, context) {
3781
+ self.model = "models/monsters/soldier/tris.md2";
3782
+ self.mins = { x: -16, y: -16, z: -24 };
3783
+ self.maxs = { x: 16, y: 16, z: 32 };
3784
+ self.movetype = 5 /* Step */;
3785
+ self.solid = 2 /* BoundingBox */;
3786
+ self.health = 20;
3787
+ self.max_health = 20;
3788
+ self.mass = 100;
3789
+ self.pain = (self2, other, kick, damage) => {
3750
3790
  };
3791
+ self.die = (self2, inflictor, attacker, damage, point) => {
3792
+ self2.deadflag = 2 /* Dead */;
3793
+ self2.solid = 0 /* Not */;
3794
+ };
3795
+ self.monsterinfo.stand = soldier_stand;
3796
+ self.monsterinfo.walk = soldier_walk;
3797
+ self.monsterinfo.run = soldier_run;
3798
+ self.monsterinfo.attack = soldier_attack;
3799
+ self.think = monster_think;
3800
+ soldier_stand(self);
3801
+ self.nextthink = self.timestamp + MONSTER_TICK;
3751
3802
  }
3752
- function walkMove(self, yawDegrees, distance2) {
3753
- const delta = yawVector(yawDegrees, distance2);
3754
- const origin = self.origin;
3755
- origin.x += delta.x;
3756
- origin.y += delta.y;
3757
- origin.z += delta.z;
3758
- return true;
3803
+ function registerMonsterSpawns(registry) {
3804
+ registry.register("monster_soldier", SP_monster_soldier);
3759
3805
  }
3760
- function changeYaw(self, deltaSeconds) {
3761
- const current = angleMod(self.angles.y);
3762
- const ideal = self.ideal_yaw;
3763
- if (current === ideal) {
3764
- self.angles.y = current;
3765
- return;
3766
- }
3767
- const speed = self.yaw_speed * deltaSeconds * 10;
3768
- let move = ideal - current;
3769
- if (ideal > current) {
3770
- if (move >= 180) move -= 360;
3771
- } else if (move <= -180) {
3772
- move += 360;
3773
- }
3774
- if (move > speed) move = speed;
3775
- else if (move < -speed) move = -speed;
3776
- self.angles.y = angleMod(current + move);
3806
+
3807
+ // src/entities/spawn.ts
3808
+ var FIELD_LOOKUP = new Map(
3809
+ ENTITY_FIELD_METADATA.map((field) => [field.name, field])
3810
+ );
3811
+ function parseVec3(text) {
3812
+ const parts = text.trim().split(/\s+/);
3813
+ const [x = 0, y = 0, z = 0] = parts.map((part) => Number.parseFloat(part)).map((value) => Number.isNaN(value) ? 0 : value);
3814
+ return { x, y, z };
3777
3815
  }
3778
- function facingIdeal(self) {
3779
- const delta = angleMod(self.angles.y - self.ideal_yaw);
3780
- const hasPathing = (self.monsterinfo.aiflags & 1073741824 /* Pathing */) !== 0;
3781
- if (hasPathing) {
3782
- return !(delta > 5 && delta < 355);
3816
+ function parseBoolean(text) {
3817
+ const normalized = text.trim().toLowerCase();
3818
+ return normalized === "1" || normalized === "true" || normalized === "yes";
3819
+ }
3820
+ function parseValue(type, value) {
3821
+ switch (type) {
3822
+ case "int":
3823
+ return Number.parseInt(value, 10) || 0;
3824
+ case "float":
3825
+ return Number.parseFloat(value) || 0;
3826
+ case "boolean":
3827
+ return parseBoolean(value);
3828
+ case "vec3":
3829
+ return parseVec3(value);
3830
+ case "string":
3831
+ return value;
3832
+ case "entity":
3833
+ case "callback":
3834
+ return void 0;
3835
+ default:
3836
+ return value;
3783
3837
  }
3784
- return !(delta > 45 && delta < 315);
3785
- }
3786
- function ai_move(self, distance2) {
3787
- walkMove(self, self.angles.y, distance2);
3788
3838
  }
3789
- function setIdealYawTowards(self, target) {
3790
- if (!target) return;
3791
- const toTarget = {
3792
- x: target.origin.x - self.origin.x,
3793
- y: target.origin.y - self.origin.y,
3794
- z: target.origin.z - self.origin.z
3839
+ function applyEntityKeyValues(entity, values) {
3840
+ if ("angle" in values && !("angles" in values)) {
3841
+ entity.angles = { x: 0, y: Number.parseFloat(values.angle) || 0, z: 0 };
3842
+ }
3843
+ for (const [key, rawValue] of Object.entries(values)) {
3844
+ if (key.startsWith("_")) {
3845
+ continue;
3846
+ }
3847
+ const descriptor = FIELD_LOOKUP.get(key);
3848
+ if (!descriptor) {
3849
+ continue;
3850
+ }
3851
+ const parsed = parseValue(descriptor.type, rawValue);
3852
+ if (parsed !== void 0) {
3853
+ entity[descriptor.name] = parsed;
3854
+ }
3855
+ }
3856
+ entity.size = {
3857
+ x: entity.maxs.x - entity.mins.x,
3858
+ y: entity.maxs.y - entity.mins.y,
3859
+ z: entity.maxs.z - entity.mins.z
3795
3860
  };
3796
- self.ideal_yaw = vectorToYaw(toTarget);
3797
3861
  }
3798
- function ai_stand(self, deltaSeconds) {
3799
- changeYaw(self, deltaSeconds);
3862
+ function parseQuoted(text, start) {
3863
+ let index = start;
3864
+ let result = "";
3865
+ while (index < text.length) {
3866
+ const char = text[index];
3867
+ if (char === '"') {
3868
+ return { value: result, nextIndex: index + 1 };
3869
+ }
3870
+ result += char;
3871
+ index += 1;
3872
+ }
3873
+ throw new Error("Unterminated quoted string in entity lump");
3800
3874
  }
3801
- function ai_walk(self, distance2, deltaSeconds) {
3802
- setIdealYawTowards(self, self.goalentity);
3803
- changeYaw(self, deltaSeconds);
3804
- if (distance2 !== 0) {
3805
- walkMove(self, self.angles.y, distance2);
3875
+ function consumeWhitespace(text, start) {
3876
+ let index = start;
3877
+ while (index < text.length && /\s/.test(text[index] ?? "")) {
3878
+ index += 1;
3806
3879
  }
3880
+ return index;
3807
3881
  }
3808
- function ai_turn(self, distance2, deltaSeconds) {
3809
- if (distance2 !== 0) {
3810
- walkMove(self, self.angles.y, distance2);
3882
+ function parseToken(text, start) {
3883
+ const index = consumeWhitespace(text, start);
3884
+ if (index >= text.length) {
3885
+ return { token: null, nextIndex: index };
3811
3886
  }
3812
- changeYaw(self, deltaSeconds);
3887
+ const current = text[index];
3888
+ if (current === "{" || current === "}") {
3889
+ return { token: current, nextIndex: index + 1 };
3890
+ }
3891
+ if (current !== '"') {
3892
+ throw new Error(`Unexpected token in entity lump: ${current}`);
3893
+ }
3894
+ const quoted = parseQuoted(text, index + 1);
3895
+ return { token: quoted.value, nextIndex: quoted.nextIndex };
3813
3896
  }
3814
- function ai_run(self, distance2, deltaSeconds) {
3815
- setIdealYawTowards(self, self.enemy ?? self.goalentity);
3816
- changeYaw(self, deltaSeconds);
3817
- if (distance2 !== 0) {
3818
- walkMove(self, self.angles.y, distance2);
3897
+ function parseEntityLump(text) {
3898
+ const entities = [];
3899
+ let index = 0;
3900
+ while (index < text.length) {
3901
+ const open = parseToken(text, index);
3902
+ index = open.nextIndex;
3903
+ if (open.token === null) {
3904
+ break;
3905
+ }
3906
+ if (open.token !== "{") {
3907
+ throw new Error("Expected { at start of entity definition");
3908
+ }
3909
+ const entity = {};
3910
+ while (true) {
3911
+ const keyToken = parseToken(text, index);
3912
+ index = keyToken.nextIndex;
3913
+ if (keyToken.token === null) {
3914
+ throw new Error("EOF reached while parsing entity");
3915
+ }
3916
+ if (keyToken.token === "}") {
3917
+ break;
3918
+ }
3919
+ const valueToken = parseToken(text, index);
3920
+ index = valueToken.nextIndex;
3921
+ if (valueToken.token === null || valueToken.token === "{" || valueToken.token === "}") {
3922
+ throw new Error("Malformed entity key/value pair");
3923
+ }
3924
+ if (!keyToken.token.startsWith("_")) {
3925
+ entity[keyToken.token] = valueToken.token;
3926
+ }
3927
+ }
3928
+ entities.push(entity);
3819
3929
  }
3930
+ return entities;
3820
3931
  }
3821
- function ai_face(self, enemy, distance2, deltaSeconds) {
3822
- if (enemy) {
3823
- setIdealYawTowards(self, enemy);
3932
+ var SpawnRegistry = class {
3933
+ constructor() {
3934
+ this.registry = /* @__PURE__ */ new Map();
3824
3935
  }
3825
- changeYaw(self, deltaSeconds);
3826
- if (distance2 !== 0) {
3827
- walkMove(self, self.angles.y, distance2);
3936
+ register(classname, spawn) {
3937
+ this.registry.set(classname, spawn);
3828
3938
  }
3829
- }
3830
- function ai_charge(self, distance2, deltaSeconds) {
3831
- setIdealYawTowards(self, self.enemy);
3832
- changeYaw(self, deltaSeconds);
3833
- if (distance2 !== 0) {
3834
- walkMove(self, self.angles.y, distance2);
3939
+ get(classname) {
3940
+ return this.registry.get(classname);
3835
3941
  }
3942
+ };
3943
+ function defaultWarn(message) {
3944
+ void message;
3836
3945
  }
3837
-
3838
- // src/ai/perception.ts
3839
- var RangeCategory = /* @__PURE__ */ ((RangeCategory2) => {
3840
- RangeCategory2["Melee"] = "melee";
3841
- RangeCategory2["Near"] = "near";
3842
- RangeCategory2["Mid"] = "mid";
3843
- RangeCategory2["Far"] = "far";
3844
- return RangeCategory2;
3845
- })(RangeCategory || {});
3846
- function absBounds(entity) {
3847
- return {
3848
- mins: {
3849
- x: entity.origin.x + entity.mins.x,
3850
- y: entity.origin.y + entity.mins.y,
3851
- z: entity.origin.z + entity.mins.z
3852
- },
3853
- maxs: {
3854
- x: entity.origin.x + entity.maxs.x,
3855
- y: entity.origin.y + entity.maxs.y,
3856
- z: entity.origin.z + entity.maxs.z
3946
+ function spawnEntityFromDictionary(dictionary, options) {
3947
+ const warn = options.onWarning ?? defaultWarn;
3948
+ const classname = dictionary.classname;
3949
+ if (!classname) {
3950
+ warn("Encountered entity with no classname");
3951
+ return null;
3952
+ }
3953
+ const isWorld = classname === "worldspawn";
3954
+ const entity = isWorld ? options.entities.world : options.entities.spawn();
3955
+ applyEntityKeyValues(entity, dictionary);
3956
+ const context = {
3957
+ keyValues: dictionary,
3958
+ entities: options.entities,
3959
+ warn,
3960
+ free(target) {
3961
+ options.entities.freeImmediate(target);
3857
3962
  }
3858
3963
  };
3859
- }
3860
- function rangeTo(self, other) {
3861
- const a = absBounds(self);
3862
- const b = absBounds(other);
3863
- const distanceSquared = distanceBetweenBoxesSquared(a.mins, a.maxs, b.mins, b.maxs);
3864
- return Math.sqrt(distanceSquared);
3865
- }
3866
- function classifyRange(distance2) {
3867
- if (distance2 <= RANGE_MELEE) {
3868
- return "melee" /* Melee */;
3869
- }
3870
- if (distance2 <= RANGE_NEAR) {
3871
- return "near" /* Near */;
3964
+ const spawnFunc = options.registry.get(classname);
3965
+ if (!spawnFunc) {
3966
+ warn(`${classname} does not have a spawn function`);
3967
+ if (!isWorld) {
3968
+ options.entities.freeImmediate(entity);
3969
+ }
3970
+ return null;
3872
3971
  }
3873
- if (distance2 <= RANGE_MID) {
3874
- return "mid" /* Mid */;
3972
+ spawnFunc(entity, context);
3973
+ if (!entity.inUse) {
3974
+ return null;
3875
3975
  }
3876
- return "far" /* Far */;
3976
+ options.entities.finalizeSpawn(entity);
3977
+ return entity;
3877
3978
  }
3878
- function infront(self, other) {
3879
- const { forward } = angleVectors(self.angles);
3880
- const direction = normalizeVec3(subtractVec3(other.origin, self.origin));
3881
- const dot = dotVec3(direction, forward);
3882
- if ((self.spawnflags & SPAWNFLAG_MONSTER_AMBUSH) !== 0 && self.trail_time === 0 && self.enemy === null) {
3883
- return dot > 0.15;
3979
+ function spawnEntitiesFromText(text, options) {
3980
+ const parsed = parseEntityLump(text);
3981
+ const spawned = [];
3982
+ for (const dictionary of parsed) {
3983
+ const entity = spawnEntityFromDictionary(dictionary, options);
3984
+ if (entity) {
3985
+ spawned.push(entity);
3986
+ }
3884
3987
  }
3885
- return dot > -0.3;
3988
+ return spawned;
3886
3989
  }
3887
- function visible(self, other, trace, options) {
3888
- if ((other.flags & FL_NOVISIBLE) !== 0) {
3889
- return false;
3890
- }
3891
- const start = { x: self.origin.x, y: self.origin.y, z: self.origin.z + self.viewheight };
3892
- const end = { x: other.origin.x, y: other.origin.y, z: other.origin.z + other.viewheight };
3893
- const mask = options?.throughGlass ? 1 /* Opaque */ : 1 /* Opaque */ | 2 /* Window */;
3894
- const result = trace(start, end, self, mask);
3895
- return result.fraction === 1 || result.entity === other;
3990
+ function findPlayerStart(entities) {
3991
+ return entities.find(
3992
+ (entity) => entity.classname === "info_player_start"
3993
+ );
3896
3994
  }
3897
-
3898
- // src/ai/targeting.ts
3899
- function setIdealYawTowards2(self, other) {
3900
- const delta = {
3901
- x: other.origin.x - self.origin.x,
3902
- y: other.origin.y - self.origin.y,
3903
- z: other.origin.z - self.origin.z
3904
- };
3905
- self.ideal_yaw = vectorToYaw(delta);
3995
+ function registerDefaultSpawns(game, registry) {
3996
+ registry.register("worldspawn", (entity) => {
3997
+ entity.movetype = 2 /* Push */;
3998
+ entity.solid = 3 /* Bsp */;
3999
+ entity.modelindex = entity.modelindex || 1;
4000
+ });
4001
+ registry.register("info_player_start", () => {
4002
+ });
4003
+ registry.register("info_player_deathmatch", () => {
4004
+ });
4005
+ registry.register("info_player_coop", () => {
4006
+ });
4007
+ registry.register("info_null", (entity, context) => {
4008
+ context.free(entity);
4009
+ });
4010
+ registry.register("info_notnull", () => {
4011
+ });
4012
+ registry.register("info_teleport_destination", () => {
4013
+ });
4014
+ registerTriggerSpawns(registry);
4015
+ registerTargetSpawns(registry);
4016
+ registerMiscSpawns(registry);
4017
+ registerItemSpawns(game, registry);
4018
+ registerFuncSpawns(registry);
4019
+ registerPathSpawns(registry);
4020
+ registerLightSpawns(registry);
4021
+ registerMonsterSpawns(registry);
3906
4022
  }
3907
- function faceYawInstantly(self) {
3908
- self.angles.y = angleMod(self.ideal_yaw);
4023
+ function createDefaultSpawnRegistry(game) {
4024
+ const registry = new SpawnRegistry();
4025
+ registerDefaultSpawns(game, registry);
4026
+ return registry;
3909
4027
  }
3910
- function huntTarget(self, level) {
3911
- if (!self.enemy) return;
3912
- self.goalentity = self.enemy;
3913
- setIdealYawTowards2(self, self.enemy);
3914
- faceYawInstantly(self);
3915
- if ((self.monsterinfo.aiflags & 1 /* StandGround */) !== 0) {
3916
- self.monsterinfo.stand?.(self);
3917
- } else {
3918
- self.monsterinfo.run?.(self);
3919
- self.attack_finished_time = level.timeSeconds + 1;
3920
- }
4028
+
4029
+ // src/entities/callbacks.ts
4030
+ function createCallbackRegistry() {
4031
+ return /* @__PURE__ */ new Map();
3921
4032
  }
3922
- function foundTarget(self, level, options) {
3923
- if (!self.enemy) return;
3924
- if ((self.enemy.svflags & 8 /* Player */) !== 0) {
3925
- level.sightEntity = self;
3926
- level.sightEntityFrame = level.frameNumber;
3927
- self.light_level = 128;
3928
- }
3929
- self.show_hostile = level.timeSeconds + 1;
3930
- const lastSighting = self.monsterinfo.last_sighting;
3931
- lastSighting.x = self.enemy.origin.x;
3932
- lastSighting.y = self.enemy.origin.y;
3933
- lastSighting.z = self.enemy.origin.z;
3934
- self.trail_time = level.timeSeconds;
3935
- self.monsterinfo.trail_time = level.timeSeconds;
3936
- if (!self.combattarget) {
3937
- huntTarget(self, level);
4033
+ function registerCallback(registry, name, fn) {
4034
+ if (registry.has(name)) {
3938
4035
  return;
3939
4036
  }
3940
- const pickTarget = options?.pickTarget;
3941
- const movetarget = pickTarget?.(self.combattarget) ?? self.enemy;
3942
- self.goalentity = movetarget;
3943
- self.movetarget = movetarget;
3944
- self.combattarget = void 0;
3945
- self.monsterinfo.aiflags |= 4096 /* CombatPoint */;
3946
- if (self.movetarget) {
3947
- self.movetarget.targetname = void 0;
3948
- }
3949
- self.monsterinfo.pausetime = 0;
3950
- self.monsterinfo.run?.(self);
4037
+ registry.set(name, fn);
3951
4038
  }
3952
- function classifyClientVisibility(self, other, level, trace) {
3953
- const distance2 = rangeTo(self, other);
3954
- const range = classifyRange(distance2);
3955
- if (range === "far" /* Far */) return false;
3956
- if (other.light_level <= 5) return false;
3957
- if (!visible(self, other, trace, { throughGlass: false })) return false;
3958
- if (range === "near" /* Near */) {
3959
- return level.timeSeconds <= other.show_hostile || infront(self, other);
4039
+
4040
+ // src/loop.ts
4041
+ var orderedStageNames = [
4042
+ "prep",
4043
+ "simulate",
4044
+ "finish"
4045
+ ];
4046
+ var GameFrameLoop = class {
4047
+ constructor(initialStages) {
4048
+ this.timeMs = 0;
4049
+ this.frame = 0;
4050
+ this.stageHandlers = {
4051
+ prep: [],
4052
+ simulate: [],
4053
+ finish: []
4054
+ };
4055
+ this.stageCounts = {
4056
+ prep: 0,
4057
+ simulate: 0,
4058
+ finish: 0
4059
+ };
4060
+ this.stageCompactionNeeded = {
4061
+ prep: false,
4062
+ simulate: false,
4063
+ finish: false
4064
+ };
4065
+ if (initialStages) {
4066
+ for (const stageName of orderedStageNames) {
4067
+ const handler = initialStages[stageName];
4068
+ if (handler) {
4069
+ this.addStage(stageName, handler);
4070
+ }
4071
+ }
4072
+ }
3960
4073
  }
3961
- if (range === "mid" /* Mid */) {
3962
- return infront(self, other);
4074
+ addStage(stage, handler) {
4075
+ const handlers = this.stageHandlers[stage];
4076
+ handlers.push(handler);
4077
+ this.stageCounts[stage] += 1;
4078
+ return () => {
4079
+ const index = handlers.indexOf(handler);
4080
+ if (index >= 0 && handlers[index]) {
4081
+ handlers[index] = void 0;
4082
+ this.stageCounts[stage] -= 1;
4083
+ this.stageCompactionNeeded[stage] = true;
4084
+ }
4085
+ };
3963
4086
  }
3964
- return true;
3965
- }
3966
- function updateSoundChase(self, client, level, hearability, trace) {
3967
- if ((self.spawnflags & SPAWNFLAG_MONSTER_AMBUSH) !== 0) {
3968
- if (!visible(self, client, trace)) return false;
3969
- } else if (hearability.canHear && !hearability.canHear(self, client)) {
3970
- return false;
4087
+ reset(startTimeMs) {
4088
+ this.timeMs = startTimeMs;
4089
+ this.frame = 0;
3971
4090
  }
3972
- const delta = subtractVec3(client.origin, self.origin);
3973
- if (lengthVec3(delta) > 1e3) return false;
3974
- if (hearability.areasConnected && !hearability.areasConnected(self, client)) return false;
3975
- self.ideal_yaw = vectorToYaw(delta);
3976
- faceYawInstantly(self);
3977
- self.monsterinfo.aiflags |= 4 /* SoundTarget */;
3978
- self.enemy = client;
3979
- return true;
3980
- }
3981
- function chooseCandidate(self, level) {
3982
- if (level.sightEntity && level.sightEntityFrame >= level.frameNumber - 1 && (self.spawnflags & SPAWNFLAG_MONSTER_AMBUSH) === 0) {
3983
- if (level.sightEntity.enemy !== self.enemy) {
3984
- return { candidate: level.sightEntity, heardit: false };
4091
+ advance(step) {
4092
+ const previousTimeMs = this.timeMs;
4093
+ this.timeMs = previousTimeMs + step.deltaMs;
4094
+ this.frame = step.frame;
4095
+ const context = {
4096
+ ...step,
4097
+ timeMs: this.timeMs,
4098
+ previousTimeMs,
4099
+ deltaSeconds: step.deltaMs / 1e3
4100
+ };
4101
+ this.runStage("prep", context);
4102
+ if (this.stageCounts.simulate === 0) {
4103
+ throw new Error("GameFrameLoop requires at least one simulate stage");
3985
4104
  }
3986
- return { candidate: null, heardit: false };
4105
+ this.runStage("simulate", context);
4106
+ this.runStage("finish", context);
4107
+ return context;
3987
4108
  }
3988
- if (level.soundEntity && level.soundEntityFrame >= level.frameNumber - 1) {
3989
- return { candidate: level.soundEntity, heardit: true };
4109
+ runStage(stage, context) {
4110
+ const handlers = this.stageHandlers[stage];
4111
+ for (let i = 0; i < handlers.length; i += 1) {
4112
+ const handler = handlers[i];
4113
+ if (!handler) {
4114
+ continue;
4115
+ }
4116
+ handler(context);
4117
+ }
4118
+ if (this.stageCompactionNeeded[stage]) {
4119
+ this.compactStageHandlers(stage);
4120
+ }
3990
4121
  }
3991
- if (!self.enemy && level.sound2Entity && level.sound2EntityFrame >= level.frameNumber - 1 && (self.spawnflags & SPAWNFLAG_MONSTER_AMBUSH) === 0) {
3992
- return { candidate: level.sound2Entity, heardit: true };
4122
+ compactStageHandlers(stage) {
4123
+ const handlers = this.stageHandlers[stage];
4124
+ let writeIndex = 0;
4125
+ for (let readIndex = 0; readIndex < handlers.length; readIndex += 1) {
4126
+ const handler = handlers[readIndex];
4127
+ if (handler) {
4128
+ handlers[writeIndex] = handler;
4129
+ writeIndex += 1;
4130
+ }
4131
+ }
4132
+ handlers.length = writeIndex;
4133
+ this.stageCompactionNeeded[stage] = false;
3993
4134
  }
3994
- if (level.sightClient) {
3995
- return { candidate: level.sightClient, heardit: false };
4135
+ get time() {
4136
+ return this.timeMs;
3996
4137
  }
3997
- return { candidate: null, heardit: false };
3998
- }
3999
- function rejectNotargetEntity(client) {
4000
- if ((client.flags & FL_NOTARGET) !== 0) return true;
4001
- if ((client.svflags & 4 /* Monster */) !== 0 && client.enemy) {
4002
- return (client.enemy.flags & FL_NOTARGET) !== 0;
4138
+ get frameNumber() {
4139
+ return this.frame;
4003
4140
  }
4004
- if (client.enemy && (client.enemy.flags & FL_NOTARGET) !== 0) return true;
4005
- return false;
4006
- }
4007
- function findTarget(self, level, trace, hearability = {}) {
4008
- if ((self.monsterinfo.aiflags & 256 /* GoodGuy */) !== 0) {
4009
- if (self.goalentity?.classname === "target_actor") {
4010
- return false;
4011
- }
4012
- return false;
4141
+ };
4142
+
4143
+ // src/level.ts
4144
+ var ZERO_STATE = {
4145
+ frameNumber: 0,
4146
+ timeSeconds: 0,
4147
+ previousTimeSeconds: 0,
4148
+ deltaSeconds: 0
4149
+ };
4150
+ var LevelClock = class {
4151
+ constructor() {
4152
+ this.state = ZERO_STATE;
4013
4153
  }
4014
- if ((self.monsterinfo.aiflags & 4096 /* CombatPoint */) !== 0) {
4015
- return false;
4154
+ start(startTimeMs) {
4155
+ const startSeconds = startTimeMs / 1e3;
4156
+ this.state = {
4157
+ frameNumber: 0,
4158
+ timeSeconds: startSeconds,
4159
+ previousTimeSeconds: startSeconds,
4160
+ deltaSeconds: 0
4161
+ };
4016
4162
  }
4017
- const { candidate, heardit } = chooseCandidate(self, level);
4018
- if (!candidate || !candidate.inUse) return false;
4019
- if (candidate === self.enemy) return true;
4020
- if (rejectNotargetEntity(candidate)) return false;
4021
- if (!heardit) {
4022
- if (!classifyClientVisibility(self, candidate, level, trace)) return false;
4023
- self.monsterinfo.aiflags &= ~4 /* SoundTarget */;
4024
- self.enemy = candidate;
4025
- } else if (!updateSoundChase(self, candidate, level, hearability, trace)) {
4026
- return false;
4163
+ tick(context) {
4164
+ this.state = {
4165
+ frameNumber: context.frame,
4166
+ timeSeconds: context.timeMs / 1e3,
4167
+ previousTimeSeconds: context.previousTimeMs / 1e3,
4168
+ deltaSeconds: context.deltaSeconds
4169
+ };
4170
+ return this.state;
4027
4171
  }
4028
- foundTarget(self, level);
4029
- if ((self.monsterinfo.aiflags & 4 /* SoundTarget */) === 0) {
4030
- self.monsterinfo.sight?.(self, self.enemy);
4172
+ get current() {
4173
+ return this.state;
4031
4174
  }
4032
- return true;
4033
- }
4175
+ restore(state) {
4176
+ this.state = { ...state };
4177
+ }
4178
+ };
4034
4179
 
4035
4180
  // src/checksum.ts
4036
4181
  var FNV_OFFSET_BASIS = 2166136261;
@@ -5410,6 +5555,7 @@ export {
5410
5555
  HEALTH_ITEMS,
5411
5556
  KEY_ITEMS,
5412
5557
  KeyId,
5558
+ M_MoveFrame,
5413
5559
  MoveType,
5414
5560
  ORDERED_DAMAGE_MODS,
5415
5561
  POWERUP_ITEMS,
@@ -5486,6 +5632,7 @@ export {
5486
5632
  infront,
5487
5633
  isZeroVector,
5488
5634
  killBox,
5635
+ monster_think,
5489
5636
  parseEntityLump,
5490
5637
  parseRereleaseSave,
5491
5638
  parseSaveFile,