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.
@@ -40,6 +40,7 @@ __export(index_exports, {
40
40
  HEALTH_ITEMS: () => HEALTH_ITEMS,
41
41
  KEY_ITEMS: () => KEY_ITEMS,
42
42
  KeyId: () => KeyId,
43
+ M_MoveFrame: () => M_MoveFrame,
43
44
  MoveType: () => MoveType,
44
45
  ORDERED_DAMAGE_MODS: () => ORDERED_DAMAGE_MODS,
45
46
  POWERUP_ITEMS: () => POWERUP_ITEMS,
@@ -116,6 +117,7 @@ __export(index_exports, {
116
117
  infront: () => infront,
117
118
  isZeroVector: () => isZeroVector,
118
119
  killBox: () => killBox,
120
+ monster_think: () => monster_think,
119
121
  parseEntityLump: () => parseEntityLump,
120
122
  parseRereleaseSave: () => parseRereleaseSave,
121
123
  parseSaveFile: () => parseSaveFile,
@@ -3474,704 +3476,849 @@ function registerLightSpawns(registry) {
3474
3476
  });
3475
3477
  }
3476
3478
 
3477
- // src/entities/spawn.ts
3478
- var FIELD_LOOKUP = new Map(
3479
- ENTITY_FIELD_METADATA.map((field) => [field.name, field])
3480
- );
3481
- function parseVec3(text) {
3482
- const parts = text.trim().split(/\s+/);
3483
- const [x = 0, y = 0, z = 0] = parts.map((part) => Number.parseFloat(part)).map((value) => Number.isNaN(value) ? 0 : value);
3484
- return { x, y, z };
3479
+ // src/ai/constants.ts
3480
+ var RANGE_MELEE = 20;
3481
+ var RANGE_NEAR = 440;
3482
+ var RANGE_MID = 940;
3483
+ var FL_NOTARGET = 1 << 5;
3484
+ var FL_NOVISIBLE = 1 << 24;
3485
+ var SPAWNFLAG_MONSTER_AMBUSH = 1 << 0;
3486
+ var AIFlags = /* @__PURE__ */ ((AIFlags2) => {
3487
+ AIFlags2[AIFlags2["StandGround"] = 1] = "StandGround";
3488
+ AIFlags2[AIFlags2["TempStandGround"] = 2] = "TempStandGround";
3489
+ AIFlags2[AIFlags2["SoundTarget"] = 4] = "SoundTarget";
3490
+ AIFlags2[AIFlags2["LostSight"] = 8] = "LostSight";
3491
+ AIFlags2[AIFlags2["PursuitLastSeen"] = 16] = "PursuitLastSeen";
3492
+ AIFlags2[AIFlags2["PursueNext"] = 32] = "PursueNext";
3493
+ AIFlags2[AIFlags2["PursueTemp"] = 64] = "PursueTemp";
3494
+ AIFlags2[AIFlags2["HoldFrame"] = 128] = "HoldFrame";
3495
+ AIFlags2[AIFlags2["GoodGuy"] = 256] = "GoodGuy";
3496
+ AIFlags2[AIFlags2["Brutal"] = 512] = "Brutal";
3497
+ AIFlags2[AIFlags2["NoStep"] = 1024] = "NoStep";
3498
+ AIFlags2[AIFlags2["Ducked"] = 2048] = "Ducked";
3499
+ AIFlags2[AIFlags2["CombatPoint"] = 4096] = "CombatPoint";
3500
+ AIFlags2[AIFlags2["Medic"] = 8192] = "Medic";
3501
+ AIFlags2[AIFlags2["Resurrecting"] = 16384] = "Resurrecting";
3502
+ AIFlags2[AIFlags2["Pathing"] = 1073741824] = "Pathing";
3503
+ return AIFlags2;
3504
+ })(AIFlags || {});
3505
+ var TraceMask = /* @__PURE__ */ ((TraceMask2) => {
3506
+ TraceMask2[TraceMask2["Opaque"] = 1] = "Opaque";
3507
+ TraceMask2[TraceMask2["Window"] = 2] = "Window";
3508
+ return TraceMask2;
3509
+ })(TraceMask || {});
3510
+
3511
+ // src/ai/movement.ts
3512
+ function yawVector(yawDegrees, distance2) {
3513
+ if (distance2 === 0) {
3514
+ return { x: 0, y: 0, z: 0 };
3515
+ }
3516
+ const radians = degToRad(yawDegrees);
3517
+ return {
3518
+ x: Math.cos(radians) * distance2,
3519
+ y: Math.sin(radians) * distance2,
3520
+ z: 0
3521
+ };
3485
3522
  }
3486
- function parseBoolean(text) {
3487
- const normalized = text.trim().toLowerCase();
3488
- return normalized === "1" || normalized === "true" || normalized === "yes";
3523
+ function walkMove(self, yawDegrees, distance2) {
3524
+ const delta = yawVector(yawDegrees, distance2);
3525
+ const origin = self.origin;
3526
+ origin.x += delta.x;
3527
+ origin.y += delta.y;
3528
+ origin.z += delta.z;
3529
+ return true;
3489
3530
  }
3490
- function parseValue(type, value) {
3491
- switch (type) {
3492
- case "int":
3493
- return Number.parseInt(value, 10) || 0;
3494
- case "float":
3495
- return Number.parseFloat(value) || 0;
3496
- case "boolean":
3497
- return parseBoolean(value);
3498
- case "vec3":
3499
- return parseVec3(value);
3500
- case "string":
3501
- return value;
3502
- case "entity":
3503
- case "callback":
3504
- return void 0;
3505
- default:
3506
- return value;
3531
+ function changeYaw(self, deltaSeconds) {
3532
+ const current = angleMod(self.angles.y);
3533
+ const ideal = self.ideal_yaw;
3534
+ if (current === ideal) {
3535
+ self.angles.y = current;
3536
+ return;
3507
3537
  }
3508
- }
3509
- function applyEntityKeyValues(entity, values) {
3510
- if ("angle" in values && !("angles" in values)) {
3511
- entity.angles = { x: 0, y: Number.parseFloat(values.angle) || 0, z: 0 };
3538
+ const speed = self.yaw_speed * deltaSeconds * 10;
3539
+ let move = ideal - current;
3540
+ if (ideal > current) {
3541
+ if (move >= 180) move -= 360;
3542
+ } else if (move <= -180) {
3543
+ move += 360;
3512
3544
  }
3513
- for (const [key, rawValue] of Object.entries(values)) {
3514
- if (key.startsWith("_")) {
3515
- continue;
3516
- }
3517
- const descriptor = FIELD_LOOKUP.get(key);
3518
- if (!descriptor) {
3519
- continue;
3520
- }
3521
- const parsed = parseValue(descriptor.type, rawValue);
3522
- if (parsed !== void 0) {
3523
- entity[descriptor.name] = parsed;
3524
- }
3545
+ if (move > speed) move = speed;
3546
+ else if (move < -speed) move = -speed;
3547
+ self.angles.y = angleMod(current + move);
3548
+ }
3549
+ function facingIdeal(self) {
3550
+ const delta = angleMod(self.angles.y - self.ideal_yaw);
3551
+ const hasPathing = (self.monsterinfo.aiflags & 1073741824 /* Pathing */) !== 0;
3552
+ if (hasPathing) {
3553
+ return !(delta > 5 && delta < 355);
3525
3554
  }
3526
- entity.size = {
3527
- x: entity.maxs.x - entity.mins.x,
3528
- y: entity.maxs.y - entity.mins.y,
3529
- z: entity.maxs.z - entity.mins.z
3555
+ return !(delta > 45 && delta < 315);
3556
+ }
3557
+ function ai_move(self, distance2) {
3558
+ walkMove(self, self.angles.y, distance2);
3559
+ }
3560
+ function setIdealYawTowards(self, target) {
3561
+ if (!target) return;
3562
+ const toTarget = {
3563
+ x: target.origin.x - self.origin.x,
3564
+ y: target.origin.y - self.origin.y,
3565
+ z: target.origin.z - self.origin.z
3530
3566
  };
3567
+ self.ideal_yaw = vectorToYaw(toTarget);
3531
3568
  }
3532
- function parseQuoted(text, start) {
3533
- let index = start;
3534
- let result = "";
3535
- while (index < text.length) {
3536
- const char = text[index];
3537
- if (char === '"') {
3538
- return { value: result, nextIndex: index + 1 };
3539
- }
3540
- result += char;
3541
- index += 1;
3542
- }
3543
- throw new Error("Unterminated quoted string in entity lump");
3569
+ function ai_stand(self, deltaSeconds) {
3570
+ changeYaw(self, deltaSeconds);
3544
3571
  }
3545
- function consumeWhitespace(text, start) {
3546
- let index = start;
3547
- while (index < text.length && /\s/.test(text[index] ?? "")) {
3548
- index += 1;
3572
+ function ai_walk(self, distance2, deltaSeconds) {
3573
+ setIdealYawTowards(self, self.goalentity);
3574
+ changeYaw(self, deltaSeconds);
3575
+ if (distance2 !== 0) {
3576
+ walkMove(self, self.angles.y, distance2);
3549
3577
  }
3550
- return index;
3551
3578
  }
3552
- function parseToken(text, start) {
3553
- const index = consumeWhitespace(text, start);
3554
- if (index >= text.length) {
3555
- return { token: null, nextIndex: index };
3556
- }
3557
- const current = text[index];
3558
- if (current === "{" || current === "}") {
3559
- return { token: current, nextIndex: index + 1 };
3560
- }
3561
- if (current !== '"') {
3562
- throw new Error(`Unexpected token in entity lump: ${current}`);
3579
+ function ai_turn(self, distance2, deltaSeconds) {
3580
+ if (distance2 !== 0) {
3581
+ walkMove(self, self.angles.y, distance2);
3563
3582
  }
3564
- const quoted = parseQuoted(text, index + 1);
3565
- return { token: quoted.value, nextIndex: quoted.nextIndex };
3583
+ changeYaw(self, deltaSeconds);
3566
3584
  }
3567
- function parseEntityLump(text) {
3568
- const entities = [];
3569
- let index = 0;
3570
- while (index < text.length) {
3571
- const open = parseToken(text, index);
3572
- index = open.nextIndex;
3573
- if (open.token === null) {
3574
- break;
3575
- }
3576
- if (open.token !== "{") {
3577
- throw new Error("Expected { at start of entity definition");
3578
- }
3579
- const entity = {};
3580
- while (true) {
3581
- const keyToken = parseToken(text, index);
3582
- index = keyToken.nextIndex;
3583
- if (keyToken.token === null) {
3584
- throw new Error("EOF reached while parsing entity");
3585
- }
3586
- if (keyToken.token === "}") {
3587
- break;
3588
- }
3589
- const valueToken = parseToken(text, index);
3590
- index = valueToken.nextIndex;
3591
- if (valueToken.token === null || valueToken.token === "{" || valueToken.token === "}") {
3592
- throw new Error("Malformed entity key/value pair");
3593
- }
3594
- if (!keyToken.token.startsWith("_")) {
3595
- entity[keyToken.token] = valueToken.token;
3596
- }
3597
- }
3598
- entities.push(entity);
3585
+ function ai_run(self, distance2, deltaSeconds) {
3586
+ setIdealYawTowards(self, self.enemy ?? self.goalentity);
3587
+ changeYaw(self, deltaSeconds);
3588
+ if (distance2 !== 0) {
3589
+ walkMove(self, self.angles.y, distance2);
3599
3590
  }
3600
- return entities;
3601
3591
  }
3602
- var SpawnRegistry = class {
3603
- constructor() {
3604
- this.registry = /* @__PURE__ */ new Map();
3605
- }
3606
- register(classname, spawn) {
3607
- this.registry.set(classname, spawn);
3592
+ function ai_face(self, enemy, distance2, deltaSeconds) {
3593
+ if (enemy) {
3594
+ setIdealYawTowards(self, enemy);
3608
3595
  }
3609
- get(classname) {
3610
- return this.registry.get(classname);
3596
+ changeYaw(self, deltaSeconds);
3597
+ if (distance2 !== 0) {
3598
+ walkMove(self, self.angles.y, distance2);
3611
3599
  }
3612
- };
3613
- function defaultWarn(message) {
3614
- void message;
3615
3600
  }
3616
- function spawnEntityFromDictionary(dictionary, options) {
3617
- const warn = options.onWarning ?? defaultWarn;
3618
- const classname = dictionary.classname;
3619
- if (!classname) {
3620
- warn("Encountered entity with no classname");
3621
- return null;
3601
+ function ai_charge(self, distance2, deltaSeconds) {
3602
+ setIdealYawTowards(self, self.enemy);
3603
+ changeYaw(self, deltaSeconds);
3604
+ if (distance2 !== 0) {
3605
+ walkMove(self, self.angles.y, distance2);
3622
3606
  }
3623
- const isWorld = classname === "worldspawn";
3624
- const entity = isWorld ? options.entities.world : options.entities.spawn();
3625
- applyEntityKeyValues(entity, dictionary);
3626
- const context = {
3627
- keyValues: dictionary,
3628
- entities: options.entities,
3629
- warn,
3630
- free(target) {
3631
- options.entities.freeImmediate(target);
3607
+ }
3608
+
3609
+ // src/ai/perception.ts
3610
+ var RangeCategory = /* @__PURE__ */ ((RangeCategory2) => {
3611
+ RangeCategory2["Melee"] = "melee";
3612
+ RangeCategory2["Near"] = "near";
3613
+ RangeCategory2["Mid"] = "mid";
3614
+ RangeCategory2["Far"] = "far";
3615
+ return RangeCategory2;
3616
+ })(RangeCategory || {});
3617
+ function absBounds(entity) {
3618
+ return {
3619
+ mins: {
3620
+ x: entity.origin.x + entity.mins.x,
3621
+ y: entity.origin.y + entity.mins.y,
3622
+ z: entity.origin.z + entity.mins.z
3623
+ },
3624
+ maxs: {
3625
+ x: entity.origin.x + entity.maxs.x,
3626
+ y: entity.origin.y + entity.maxs.y,
3627
+ z: entity.origin.z + entity.maxs.z
3632
3628
  }
3633
3629
  };
3634
- const spawnFunc = options.registry.get(classname);
3635
- if (!spawnFunc) {
3636
- warn(`${classname} does not have a spawn function`);
3637
- if (!isWorld) {
3638
- options.entities.freeImmediate(entity);
3639
- }
3640
- return null;
3630
+ }
3631
+ function rangeTo(self, other) {
3632
+ const a = absBounds(self);
3633
+ const b = absBounds(other);
3634
+ const distanceSquared = distanceBetweenBoxesSquared(a.mins, a.maxs, b.mins, b.maxs);
3635
+ return Math.sqrt(distanceSquared);
3636
+ }
3637
+ function classifyRange(distance2) {
3638
+ if (distance2 <= RANGE_MELEE) {
3639
+ return "melee" /* Melee */;
3641
3640
  }
3642
- spawnFunc(entity, context);
3643
- if (!entity.inUse) {
3644
- return null;
3641
+ if (distance2 <= RANGE_NEAR) {
3642
+ return "near" /* Near */;
3645
3643
  }
3646
- options.entities.finalizeSpawn(entity);
3647
- return entity;
3644
+ if (distance2 <= RANGE_MID) {
3645
+ return "mid" /* Mid */;
3646
+ }
3647
+ return "far" /* Far */;
3648
3648
  }
3649
- function spawnEntitiesFromText(text, options) {
3650
- const parsed = parseEntityLump(text);
3651
- const spawned = [];
3652
- for (const dictionary of parsed) {
3653
- const entity = spawnEntityFromDictionary(dictionary, options);
3654
- if (entity) {
3655
- spawned.push(entity);
3656
- }
3649
+ function infront(self, other) {
3650
+ const { forward } = angleVectors(self.angles);
3651
+ const direction = normalizeVec3(subtractVec3(other.origin, self.origin));
3652
+ const dot = dotVec3(direction, forward);
3653
+ if ((self.spawnflags & SPAWNFLAG_MONSTER_AMBUSH) !== 0 && self.trail_time === 0 && self.enemy === null) {
3654
+ return dot > 0.15;
3657
3655
  }
3658
- return spawned;
3656
+ return dot > -0.3;
3659
3657
  }
3660
- function findPlayerStart(entities) {
3661
- return entities.find(
3662
- (entity) => entity.classname === "info_player_start"
3663
- );
3658
+ function visible(self, other, trace, options) {
3659
+ if ((other.flags & FL_NOVISIBLE) !== 0) {
3660
+ return false;
3661
+ }
3662
+ const start = { x: self.origin.x, y: self.origin.y, z: self.origin.z + self.viewheight };
3663
+ const end = { x: other.origin.x, y: other.origin.y, z: other.origin.z + other.viewheight };
3664
+ const mask = options?.throughGlass ? 1 /* Opaque */ : 1 /* Opaque */ | 2 /* Window */;
3665
+ const result = trace(start, end, self, mask);
3666
+ return result.fraction === 1 || result.entity === other;
3664
3667
  }
3665
- function registerDefaultSpawns(game, registry) {
3666
- registry.register("worldspawn", (entity) => {
3667
- entity.movetype = 2 /* Push */;
3668
- entity.solid = 3 /* Bsp */;
3669
- entity.modelindex = entity.modelindex || 1;
3670
- });
3671
- registry.register("info_player_start", () => {
3672
- });
3673
- registry.register("info_player_deathmatch", () => {
3674
- });
3675
- registry.register("info_player_coop", () => {
3676
- });
3677
- registry.register("info_null", (entity, context) => {
3678
- context.free(entity);
3679
- });
3680
- registry.register("info_notnull", () => {
3681
- });
3682
- registry.register("info_teleport_destination", () => {
3683
- });
3684
- registerTriggerSpawns(registry);
3685
- registerTargetSpawns(registry);
3686
- registerMiscSpawns(registry);
3687
- registerItemSpawns(game, registry);
3688
- registerFuncSpawns(registry);
3689
- registerPathSpawns(registry);
3690
- registerLightSpawns(registry);
3668
+
3669
+ // src/ai/targeting.ts
3670
+ function setIdealYawTowards2(self, other) {
3671
+ const delta = {
3672
+ x: other.origin.x - self.origin.x,
3673
+ y: other.origin.y - self.origin.y,
3674
+ z: other.origin.z - self.origin.z
3675
+ };
3676
+ self.ideal_yaw = vectorToYaw(delta);
3691
3677
  }
3692
- function createDefaultSpawnRegistry(game) {
3693
- const registry = new SpawnRegistry();
3694
- registerDefaultSpawns(game, registry);
3695
- return registry;
3678
+ function faceYawInstantly(self) {
3679
+ self.angles.y = angleMod(self.ideal_yaw);
3696
3680
  }
3697
-
3698
- // src/entities/callbacks.ts
3699
- function createCallbackRegistry() {
3700
- return /* @__PURE__ */ new Map();
3681
+ function huntTarget(self, level) {
3682
+ if (!self.enemy) return;
3683
+ self.goalentity = self.enemy;
3684
+ setIdealYawTowards2(self, self.enemy);
3685
+ faceYawInstantly(self);
3686
+ if ((self.monsterinfo.aiflags & 1 /* StandGround */) !== 0) {
3687
+ self.monsterinfo.stand?.(self);
3688
+ } else {
3689
+ self.monsterinfo.run?.(self);
3690
+ self.attack_finished_time = level.timeSeconds + 1;
3691
+ }
3701
3692
  }
3702
- function registerCallback(registry, name, fn) {
3703
- if (registry.has(name)) {
3693
+ function foundTarget(self, level, options) {
3694
+ if (!self.enemy) return;
3695
+ if ((self.enemy.svflags & 8 /* Player */) !== 0) {
3696
+ level.sightEntity = self;
3697
+ level.sightEntityFrame = level.frameNumber;
3698
+ self.light_level = 128;
3699
+ }
3700
+ self.show_hostile = level.timeSeconds + 1;
3701
+ const lastSighting = self.monsterinfo.last_sighting;
3702
+ lastSighting.x = self.enemy.origin.x;
3703
+ lastSighting.y = self.enemy.origin.y;
3704
+ lastSighting.z = self.enemy.origin.z;
3705
+ self.trail_time = level.timeSeconds;
3706
+ self.monsterinfo.trail_time = level.timeSeconds;
3707
+ if (!self.combattarget) {
3708
+ huntTarget(self, level);
3704
3709
  return;
3705
3710
  }
3706
- registry.set(name, fn);
3711
+ const pickTarget = options?.pickTarget;
3712
+ const movetarget = pickTarget?.(self.combattarget) ?? self.enemy;
3713
+ self.goalentity = movetarget;
3714
+ self.movetarget = movetarget;
3715
+ self.combattarget = void 0;
3716
+ self.monsterinfo.aiflags |= 4096 /* CombatPoint */;
3717
+ if (self.movetarget) {
3718
+ self.movetarget.targetname = void 0;
3719
+ }
3720
+ self.monsterinfo.pausetime = 0;
3721
+ self.monsterinfo.run?.(self);
3707
3722
  }
3708
-
3709
- // src/loop.ts
3710
- var orderedStageNames = [
3711
- "prep",
3712
- "simulate",
3713
- "finish"
3714
- ];
3715
- var GameFrameLoop = class {
3716
- constructor(initialStages) {
3717
- this.timeMs = 0;
3718
- this.frame = 0;
3719
- this.stageHandlers = {
3720
- prep: [],
3721
- simulate: [],
3722
- finish: []
3723
- };
3724
- this.stageCounts = {
3725
- prep: 0,
3726
- simulate: 0,
3727
- finish: 0
3728
- };
3729
- this.stageCompactionNeeded = {
3730
- prep: false,
3731
- simulate: false,
3732
- finish: false
3733
- };
3734
- if (initialStages) {
3735
- for (const stageName of orderedStageNames) {
3736
- const handler = initialStages[stageName];
3737
- if (handler) {
3738
- this.addStage(stageName, handler);
3739
- }
3740
- }
3723
+ function classifyClientVisibility(self, other, level, trace) {
3724
+ const distance2 = rangeTo(self, other);
3725
+ const range = classifyRange(distance2);
3726
+ if (range === "far" /* Far */) return false;
3727
+ if (other.light_level <= 5) return false;
3728
+ if (!visible(self, other, trace, { throughGlass: false })) return false;
3729
+ if (range === "near" /* Near */) {
3730
+ return level.timeSeconds <= other.show_hostile || infront(self, other);
3731
+ }
3732
+ if (range === "mid" /* Mid */) {
3733
+ return infront(self, other);
3734
+ }
3735
+ return true;
3736
+ }
3737
+ function updateSoundChase(self, client, level, hearability, trace) {
3738
+ if ((self.spawnflags & SPAWNFLAG_MONSTER_AMBUSH) !== 0) {
3739
+ if (!visible(self, client, trace)) return false;
3740
+ } else if (hearability.canHear && !hearability.canHear(self, client)) {
3741
+ return false;
3742
+ }
3743
+ const delta = subtractVec3(client.origin, self.origin);
3744
+ if (lengthVec3(delta) > 1e3) return false;
3745
+ if (hearability.areasConnected && !hearability.areasConnected(self, client)) return false;
3746
+ self.ideal_yaw = vectorToYaw(delta);
3747
+ faceYawInstantly(self);
3748
+ self.monsterinfo.aiflags |= 4 /* SoundTarget */;
3749
+ self.enemy = client;
3750
+ return true;
3751
+ }
3752
+ function chooseCandidate(self, level) {
3753
+ if (level.sightEntity && level.sightEntityFrame >= level.frameNumber - 1 && (self.spawnflags & SPAWNFLAG_MONSTER_AMBUSH) === 0) {
3754
+ if (level.sightEntity.enemy !== self.enemy) {
3755
+ return { candidate: level.sightEntity, heardit: false };
3741
3756
  }
3757
+ return { candidate: null, heardit: false };
3742
3758
  }
3743
- addStage(stage, handler) {
3744
- const handlers = this.stageHandlers[stage];
3745
- handlers.push(handler);
3746
- this.stageCounts[stage] += 1;
3747
- return () => {
3748
- const index = handlers.indexOf(handler);
3749
- if (index >= 0 && handlers[index]) {
3750
- handlers[index] = void 0;
3751
- this.stageCounts[stage] -= 1;
3752
- this.stageCompactionNeeded[stage] = true;
3753
- }
3754
- };
3759
+ if (level.soundEntity && level.soundEntityFrame >= level.frameNumber - 1) {
3760
+ return { candidate: level.soundEntity, heardit: true };
3755
3761
  }
3756
- reset(startTimeMs) {
3757
- this.timeMs = startTimeMs;
3758
- this.frame = 0;
3762
+ if (!self.enemy && level.sound2Entity && level.sound2EntityFrame >= level.frameNumber - 1 && (self.spawnflags & SPAWNFLAG_MONSTER_AMBUSH) === 0) {
3763
+ return { candidate: level.sound2Entity, heardit: true };
3759
3764
  }
3760
- advance(step) {
3761
- const previousTimeMs = this.timeMs;
3762
- this.timeMs = previousTimeMs + step.deltaMs;
3763
- this.frame = step.frame;
3764
- const context = {
3765
- ...step,
3766
- timeMs: this.timeMs,
3767
- previousTimeMs,
3768
- deltaSeconds: step.deltaMs / 1e3
3769
- };
3770
- this.runStage("prep", context);
3771
- if (this.stageCounts.simulate === 0) {
3772
- throw new Error("GameFrameLoop requires at least one simulate stage");
3773
- }
3774
- this.runStage("simulate", context);
3775
- this.runStage("finish", context);
3776
- return context;
3765
+ if (level.sightClient) {
3766
+ return { candidate: level.sightClient, heardit: false };
3777
3767
  }
3778
- runStage(stage, context) {
3779
- const handlers = this.stageHandlers[stage];
3780
- for (let i = 0; i < handlers.length; i += 1) {
3781
- const handler = handlers[i];
3782
- if (!handler) {
3783
- continue;
3784
- }
3785
- handler(context);
3786
- }
3787
- if (this.stageCompactionNeeded[stage]) {
3788
- this.compactStageHandlers(stage);
3789
- }
3768
+ return { candidate: null, heardit: false };
3769
+ }
3770
+ function rejectNotargetEntity(client) {
3771
+ if ((client.flags & FL_NOTARGET) !== 0) return true;
3772
+ if ((client.svflags & 4 /* Monster */) !== 0 && client.enemy) {
3773
+ return (client.enemy.flags & FL_NOTARGET) !== 0;
3790
3774
  }
3791
- compactStageHandlers(stage) {
3792
- const handlers = this.stageHandlers[stage];
3793
- let writeIndex = 0;
3794
- for (let readIndex = 0; readIndex < handlers.length; readIndex += 1) {
3795
- const handler = handlers[readIndex];
3796
- if (handler) {
3797
- handlers[writeIndex] = handler;
3798
- writeIndex += 1;
3799
- }
3775
+ if (client.enemy && (client.enemy.flags & FL_NOTARGET) !== 0) return true;
3776
+ return false;
3777
+ }
3778
+ function findTarget(self, level, trace, hearability = {}) {
3779
+ if ((self.monsterinfo.aiflags & 256 /* GoodGuy */) !== 0) {
3780
+ if (self.goalentity?.classname === "target_actor") {
3781
+ return false;
3800
3782
  }
3801
- handlers.length = writeIndex;
3802
- this.stageCompactionNeeded[stage] = false;
3783
+ return false;
3803
3784
  }
3804
- get time() {
3805
- return this.timeMs;
3785
+ if ((self.monsterinfo.aiflags & 4096 /* CombatPoint */) !== 0) {
3786
+ return false;
3806
3787
  }
3807
- get frameNumber() {
3808
- return this.frame;
3788
+ const { candidate, heardit } = chooseCandidate(self, level);
3789
+ if (!candidate || !candidate.inUse) return false;
3790
+ if (candidate === self.enemy) return true;
3791
+ if (rejectNotargetEntity(candidate)) return false;
3792
+ if (!heardit) {
3793
+ if (!classifyClientVisibility(self, candidate, level, trace)) return false;
3794
+ self.monsterinfo.aiflags &= ~4 /* SoundTarget */;
3795
+ self.enemy = candidate;
3796
+ } else if (!updateSoundChase(self, candidate, level, hearability, trace)) {
3797
+ return false;
3809
3798
  }
3810
- };
3799
+ foundTarget(self, level);
3800
+ if ((self.monsterinfo.aiflags & 4 /* SoundTarget */) === 0) {
3801
+ self.monsterinfo.sight?.(self, self.enemy);
3802
+ }
3803
+ return true;
3804
+ }
3811
3805
 
3812
- // src/level.ts
3813
- var ZERO_STATE = {
3814
- frameNumber: 0,
3815
- timeSeconds: 0,
3816
- previousTimeSeconds: 0,
3817
- deltaSeconds: 0
3818
- };
3819
- var LevelClock = class {
3820
- constructor() {
3821
- this.state = ZERO_STATE;
3806
+ // src/ai/monster.ts
3807
+ function M_MoveFrame(self) {
3808
+ const move = self.monsterinfo.current_move;
3809
+ if (!move) {
3810
+ return;
3822
3811
  }
3823
- start(startTimeMs) {
3824
- const startSeconds = startTimeMs / 1e3;
3825
- this.state = {
3826
- frameNumber: 0,
3827
- timeSeconds: startSeconds,
3828
- previousTimeSeconds: startSeconds,
3829
- deltaSeconds: 0
3830
- };
3812
+ if (self.frame < move.firstframe || self.frame > move.lastframe) {
3813
+ self.monsterinfo.aiflags &= ~128 /* HoldFrame */;
3814
+ self.frame = move.firstframe;
3831
3815
  }
3832
- tick(context) {
3833
- this.state = {
3834
- frameNumber: context.frame,
3835
- timeSeconds: context.timeMs / 1e3,
3836
- previousTimeSeconds: context.previousTimeMs / 1e3,
3837
- deltaSeconds: context.deltaSeconds
3838
- };
3839
- return this.state;
3816
+ if ((self.monsterinfo.aiflags & 128 /* HoldFrame */) !== 0) {
3817
+ return;
3840
3818
  }
3841
- get current() {
3842
- return this.state;
3819
+ const index = self.frame - move.firstframe;
3820
+ const frame = move.frames[index];
3821
+ if (frame.ai) {
3822
+ frame.ai(self, frame.dist);
3843
3823
  }
3844
- restore(state) {
3845
- this.state = { ...state };
3824
+ if (frame.think) {
3825
+ frame.think(self);
3846
3826
  }
3847
- };
3848
-
3849
- // src/ai/constants.ts
3850
- var RANGE_MELEE = 20;
3851
- var RANGE_NEAR = 440;
3852
- var RANGE_MID = 940;
3853
- var FL_NOTARGET = 1 << 5;
3854
- var FL_NOVISIBLE = 1 << 24;
3855
- var SPAWNFLAG_MONSTER_AMBUSH = 1 << 0;
3856
- var AIFlags = /* @__PURE__ */ ((AIFlags2) => {
3857
- AIFlags2[AIFlags2["StandGround"] = 1] = "StandGround";
3858
- AIFlags2[AIFlags2["TempStandGround"] = 2] = "TempStandGround";
3859
- AIFlags2[AIFlags2["SoundTarget"] = 4] = "SoundTarget";
3860
- AIFlags2[AIFlags2["LostSight"] = 8] = "LostSight";
3861
- AIFlags2[AIFlags2["PursuitLastSeen"] = 16] = "PursuitLastSeen";
3862
- AIFlags2[AIFlags2["PursueNext"] = 32] = "PursueNext";
3863
- AIFlags2[AIFlags2["PursueTemp"] = 64] = "PursueTemp";
3864
- AIFlags2[AIFlags2["HoldFrame"] = 128] = "HoldFrame";
3865
- AIFlags2[AIFlags2["GoodGuy"] = 256] = "GoodGuy";
3866
- AIFlags2[AIFlags2["Brutal"] = 512] = "Brutal";
3867
- AIFlags2[AIFlags2["NoStep"] = 1024] = "NoStep";
3868
- AIFlags2[AIFlags2["Ducked"] = 2048] = "Ducked";
3869
- AIFlags2[AIFlags2["CombatPoint"] = 4096] = "CombatPoint";
3870
- AIFlags2[AIFlags2["Medic"] = 8192] = "Medic";
3871
- AIFlags2[AIFlags2["Resurrecting"] = 16384] = "Resurrecting";
3872
- AIFlags2[AIFlags2["Pathing"] = 1073741824] = "Pathing";
3873
- return AIFlags2;
3874
- })(AIFlags || {});
3875
- var TraceMask = /* @__PURE__ */ ((TraceMask2) => {
3876
- TraceMask2[TraceMask2["Opaque"] = 1] = "Opaque";
3877
- TraceMask2[TraceMask2["Window"] = 2] = "Window";
3878
- return TraceMask2;
3879
- })(TraceMask || {});
3827
+ if (!self.inUse) {
3828
+ return;
3829
+ }
3830
+ self.frame++;
3831
+ if (self.frame > move.lastframe) {
3832
+ if (move.endfunc) {
3833
+ move.endfunc(self);
3834
+ if (self.monsterinfo.current_move !== move) {
3835
+ return;
3836
+ }
3837
+ }
3838
+ }
3839
+ }
3840
+ function monster_think(self, context) {
3841
+ M_MoveFrame(self);
3842
+ const time = context && typeof context.timeSeconds === "number" ? context.timeSeconds : self.nextthink;
3843
+ self.nextthink = time + 0.1;
3844
+ }
3880
3845
 
3881
- // src/ai/movement.ts
3882
- function yawVector(yawDegrees, distance2) {
3883
- if (distance2 === 0) {
3884
- return { x: 0, y: 0, z: 0 };
3846
+ // src/entities/monsters/soldier.ts
3847
+ var MONSTER_TICK = 0.1;
3848
+ function monster_ai_stand(self, dist) {
3849
+ ai_stand(self, MONSTER_TICK);
3850
+ }
3851
+ function monster_ai_walk(self, dist) {
3852
+ ai_walk(self, dist, MONSTER_TICK);
3853
+ }
3854
+ function monster_ai_run(self, dist) {
3855
+ ai_run(self, dist, MONSTER_TICK);
3856
+ }
3857
+ function monster_ai_charge(self, dist) {
3858
+ ai_charge(self, dist, MONSTER_TICK);
3859
+ }
3860
+ var stand_move;
3861
+ var walk_move;
3862
+ var run_move;
3863
+ var attack_move;
3864
+ function soldier_stand(self) {
3865
+ self.monsterinfo.current_move = stand_move;
3866
+ }
3867
+ function soldier_walk(self) {
3868
+ self.monsterinfo.current_move = walk_move;
3869
+ }
3870
+ function soldier_run(self) {
3871
+ if (self.enemy && self.enemy.health > 0) {
3872
+ self.monsterinfo.current_move = run_move;
3873
+ } else {
3874
+ self.monsterinfo.current_move = stand_move;
3885
3875
  }
3886
- const radians = degToRad(yawDegrees);
3887
- return {
3888
- x: Math.cos(radians) * distance2,
3889
- y: Math.sin(radians) * distance2,
3890
- z: 0
3876
+ }
3877
+ function soldier_attack(self) {
3878
+ self.monsterinfo.current_move = attack_move;
3879
+ }
3880
+ function soldier_fire(self) {
3881
+ }
3882
+ var stand_frames = Array.from({ length: 30 }, () => ({
3883
+ ai: monster_ai_stand,
3884
+ dist: 0
3885
+ }));
3886
+ stand_move = {
3887
+ firstframe: 0,
3888
+ lastframe: 29,
3889
+ frames: stand_frames,
3890
+ endfunc: soldier_stand
3891
+ };
3892
+ var walk_frames = Array.from({ length: 40 }, () => ({
3893
+ ai: monster_ai_walk,
3894
+ dist: 2
3895
+ }));
3896
+ walk_move = {
3897
+ firstframe: 30,
3898
+ lastframe: 69,
3899
+ frames: walk_frames,
3900
+ endfunc: soldier_walk
3901
+ };
3902
+ var run_frames = Array.from({ length: 20 }, () => ({
3903
+ ai: monster_ai_run,
3904
+ dist: 10
3905
+ }));
3906
+ run_move = {
3907
+ firstframe: 70,
3908
+ lastframe: 89,
3909
+ frames: run_frames,
3910
+ endfunc: soldier_run
3911
+ };
3912
+ var attack_frames = Array.from({ length: 10 }, (_, i) => ({
3913
+ ai: monster_ai_charge,
3914
+ dist: 0,
3915
+ think: i === 5 ? soldier_fire : null
3916
+ }));
3917
+ attack_move = {
3918
+ firstframe: 90,
3919
+ lastframe: 99,
3920
+ frames: attack_frames,
3921
+ endfunc: soldier_run
3922
+ };
3923
+ function SP_monster_soldier(self, context) {
3924
+ self.model = "models/monsters/soldier/tris.md2";
3925
+ self.mins = { x: -16, y: -16, z: -24 };
3926
+ self.maxs = { x: 16, y: 16, z: 32 };
3927
+ self.movetype = 5 /* Step */;
3928
+ self.solid = 2 /* BoundingBox */;
3929
+ self.health = 20;
3930
+ self.max_health = 20;
3931
+ self.mass = 100;
3932
+ self.pain = (self2, other, kick, damage) => {
3891
3933
  };
3934
+ self.die = (self2, inflictor, attacker, damage, point) => {
3935
+ self2.deadflag = 2 /* Dead */;
3936
+ self2.solid = 0 /* Not */;
3937
+ };
3938
+ self.monsterinfo.stand = soldier_stand;
3939
+ self.monsterinfo.walk = soldier_walk;
3940
+ self.monsterinfo.run = soldier_run;
3941
+ self.monsterinfo.attack = soldier_attack;
3942
+ self.think = monster_think;
3943
+ soldier_stand(self);
3944
+ self.nextthink = self.timestamp + MONSTER_TICK;
3892
3945
  }
3893
- function walkMove(self, yawDegrees, distance2) {
3894
- const delta = yawVector(yawDegrees, distance2);
3895
- const origin = self.origin;
3896
- origin.x += delta.x;
3897
- origin.y += delta.y;
3898
- origin.z += delta.z;
3899
- return true;
3946
+ function registerMonsterSpawns(registry) {
3947
+ registry.register("monster_soldier", SP_monster_soldier);
3900
3948
  }
3901
- function changeYaw(self, deltaSeconds) {
3902
- const current = angleMod(self.angles.y);
3903
- const ideal = self.ideal_yaw;
3904
- if (current === ideal) {
3905
- self.angles.y = current;
3906
- return;
3907
- }
3908
- const speed = self.yaw_speed * deltaSeconds * 10;
3909
- let move = ideal - current;
3910
- if (ideal > current) {
3911
- if (move >= 180) move -= 360;
3912
- } else if (move <= -180) {
3913
- move += 360;
3914
- }
3915
- if (move > speed) move = speed;
3916
- else if (move < -speed) move = -speed;
3917
- self.angles.y = angleMod(current + move);
3949
+
3950
+ // src/entities/spawn.ts
3951
+ var FIELD_LOOKUP = new Map(
3952
+ ENTITY_FIELD_METADATA.map((field) => [field.name, field])
3953
+ );
3954
+ function parseVec3(text) {
3955
+ const parts = text.trim().split(/\s+/);
3956
+ const [x = 0, y = 0, z = 0] = parts.map((part) => Number.parseFloat(part)).map((value) => Number.isNaN(value) ? 0 : value);
3957
+ return { x, y, z };
3918
3958
  }
3919
- function facingIdeal(self) {
3920
- const delta = angleMod(self.angles.y - self.ideal_yaw);
3921
- const hasPathing = (self.monsterinfo.aiflags & 1073741824 /* Pathing */) !== 0;
3922
- if (hasPathing) {
3923
- return !(delta > 5 && delta < 355);
3959
+ function parseBoolean(text) {
3960
+ const normalized = text.trim().toLowerCase();
3961
+ return normalized === "1" || normalized === "true" || normalized === "yes";
3962
+ }
3963
+ function parseValue(type, value) {
3964
+ switch (type) {
3965
+ case "int":
3966
+ return Number.parseInt(value, 10) || 0;
3967
+ case "float":
3968
+ return Number.parseFloat(value) || 0;
3969
+ case "boolean":
3970
+ return parseBoolean(value);
3971
+ case "vec3":
3972
+ return parseVec3(value);
3973
+ case "string":
3974
+ return value;
3975
+ case "entity":
3976
+ case "callback":
3977
+ return void 0;
3978
+ default:
3979
+ return value;
3924
3980
  }
3925
- return !(delta > 45 && delta < 315);
3926
- }
3927
- function ai_move(self, distance2) {
3928
- walkMove(self, self.angles.y, distance2);
3929
3981
  }
3930
- function setIdealYawTowards(self, target) {
3931
- if (!target) return;
3932
- const toTarget = {
3933
- x: target.origin.x - self.origin.x,
3934
- y: target.origin.y - self.origin.y,
3935
- z: target.origin.z - self.origin.z
3982
+ function applyEntityKeyValues(entity, values) {
3983
+ if ("angle" in values && !("angles" in values)) {
3984
+ entity.angles = { x: 0, y: Number.parseFloat(values.angle) || 0, z: 0 };
3985
+ }
3986
+ for (const [key, rawValue] of Object.entries(values)) {
3987
+ if (key.startsWith("_")) {
3988
+ continue;
3989
+ }
3990
+ const descriptor = FIELD_LOOKUP.get(key);
3991
+ if (!descriptor) {
3992
+ continue;
3993
+ }
3994
+ const parsed = parseValue(descriptor.type, rawValue);
3995
+ if (parsed !== void 0) {
3996
+ entity[descriptor.name] = parsed;
3997
+ }
3998
+ }
3999
+ entity.size = {
4000
+ x: entity.maxs.x - entity.mins.x,
4001
+ y: entity.maxs.y - entity.mins.y,
4002
+ z: entity.maxs.z - entity.mins.z
3936
4003
  };
3937
- self.ideal_yaw = vectorToYaw(toTarget);
3938
4004
  }
3939
- function ai_stand(self, deltaSeconds) {
3940
- changeYaw(self, deltaSeconds);
4005
+ function parseQuoted(text, start) {
4006
+ let index = start;
4007
+ let result = "";
4008
+ while (index < text.length) {
4009
+ const char = text[index];
4010
+ if (char === '"') {
4011
+ return { value: result, nextIndex: index + 1 };
4012
+ }
4013
+ result += char;
4014
+ index += 1;
4015
+ }
4016
+ throw new Error("Unterminated quoted string in entity lump");
3941
4017
  }
3942
- function ai_walk(self, distance2, deltaSeconds) {
3943
- setIdealYawTowards(self, self.goalentity);
3944
- changeYaw(self, deltaSeconds);
3945
- if (distance2 !== 0) {
3946
- walkMove(self, self.angles.y, distance2);
4018
+ function consumeWhitespace(text, start) {
4019
+ let index = start;
4020
+ while (index < text.length && /\s/.test(text[index] ?? "")) {
4021
+ index += 1;
3947
4022
  }
4023
+ return index;
3948
4024
  }
3949
- function ai_turn(self, distance2, deltaSeconds) {
3950
- if (distance2 !== 0) {
3951
- walkMove(self, self.angles.y, distance2);
4025
+ function parseToken(text, start) {
4026
+ const index = consumeWhitespace(text, start);
4027
+ if (index >= text.length) {
4028
+ return { token: null, nextIndex: index };
3952
4029
  }
3953
- changeYaw(self, deltaSeconds);
4030
+ const current = text[index];
4031
+ if (current === "{" || current === "}") {
4032
+ return { token: current, nextIndex: index + 1 };
4033
+ }
4034
+ if (current !== '"') {
4035
+ throw new Error(`Unexpected token in entity lump: ${current}`);
4036
+ }
4037
+ const quoted = parseQuoted(text, index + 1);
4038
+ return { token: quoted.value, nextIndex: quoted.nextIndex };
3954
4039
  }
3955
- function ai_run(self, distance2, deltaSeconds) {
3956
- setIdealYawTowards(self, self.enemy ?? self.goalentity);
3957
- changeYaw(self, deltaSeconds);
3958
- if (distance2 !== 0) {
3959
- walkMove(self, self.angles.y, distance2);
4040
+ function parseEntityLump(text) {
4041
+ const entities = [];
4042
+ let index = 0;
4043
+ while (index < text.length) {
4044
+ const open = parseToken(text, index);
4045
+ index = open.nextIndex;
4046
+ if (open.token === null) {
4047
+ break;
4048
+ }
4049
+ if (open.token !== "{") {
4050
+ throw new Error("Expected { at start of entity definition");
4051
+ }
4052
+ const entity = {};
4053
+ while (true) {
4054
+ const keyToken = parseToken(text, index);
4055
+ index = keyToken.nextIndex;
4056
+ if (keyToken.token === null) {
4057
+ throw new Error("EOF reached while parsing entity");
4058
+ }
4059
+ if (keyToken.token === "}") {
4060
+ break;
4061
+ }
4062
+ const valueToken = parseToken(text, index);
4063
+ index = valueToken.nextIndex;
4064
+ if (valueToken.token === null || valueToken.token === "{" || valueToken.token === "}") {
4065
+ throw new Error("Malformed entity key/value pair");
4066
+ }
4067
+ if (!keyToken.token.startsWith("_")) {
4068
+ entity[keyToken.token] = valueToken.token;
4069
+ }
4070
+ }
4071
+ entities.push(entity);
3960
4072
  }
4073
+ return entities;
3961
4074
  }
3962
- function ai_face(self, enemy, distance2, deltaSeconds) {
3963
- if (enemy) {
3964
- setIdealYawTowards(self, enemy);
4075
+ var SpawnRegistry = class {
4076
+ constructor() {
4077
+ this.registry = /* @__PURE__ */ new Map();
3965
4078
  }
3966
- changeYaw(self, deltaSeconds);
3967
- if (distance2 !== 0) {
3968
- walkMove(self, self.angles.y, distance2);
4079
+ register(classname, spawn) {
4080
+ this.registry.set(classname, spawn);
3969
4081
  }
3970
- }
3971
- function ai_charge(self, distance2, deltaSeconds) {
3972
- setIdealYawTowards(self, self.enemy);
3973
- changeYaw(self, deltaSeconds);
3974
- if (distance2 !== 0) {
3975
- walkMove(self, self.angles.y, distance2);
4082
+ get(classname) {
4083
+ return this.registry.get(classname);
3976
4084
  }
4085
+ };
4086
+ function defaultWarn(message) {
4087
+ void message;
3977
4088
  }
3978
-
3979
- // src/ai/perception.ts
3980
- var RangeCategory = /* @__PURE__ */ ((RangeCategory2) => {
3981
- RangeCategory2["Melee"] = "melee";
3982
- RangeCategory2["Near"] = "near";
3983
- RangeCategory2["Mid"] = "mid";
3984
- RangeCategory2["Far"] = "far";
3985
- return RangeCategory2;
3986
- })(RangeCategory || {});
3987
- function absBounds(entity) {
3988
- return {
3989
- mins: {
3990
- x: entity.origin.x + entity.mins.x,
3991
- y: entity.origin.y + entity.mins.y,
3992
- z: entity.origin.z + entity.mins.z
3993
- },
3994
- maxs: {
3995
- x: entity.origin.x + entity.maxs.x,
3996
- y: entity.origin.y + entity.maxs.y,
3997
- z: entity.origin.z + entity.maxs.z
4089
+ function spawnEntityFromDictionary(dictionary, options) {
4090
+ const warn = options.onWarning ?? defaultWarn;
4091
+ const classname = dictionary.classname;
4092
+ if (!classname) {
4093
+ warn("Encountered entity with no classname");
4094
+ return null;
4095
+ }
4096
+ const isWorld = classname === "worldspawn";
4097
+ const entity = isWorld ? options.entities.world : options.entities.spawn();
4098
+ applyEntityKeyValues(entity, dictionary);
4099
+ const context = {
4100
+ keyValues: dictionary,
4101
+ entities: options.entities,
4102
+ warn,
4103
+ free(target) {
4104
+ options.entities.freeImmediate(target);
3998
4105
  }
3999
4106
  };
4000
- }
4001
- function rangeTo(self, other) {
4002
- const a = absBounds(self);
4003
- const b = absBounds(other);
4004
- const distanceSquared = distanceBetweenBoxesSquared(a.mins, a.maxs, b.mins, b.maxs);
4005
- return Math.sqrt(distanceSquared);
4006
- }
4007
- function classifyRange(distance2) {
4008
- if (distance2 <= RANGE_MELEE) {
4009
- return "melee" /* Melee */;
4010
- }
4011
- if (distance2 <= RANGE_NEAR) {
4012
- return "near" /* Near */;
4107
+ const spawnFunc = options.registry.get(classname);
4108
+ if (!spawnFunc) {
4109
+ warn(`${classname} does not have a spawn function`);
4110
+ if (!isWorld) {
4111
+ options.entities.freeImmediate(entity);
4112
+ }
4113
+ return null;
4013
4114
  }
4014
- if (distance2 <= RANGE_MID) {
4015
- return "mid" /* Mid */;
4115
+ spawnFunc(entity, context);
4116
+ if (!entity.inUse) {
4117
+ return null;
4016
4118
  }
4017
- return "far" /* Far */;
4119
+ options.entities.finalizeSpawn(entity);
4120
+ return entity;
4018
4121
  }
4019
- function infront(self, other) {
4020
- const { forward } = angleVectors(self.angles);
4021
- const direction = normalizeVec3(subtractVec3(other.origin, self.origin));
4022
- const dot = dotVec3(direction, forward);
4023
- if ((self.spawnflags & SPAWNFLAG_MONSTER_AMBUSH) !== 0 && self.trail_time === 0 && self.enemy === null) {
4024
- return dot > 0.15;
4122
+ function spawnEntitiesFromText(text, options) {
4123
+ const parsed = parseEntityLump(text);
4124
+ const spawned = [];
4125
+ for (const dictionary of parsed) {
4126
+ const entity = spawnEntityFromDictionary(dictionary, options);
4127
+ if (entity) {
4128
+ spawned.push(entity);
4129
+ }
4025
4130
  }
4026
- return dot > -0.3;
4131
+ return spawned;
4027
4132
  }
4028
- function visible(self, other, trace, options) {
4029
- if ((other.flags & FL_NOVISIBLE) !== 0) {
4030
- return false;
4031
- }
4032
- const start = { x: self.origin.x, y: self.origin.y, z: self.origin.z + self.viewheight };
4033
- const end = { x: other.origin.x, y: other.origin.y, z: other.origin.z + other.viewheight };
4034
- const mask = options?.throughGlass ? 1 /* Opaque */ : 1 /* Opaque */ | 2 /* Window */;
4035
- const result = trace(start, end, self, mask);
4036
- return result.fraction === 1 || result.entity === other;
4133
+ function findPlayerStart(entities) {
4134
+ return entities.find(
4135
+ (entity) => entity.classname === "info_player_start"
4136
+ );
4037
4137
  }
4038
-
4039
- // src/ai/targeting.ts
4040
- function setIdealYawTowards2(self, other) {
4041
- const delta = {
4042
- x: other.origin.x - self.origin.x,
4043
- y: other.origin.y - self.origin.y,
4044
- z: other.origin.z - self.origin.z
4045
- };
4046
- self.ideal_yaw = vectorToYaw(delta);
4138
+ function registerDefaultSpawns(game, registry) {
4139
+ registry.register("worldspawn", (entity) => {
4140
+ entity.movetype = 2 /* Push */;
4141
+ entity.solid = 3 /* Bsp */;
4142
+ entity.modelindex = entity.modelindex || 1;
4143
+ });
4144
+ registry.register("info_player_start", () => {
4145
+ });
4146
+ registry.register("info_player_deathmatch", () => {
4147
+ });
4148
+ registry.register("info_player_coop", () => {
4149
+ });
4150
+ registry.register("info_null", (entity, context) => {
4151
+ context.free(entity);
4152
+ });
4153
+ registry.register("info_notnull", () => {
4154
+ });
4155
+ registry.register("info_teleport_destination", () => {
4156
+ });
4157
+ registerTriggerSpawns(registry);
4158
+ registerTargetSpawns(registry);
4159
+ registerMiscSpawns(registry);
4160
+ registerItemSpawns(game, registry);
4161
+ registerFuncSpawns(registry);
4162
+ registerPathSpawns(registry);
4163
+ registerLightSpawns(registry);
4164
+ registerMonsterSpawns(registry);
4047
4165
  }
4048
- function faceYawInstantly(self) {
4049
- self.angles.y = angleMod(self.ideal_yaw);
4166
+ function createDefaultSpawnRegistry(game) {
4167
+ const registry = new SpawnRegistry();
4168
+ registerDefaultSpawns(game, registry);
4169
+ return registry;
4050
4170
  }
4051
- function huntTarget(self, level) {
4052
- if (!self.enemy) return;
4053
- self.goalentity = self.enemy;
4054
- setIdealYawTowards2(self, self.enemy);
4055
- faceYawInstantly(self);
4056
- if ((self.monsterinfo.aiflags & 1 /* StandGround */) !== 0) {
4057
- self.monsterinfo.stand?.(self);
4058
- } else {
4059
- self.monsterinfo.run?.(self);
4060
- self.attack_finished_time = level.timeSeconds + 1;
4061
- }
4171
+
4172
+ // src/entities/callbacks.ts
4173
+ function createCallbackRegistry() {
4174
+ return /* @__PURE__ */ new Map();
4062
4175
  }
4063
- function foundTarget(self, level, options) {
4064
- if (!self.enemy) return;
4065
- if ((self.enemy.svflags & 8 /* Player */) !== 0) {
4066
- level.sightEntity = self;
4067
- level.sightEntityFrame = level.frameNumber;
4068
- self.light_level = 128;
4069
- }
4070
- self.show_hostile = level.timeSeconds + 1;
4071
- const lastSighting = self.monsterinfo.last_sighting;
4072
- lastSighting.x = self.enemy.origin.x;
4073
- lastSighting.y = self.enemy.origin.y;
4074
- lastSighting.z = self.enemy.origin.z;
4075
- self.trail_time = level.timeSeconds;
4076
- self.monsterinfo.trail_time = level.timeSeconds;
4077
- if (!self.combattarget) {
4078
- huntTarget(self, level);
4176
+ function registerCallback(registry, name, fn) {
4177
+ if (registry.has(name)) {
4079
4178
  return;
4080
4179
  }
4081
- const pickTarget = options?.pickTarget;
4082
- const movetarget = pickTarget?.(self.combattarget) ?? self.enemy;
4083
- self.goalentity = movetarget;
4084
- self.movetarget = movetarget;
4085
- self.combattarget = void 0;
4086
- self.monsterinfo.aiflags |= 4096 /* CombatPoint */;
4087
- if (self.movetarget) {
4088
- self.movetarget.targetname = void 0;
4089
- }
4090
- self.monsterinfo.pausetime = 0;
4091
- self.monsterinfo.run?.(self);
4180
+ registry.set(name, fn);
4092
4181
  }
4093
- function classifyClientVisibility(self, other, level, trace) {
4094
- const distance2 = rangeTo(self, other);
4095
- const range = classifyRange(distance2);
4096
- if (range === "far" /* Far */) return false;
4097
- if (other.light_level <= 5) return false;
4098
- if (!visible(self, other, trace, { throughGlass: false })) return false;
4099
- if (range === "near" /* Near */) {
4100
- return level.timeSeconds <= other.show_hostile || infront(self, other);
4182
+
4183
+ // src/loop.ts
4184
+ var orderedStageNames = [
4185
+ "prep",
4186
+ "simulate",
4187
+ "finish"
4188
+ ];
4189
+ var GameFrameLoop = class {
4190
+ constructor(initialStages) {
4191
+ this.timeMs = 0;
4192
+ this.frame = 0;
4193
+ this.stageHandlers = {
4194
+ prep: [],
4195
+ simulate: [],
4196
+ finish: []
4197
+ };
4198
+ this.stageCounts = {
4199
+ prep: 0,
4200
+ simulate: 0,
4201
+ finish: 0
4202
+ };
4203
+ this.stageCompactionNeeded = {
4204
+ prep: false,
4205
+ simulate: false,
4206
+ finish: false
4207
+ };
4208
+ if (initialStages) {
4209
+ for (const stageName of orderedStageNames) {
4210
+ const handler = initialStages[stageName];
4211
+ if (handler) {
4212
+ this.addStage(stageName, handler);
4213
+ }
4214
+ }
4215
+ }
4101
4216
  }
4102
- if (range === "mid" /* Mid */) {
4103
- return infront(self, other);
4217
+ addStage(stage, handler) {
4218
+ const handlers = this.stageHandlers[stage];
4219
+ handlers.push(handler);
4220
+ this.stageCounts[stage] += 1;
4221
+ return () => {
4222
+ const index = handlers.indexOf(handler);
4223
+ if (index >= 0 && handlers[index]) {
4224
+ handlers[index] = void 0;
4225
+ this.stageCounts[stage] -= 1;
4226
+ this.stageCompactionNeeded[stage] = true;
4227
+ }
4228
+ };
4104
4229
  }
4105
- return true;
4106
- }
4107
- function updateSoundChase(self, client, level, hearability, trace) {
4108
- if ((self.spawnflags & SPAWNFLAG_MONSTER_AMBUSH) !== 0) {
4109
- if (!visible(self, client, trace)) return false;
4110
- } else if (hearability.canHear && !hearability.canHear(self, client)) {
4111
- return false;
4230
+ reset(startTimeMs) {
4231
+ this.timeMs = startTimeMs;
4232
+ this.frame = 0;
4112
4233
  }
4113
- const delta = subtractVec3(client.origin, self.origin);
4114
- if (lengthVec3(delta) > 1e3) return false;
4115
- if (hearability.areasConnected && !hearability.areasConnected(self, client)) return false;
4116
- self.ideal_yaw = vectorToYaw(delta);
4117
- faceYawInstantly(self);
4118
- self.monsterinfo.aiflags |= 4 /* SoundTarget */;
4119
- self.enemy = client;
4120
- return true;
4121
- }
4122
- function chooseCandidate(self, level) {
4123
- if (level.sightEntity && level.sightEntityFrame >= level.frameNumber - 1 && (self.spawnflags & SPAWNFLAG_MONSTER_AMBUSH) === 0) {
4124
- if (level.sightEntity.enemy !== self.enemy) {
4125
- return { candidate: level.sightEntity, heardit: false };
4234
+ advance(step) {
4235
+ const previousTimeMs = this.timeMs;
4236
+ this.timeMs = previousTimeMs + step.deltaMs;
4237
+ this.frame = step.frame;
4238
+ const context = {
4239
+ ...step,
4240
+ timeMs: this.timeMs,
4241
+ previousTimeMs,
4242
+ deltaSeconds: step.deltaMs / 1e3
4243
+ };
4244
+ this.runStage("prep", context);
4245
+ if (this.stageCounts.simulate === 0) {
4246
+ throw new Error("GameFrameLoop requires at least one simulate stage");
4126
4247
  }
4127
- return { candidate: null, heardit: false };
4248
+ this.runStage("simulate", context);
4249
+ this.runStage("finish", context);
4250
+ return context;
4128
4251
  }
4129
- if (level.soundEntity && level.soundEntityFrame >= level.frameNumber - 1) {
4130
- return { candidate: level.soundEntity, heardit: true };
4252
+ runStage(stage, context) {
4253
+ const handlers = this.stageHandlers[stage];
4254
+ for (let i = 0; i < handlers.length; i += 1) {
4255
+ const handler = handlers[i];
4256
+ if (!handler) {
4257
+ continue;
4258
+ }
4259
+ handler(context);
4260
+ }
4261
+ if (this.stageCompactionNeeded[stage]) {
4262
+ this.compactStageHandlers(stage);
4263
+ }
4131
4264
  }
4132
- if (!self.enemy && level.sound2Entity && level.sound2EntityFrame >= level.frameNumber - 1 && (self.spawnflags & SPAWNFLAG_MONSTER_AMBUSH) === 0) {
4133
- return { candidate: level.sound2Entity, heardit: true };
4265
+ compactStageHandlers(stage) {
4266
+ const handlers = this.stageHandlers[stage];
4267
+ let writeIndex = 0;
4268
+ for (let readIndex = 0; readIndex < handlers.length; readIndex += 1) {
4269
+ const handler = handlers[readIndex];
4270
+ if (handler) {
4271
+ handlers[writeIndex] = handler;
4272
+ writeIndex += 1;
4273
+ }
4274
+ }
4275
+ handlers.length = writeIndex;
4276
+ this.stageCompactionNeeded[stage] = false;
4134
4277
  }
4135
- if (level.sightClient) {
4136
- return { candidate: level.sightClient, heardit: false };
4278
+ get time() {
4279
+ return this.timeMs;
4137
4280
  }
4138
- return { candidate: null, heardit: false };
4139
- }
4140
- function rejectNotargetEntity(client) {
4141
- if ((client.flags & FL_NOTARGET) !== 0) return true;
4142
- if ((client.svflags & 4 /* Monster */) !== 0 && client.enemy) {
4143
- return (client.enemy.flags & FL_NOTARGET) !== 0;
4281
+ get frameNumber() {
4282
+ return this.frame;
4144
4283
  }
4145
- if (client.enemy && (client.enemy.flags & FL_NOTARGET) !== 0) return true;
4146
- return false;
4147
- }
4148
- function findTarget(self, level, trace, hearability = {}) {
4149
- if ((self.monsterinfo.aiflags & 256 /* GoodGuy */) !== 0) {
4150
- if (self.goalentity?.classname === "target_actor") {
4151
- return false;
4152
- }
4153
- return false;
4284
+ };
4285
+
4286
+ // src/level.ts
4287
+ var ZERO_STATE = {
4288
+ frameNumber: 0,
4289
+ timeSeconds: 0,
4290
+ previousTimeSeconds: 0,
4291
+ deltaSeconds: 0
4292
+ };
4293
+ var LevelClock = class {
4294
+ constructor() {
4295
+ this.state = ZERO_STATE;
4154
4296
  }
4155
- if ((self.monsterinfo.aiflags & 4096 /* CombatPoint */) !== 0) {
4156
- return false;
4297
+ start(startTimeMs) {
4298
+ const startSeconds = startTimeMs / 1e3;
4299
+ this.state = {
4300
+ frameNumber: 0,
4301
+ timeSeconds: startSeconds,
4302
+ previousTimeSeconds: startSeconds,
4303
+ deltaSeconds: 0
4304
+ };
4157
4305
  }
4158
- const { candidate, heardit } = chooseCandidate(self, level);
4159
- if (!candidate || !candidate.inUse) return false;
4160
- if (candidate === self.enemy) return true;
4161
- if (rejectNotargetEntity(candidate)) return false;
4162
- if (!heardit) {
4163
- if (!classifyClientVisibility(self, candidate, level, trace)) return false;
4164
- self.monsterinfo.aiflags &= ~4 /* SoundTarget */;
4165
- self.enemy = candidate;
4166
- } else if (!updateSoundChase(self, candidate, level, hearability, trace)) {
4167
- return false;
4306
+ tick(context) {
4307
+ this.state = {
4308
+ frameNumber: context.frame,
4309
+ timeSeconds: context.timeMs / 1e3,
4310
+ previousTimeSeconds: context.previousTimeMs / 1e3,
4311
+ deltaSeconds: context.deltaSeconds
4312
+ };
4313
+ return this.state;
4168
4314
  }
4169
- foundTarget(self, level);
4170
- if ((self.monsterinfo.aiflags & 4 /* SoundTarget */) === 0) {
4171
- self.monsterinfo.sight?.(self, self.enemy);
4315
+ get current() {
4316
+ return this.state;
4172
4317
  }
4173
- return true;
4174
- }
4318
+ restore(state) {
4319
+ this.state = { ...state };
4320
+ }
4321
+ };
4175
4322
 
4176
4323
  // src/checksum.ts
4177
4324
  var FNV_OFFSET_BASIS = 2166136261;
@@ -5552,6 +5699,7 @@ function createGame({ trace, pointcontents }, engine, options) {
5552
5699
  HEALTH_ITEMS,
5553
5700
  KEY_ITEMS,
5554
5701
  KeyId,
5702
+ M_MoveFrame,
5555
5703
  MoveType,
5556
5704
  ORDERED_DAMAGE_MODS,
5557
5705
  POWERUP_ITEMS,
@@ -5628,6 +5776,7 @@ function createGame({ trace, pointcontents }, engine, options) {
5628
5776
  infront,
5629
5777
  isZeroVector,
5630
5778
  killBox,
5779
+ monster_think,
5631
5780
  parseEntityLump,
5632
5781
  parseRereleaseSave,
5633
5782
  parseSaveFile,