@quake2ts/shared 0.0.1

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.
Files changed (209) hide show
  1. package/dist/browser/index.global.js +2 -0
  2. package/dist/browser/index.global.js.map +1 -0
  3. package/dist/cjs/index.cjs +6569 -0
  4. package/dist/cjs/index.cjs.map +1 -0
  5. package/dist/esm/index.js +6200 -0
  6. package/dist/esm/index.js.map +1 -0
  7. package/dist/tsconfig.tsbuildinfo +1 -0
  8. package/dist/types/audio/constants.d.ts +24 -0
  9. package/dist/types/audio/constants.d.ts.map +1 -0
  10. package/dist/types/bsp/collision.d.ts +201 -0
  11. package/dist/types/bsp/collision.d.ts.map +1 -0
  12. package/dist/types/bsp/contents.d.ts +72 -0
  13. package/dist/types/bsp/contents.d.ts.map +1 -0
  14. package/dist/types/bsp/spatial.d.ts +13 -0
  15. package/dist/types/bsp/spatial.d.ts.map +1 -0
  16. package/dist/types/index.d.ts +38 -0
  17. package/dist/types/index.d.ts.map +1 -0
  18. package/dist/types/inventory-helpers.d.ts +19 -0
  19. package/dist/types/inventory-helpers.d.ts.map +1 -0
  20. package/dist/types/io/binaryStream.d.ts +38 -0
  21. package/dist/types/io/binaryStream.d.ts.map +1 -0
  22. package/dist/types/io/binaryWriter.d.ts +26 -0
  23. package/dist/types/io/binaryWriter.d.ts.map +1 -0
  24. package/dist/types/io/index.d.ts +4 -0
  25. package/dist/types/io/index.d.ts.map +1 -0
  26. package/dist/types/io/messageBuilder.d.ts +21 -0
  27. package/dist/types/io/messageBuilder.d.ts.map +1 -0
  28. package/dist/types/items/ammo.d.ts +40 -0
  29. package/dist/types/items/ammo.d.ts.map +1 -0
  30. package/dist/types/items/index.d.ts +8 -0
  31. package/dist/types/items/index.d.ts.map +1 -0
  32. package/dist/types/items/powerups.d.ts +31 -0
  33. package/dist/types/items/powerups.d.ts.map +1 -0
  34. package/dist/types/items/weaponInfo.d.ts +5 -0
  35. package/dist/types/items/weaponInfo.d.ts.map +1 -0
  36. package/dist/types/items/weapons.d.ts +27 -0
  37. package/dist/types/items/weapons.d.ts.map +1 -0
  38. package/dist/types/math/angles.d.ts +19 -0
  39. package/dist/types/math/angles.d.ts.map +1 -0
  40. package/dist/types/math/anorms.d.ts +2 -0
  41. package/dist/types/math/anorms.d.ts.map +1 -0
  42. package/dist/types/math/color.d.ts +12 -0
  43. package/dist/types/math/color.d.ts.map +1 -0
  44. package/dist/types/math/mat4.d.ts +7 -0
  45. package/dist/types/math/mat4.d.ts.map +1 -0
  46. package/dist/types/math/random.d.ts +60 -0
  47. package/dist/types/math/random.d.ts.map +1 -0
  48. package/dist/types/math/vec3.d.ts +79 -0
  49. package/dist/types/math/vec3.d.ts.map +1 -0
  50. package/dist/types/net/driver.d.ts +10 -0
  51. package/dist/types/net/driver.d.ts.map +1 -0
  52. package/dist/types/net/index.d.ts +3 -0
  53. package/dist/types/net/index.d.ts.map +1 -0
  54. package/dist/types/net/netchan.d.ts +85 -0
  55. package/dist/types/net/netchan.d.ts.map +1 -0
  56. package/dist/types/pmove/apply.d.ts +5 -0
  57. package/dist/types/pmove/apply.d.ts.map +1 -0
  58. package/dist/types/pmove/categorize.d.ts +36 -0
  59. package/dist/types/pmove/categorize.d.ts.map +1 -0
  60. package/dist/types/pmove/config.d.ts +5 -0
  61. package/dist/types/pmove/config.d.ts.map +1 -0
  62. package/dist/types/pmove/constants.d.ts +76 -0
  63. package/dist/types/pmove/constants.d.ts.map +1 -0
  64. package/dist/types/pmove/currents.d.ts +58 -0
  65. package/dist/types/pmove/currents.d.ts.map +1 -0
  66. package/dist/types/pmove/dimensions.d.ts +14 -0
  67. package/dist/types/pmove/dimensions.d.ts.map +1 -0
  68. package/dist/types/pmove/duck.d.ts +39 -0
  69. package/dist/types/pmove/duck.d.ts.map +1 -0
  70. package/dist/types/pmove/fly.d.ts +34 -0
  71. package/dist/types/pmove/fly.d.ts.map +1 -0
  72. package/dist/types/pmove/index.d.ts +18 -0
  73. package/dist/types/pmove/index.d.ts.map +1 -0
  74. package/dist/types/pmove/jump.d.ts +28 -0
  75. package/dist/types/pmove/jump.d.ts.map +1 -0
  76. package/dist/types/pmove/move.d.ts +78 -0
  77. package/dist/types/pmove/move.d.ts.map +1 -0
  78. package/dist/types/pmove/pmove.d.ts +40 -0
  79. package/dist/types/pmove/pmove.d.ts.map +1 -0
  80. package/dist/types/pmove/slide.d.ts +63 -0
  81. package/dist/types/pmove/slide.d.ts.map +1 -0
  82. package/dist/types/pmove/snap.d.ts +40 -0
  83. package/dist/types/pmove/snap.d.ts.map +1 -0
  84. package/dist/types/pmove/special.d.ts +39 -0
  85. package/dist/types/pmove/special.d.ts.map +1 -0
  86. package/dist/types/pmove/stuck.d.ts +21 -0
  87. package/dist/types/pmove/stuck.d.ts.map +1 -0
  88. package/dist/types/pmove/types.d.ts +72 -0
  89. package/dist/types/pmove/types.d.ts.map +1 -0
  90. package/dist/types/pmove/view.d.ts +19 -0
  91. package/dist/types/pmove/view.d.ts.map +1 -0
  92. package/dist/types/pmove/water.d.ts +21 -0
  93. package/dist/types/pmove/water.d.ts.map +1 -0
  94. package/dist/types/protocol/bitpack.d.ts +17 -0
  95. package/dist/types/protocol/bitpack.d.ts.map +1 -0
  96. package/dist/types/protocol/configstrings.d.ts +73 -0
  97. package/dist/types/protocol/configstrings.d.ts.map +1 -0
  98. package/dist/types/protocol/constants.d.ts +36 -0
  99. package/dist/types/protocol/constants.d.ts.map +1 -0
  100. package/dist/types/protocol/contracts.d.ts +17 -0
  101. package/dist/types/protocol/contracts.d.ts.map +1 -0
  102. package/dist/types/protocol/crc.d.ts +5 -0
  103. package/dist/types/protocol/crc.d.ts.map +1 -0
  104. package/dist/types/protocol/cvar.d.ts +15 -0
  105. package/dist/types/protocol/cvar.d.ts.map +1 -0
  106. package/dist/types/protocol/effects.d.ts +33 -0
  107. package/dist/types/protocol/effects.d.ts.map +1 -0
  108. package/dist/types/protocol/entity.d.ts +46 -0
  109. package/dist/types/protocol/entity.d.ts.map +1 -0
  110. package/dist/types/protocol/entityEvent.d.ts +13 -0
  111. package/dist/types/protocol/entityEvent.d.ts.map +1 -0
  112. package/dist/types/protocol/entityState.d.ts +26 -0
  113. package/dist/types/protocol/entityState.d.ts.map +1 -0
  114. package/dist/types/protocol/index.d.ts +19 -0
  115. package/dist/types/protocol/index.d.ts.map +1 -0
  116. package/dist/types/protocol/layout.d.ts +9 -0
  117. package/dist/types/protocol/layout.d.ts.map +1 -0
  118. package/dist/types/protocol/ops.d.ts +44 -0
  119. package/dist/types/protocol/ops.d.ts.map +1 -0
  120. package/dist/types/protocol/player-state.d.ts +40 -0
  121. package/dist/types/protocol/player-state.d.ts.map +1 -0
  122. package/dist/types/protocol/player.d.ts +28 -0
  123. package/dist/types/protocol/player.d.ts.map +1 -0
  124. package/dist/types/protocol/renderFx.d.ts +23 -0
  125. package/dist/types/protocol/renderFx.d.ts.map +1 -0
  126. package/dist/types/protocol/stats.d.ts +61 -0
  127. package/dist/types/protocol/stats.d.ts.map +1 -0
  128. package/dist/types/protocol/tempEntity.d.ts +67 -0
  129. package/dist/types/protocol/tempEntity.d.ts.map +1 -0
  130. package/dist/types/protocol/usercmd.d.ts +33 -0
  131. package/dist/types/protocol/usercmd.d.ts.map +1 -0
  132. package/dist/types/protocol/writeUserCmd.d.ts +4 -0
  133. package/dist/types/protocol/writeUserCmd.d.ts.map +1 -0
  134. package/dist/types/replay/index.d.ts +3 -0
  135. package/dist/types/replay/index.d.ts.map +1 -0
  136. package/dist/types/replay/io.d.ts +7 -0
  137. package/dist/types/replay/io.d.ts.map +1 -0
  138. package/dist/types/replay/schema.d.ts +41 -0
  139. package/dist/types/replay/schema.d.ts.map +1 -0
  140. package/dist/types/testing.d.ts +6 -0
  141. package/dist/types/testing.d.ts.map +1 -0
  142. package/package.json +43 -0
  143. package/src/audio/constants.ts +35 -0
  144. package/src/bsp/collision.ts +1075 -0
  145. package/src/bsp/contents.ts +108 -0
  146. package/src/bsp/spatial.ts +116 -0
  147. package/src/index.ts +37 -0
  148. package/src/inventory-helpers.ts +81 -0
  149. package/src/io/binaryStream.ts +159 -0
  150. package/src/io/binaryWriter.ts +146 -0
  151. package/src/io/index.ts +3 -0
  152. package/src/io/messageBuilder.ts +117 -0
  153. package/src/items/ammo.ts +47 -0
  154. package/src/items/index.ts +8 -0
  155. package/src/items/powerups.ts +32 -0
  156. package/src/items/weaponInfo.ts +45 -0
  157. package/src/items/weapons.ts +28 -0
  158. package/src/math/angles.ts +135 -0
  159. package/src/math/anorms.ts +165 -0
  160. package/src/math/color.ts +42 -0
  161. package/src/math/mat4.ts +58 -0
  162. package/src/math/random.ts +182 -0
  163. package/src/math/vec3.ts +379 -0
  164. package/src/net/driver.ts +9 -0
  165. package/src/net/index.ts +2 -0
  166. package/src/net/netchan.ts +451 -0
  167. package/src/pmove/apply.ts +151 -0
  168. package/src/pmove/categorize.ts +162 -0
  169. package/src/pmove/config.ts +5 -0
  170. package/src/pmove/constants.ts +94 -0
  171. package/src/pmove/currents.ts +287 -0
  172. package/src/pmove/dimensions.ts +40 -0
  173. package/src/pmove/duck.ts +154 -0
  174. package/src/pmove/fly.ts +197 -0
  175. package/src/pmove/index.ts +18 -0
  176. package/src/pmove/jump.ts +92 -0
  177. package/src/pmove/move.ts +527 -0
  178. package/src/pmove/pmove.ts +446 -0
  179. package/src/pmove/slide.ts +267 -0
  180. package/src/pmove/snap.ts +89 -0
  181. package/src/pmove/special.ts +207 -0
  182. package/src/pmove/stuck.ts +258 -0
  183. package/src/pmove/types.ts +82 -0
  184. package/src/pmove/view.ts +57 -0
  185. package/src/pmove/water.ts +56 -0
  186. package/src/protocol/bitpack.ts +139 -0
  187. package/src/protocol/configstrings.ts +104 -0
  188. package/src/protocol/constants.ts +40 -0
  189. package/src/protocol/contracts.ts +149 -0
  190. package/src/protocol/crc.ts +32 -0
  191. package/src/protocol/cvar.ts +15 -0
  192. package/src/protocol/effects.ts +33 -0
  193. package/src/protocol/entity.ts +304 -0
  194. package/src/protocol/entityEvent.ts +14 -0
  195. package/src/protocol/entityState.ts +28 -0
  196. package/src/protocol/index.ts +19 -0
  197. package/src/protocol/layout.ts +9 -0
  198. package/src/protocol/ops.ts +49 -0
  199. package/src/protocol/player-state.ts +51 -0
  200. package/src/protocol/player.ts +165 -0
  201. package/src/protocol/renderFx.ts +22 -0
  202. package/src/protocol/stats.ts +161 -0
  203. package/src/protocol/tempEntity.ts +69 -0
  204. package/src/protocol/usercmd.ts +63 -0
  205. package/src/protocol/writeUserCmd.ts +30 -0
  206. package/src/replay/index.ts +2 -0
  207. package/src/replay/io.ts +37 -0
  208. package/src/replay/schema.ts +42 -0
  209. package/src/testing.ts +200 -0
@@ -0,0 +1,161 @@
1
+ import { setCompressedInteger, getCompressedInteger } from './bitpack.js';
2
+ import { PowerupId } from '../items/powerups.js';
3
+
4
+ // Matching rerelease/bg_local.h:196-262
5
+ export enum PlayerStat {
6
+ STAT_HEALTH_ICON = 0,
7
+ STAT_HEALTH,
8
+ STAT_AMMO_ICON,
9
+ STAT_AMMO,
10
+ STAT_ARMOR_ICON,
11
+ STAT_ARMOR,
12
+ STAT_SELECTED_ICON,
13
+ STAT_PICKUP_ICON,
14
+ STAT_PICKUP_STRING,
15
+ STAT_TIMER_ICON,
16
+ STAT_TIMER,
17
+ STAT_HELPICON,
18
+ STAT_SELECTED_ITEM,
19
+ STAT_LAYOUTS,
20
+ STAT_FRAGS,
21
+ STAT_FLASHES,
22
+ STAT_CHASE,
23
+ STAT_SPECTATOR,
24
+
25
+ // CTF Stats (Rerelease/KEX)
26
+ STAT_CTF_TEAM1_PIC = 18,
27
+ STAT_CTF_TEAM1_CAPS = 19,
28
+ STAT_CTF_TEAM2_PIC = 20,
29
+ STAT_CTF_TEAM2_CAPS = 21,
30
+ STAT_CTF_FLAG_PIC = 22,
31
+ STAT_CTF_JOINED_TEAM1_PIC = 23,
32
+ STAT_CTF_JOINED_TEAM2_PIC = 24,
33
+ STAT_CTF_TEAM1_HEADER = 25,
34
+ STAT_CTF_TEAM2_HEADER = 26,
35
+ STAT_CTF_TECH = 27,
36
+ STAT_CTF_ID_VIEW = 28,
37
+ STAT_CTF_MATCH = 29,
38
+ STAT_CTF_ID_VIEW_COLOR = 30,
39
+ STAT_CTF_TEAMINFO = 31,
40
+
41
+ // Rerelease additions
42
+ STAT_WEAPONS_OWNED_1 = 32,
43
+ STAT_WEAPONS_OWNED_2 = 33,
44
+
45
+ // Ammo counts (start index)
46
+ STAT_AMMO_INFO_START = 34,
47
+ // Calculated below, but enum needs literal or constant if we want to use it as type.
48
+ // However, for TS Enum, we can just define start.
49
+
50
+ // Powerups start after Ammo.
51
+ // AMMO_MAX=12, 9 bits each -> 108 bits -> 7 int16s.
52
+ // 34 + 7 = 41.
53
+ STAT_POWERUP_INFO_START = 41,
54
+
55
+ // Keys and other KEX stats (Start after Powerups)
56
+ // POWERUP_MAX=23, 2 bits each -> 46 bits -> 3 int16s.
57
+ // 41 + 3 = 44.
58
+ STAT_KEY_A = 44,
59
+ STAT_KEY_B = 45,
60
+ STAT_KEY_C = 46,
61
+
62
+ STAT_ACTIVE_WHEEL_WEAPON = 47,
63
+ STAT_COOP_RESPAWN = 48,
64
+ STAT_LIVES = 49,
65
+ STAT_HIT_MARKER = 50,
66
+ STAT_SELECTED_ITEM_NAME = 51,
67
+ STAT_HEALTH_BARS = 52,
68
+ STAT_ACTIVE_WEAPON = 53,
69
+
70
+ STAT_LAST
71
+ }
72
+
73
+ // Constants for bit packing logic
74
+ export const AMMO_MAX = 12;
75
+ export const NUM_BITS_FOR_AMMO = 9;
76
+ export const NUM_AMMO_STATS = Math.ceil((AMMO_MAX * NUM_BITS_FOR_AMMO) / 16); // 7
77
+
78
+ export const POWERUP_MAX = 23; // Adjusted to include TechRegeneration (index 22)
79
+ export const NUM_BITS_FOR_POWERUP = 2;
80
+ export const NUM_POWERUP_STATS = Math.ceil((POWERUP_MAX * NUM_BITS_FOR_POWERUP) / 16); // 3
81
+
82
+ // Powerup ID mapping from string to C++ integer index (powerup_t in bg_local.h)
83
+ const POWERUP_STAT_MAP: Partial<Record<PowerupId, number>> = {
84
+ [PowerupId.PowerScreen]: 0,
85
+ [PowerupId.PowerShield]: 1,
86
+ // 2 is POWERUP_AM_BOMB (not in PowerupId?)
87
+ [PowerupId.QuadDamage]: 3,
88
+ [PowerupId.QuadFire]: 4,
89
+ [PowerupId.Invulnerability]: 5,
90
+ [PowerupId.Invisibility]: 6,
91
+ [PowerupId.Silencer]: 7,
92
+ [PowerupId.Rebreather]: 8,
93
+ [PowerupId.EnviroSuit]: 9,
94
+ [PowerupId.Bandolier]: 10, // Placeholder/Map mismatch handling?
95
+ [PowerupId.AmmoPack]: 10, // Original reused indices or had gaps?
96
+ [PowerupId.IRGoggles]: 11,
97
+ [PowerupId.DoubleDamage]: 12,
98
+ [PowerupId.SphereVengeance]: 13,
99
+ [PowerupId.SphereHunter]: 14,
100
+ [PowerupId.SphereDefender]: 15,
101
+ [PowerupId.Doppelganger]: 16,
102
+ [PowerupId.Flashlight]: 17,
103
+ [PowerupId.Compass]: 18,
104
+ [PowerupId.TechResistance]: 19,
105
+ [PowerupId.TechStrength]: 20,
106
+ [PowerupId.TechHaste]: 21,
107
+ [PowerupId.TechRegeneration]: 22,
108
+ // Add missing mappings to avoid runtime lookups failing for new types
109
+ [PowerupId.TagToken]: -1,
110
+ };
111
+
112
+ // 9 bits for ammo count
113
+ export function G_SetAmmoStat(stats: number[], ammoId: number, count: number): void {
114
+ if (ammoId < 0 || ammoId >= AMMO_MAX) return;
115
+
116
+ // Clamp count to 9 bits (0-511)
117
+ let val = count;
118
+ if (val > 511) val = 511;
119
+ if (val < 0) val = 0;
120
+
121
+ setCompressedInteger(stats, PlayerStat.STAT_AMMO_INFO_START, ammoId, val, NUM_BITS_FOR_AMMO);
122
+ }
123
+
124
+ export function G_GetAmmoStat(stats: number[], ammoId: number): number {
125
+ if (ammoId < 0 || ammoId >= AMMO_MAX) return 0;
126
+ return getCompressedInteger(stats, PlayerStat.STAT_AMMO_INFO_START, ammoId, NUM_BITS_FOR_AMMO);
127
+ }
128
+
129
+ // 2 bits for powerup active/inactive state
130
+ export function G_SetPowerupStat(stats: number[], powerupId: PowerupId | number, val: number): void {
131
+ let index: number | undefined;
132
+
133
+ if (typeof powerupId === 'number') {
134
+ index = powerupId;
135
+ } else {
136
+ index = POWERUP_STAT_MAP[powerupId];
137
+ }
138
+
139
+ if (index === undefined || index < 0 || index >= POWERUP_MAX) return;
140
+
141
+ // Clamp value to 2 bits (0-3)
142
+ let safeVal = val;
143
+ if (safeVal > 3) safeVal = 3;
144
+ if (safeVal < 0) safeVal = 0;
145
+
146
+ setCompressedInteger(stats, PlayerStat.STAT_POWERUP_INFO_START, index, safeVal, NUM_BITS_FOR_POWERUP);
147
+ }
148
+
149
+ export function G_GetPowerupStat(stats: number[], powerupId: PowerupId | number): number {
150
+ let index: number | undefined;
151
+
152
+ if (typeof powerupId === 'number') {
153
+ index = powerupId;
154
+ } else {
155
+ index = POWERUP_STAT_MAP[powerupId];
156
+ }
157
+
158
+ if (index === undefined || index < 0 || index >= POWERUP_MAX) return 0;
159
+
160
+ return getCompressedInteger(stats, PlayerStat.STAT_POWERUP_INFO_START, index, NUM_BITS_FOR_POWERUP);
161
+ }
@@ -0,0 +1,69 @@
1
+
2
+ // Temp entity constants from Quake 2
3
+ export enum TempEntity {
4
+ GUNSHOT = 0,
5
+ BLOOD = 1,
6
+ BLASTER = 2,
7
+ RAILTRAIL = 3,
8
+ SHOTGUN = 4,
9
+ EXPLOSION1 = 5,
10
+ EXPLOSION2 = 6,
11
+ ROCKET_EXPLOSION = 7,
12
+ GRENADE_EXPLOSION = 8,
13
+ SPARKS = 9,
14
+ SPLASH = 10,
15
+ BUBBLETRAIL = 11,
16
+ SCREEN_SPARKS = 12,
17
+ SHIELD_SPARKS = 13,
18
+ BULLET_SPARKS = 14,
19
+ LASER_SPARKS = 15,
20
+ PARASITE_ATTACK = 16,
21
+ ROCKET_EXPLOSION_WATER = 17,
22
+ GRENADE_EXPLOSION_WATER = 18,
23
+ MEDIC_CABLE_ATTACK = 19,
24
+ BFG_EXPLOSION = 20,
25
+ BFG_BIGEXPLOSION = 21,
26
+ BOSSTPORT = 22,
27
+ BFG_LASER = 23,
28
+ GRAPPLE_CABLE = 24,
29
+ WELDING_SPARKS = 25,
30
+ GREENBLOOD = 26,
31
+ BLUEHYPERBLASTER = 27,
32
+ PLASMA_EXPLOSION = 28,
33
+ TUNNEL_SPARKS = 29,
34
+ // ROGUE
35
+ BLASTER2 = 30,
36
+ RAILTRAIL2 = 31,
37
+ FLAME = 32,
38
+ LIGHTNING = 33,
39
+ DEBUGTRAIL = 34,
40
+ PLAIN_EXPLOSION = 35,
41
+ FLASHLIGHT = 36,
42
+ FORCEWALL = 37,
43
+ HEATBEAM = 38,
44
+ MONSTER_HEATBEAM = 39,
45
+ STEAM = 40,
46
+ BUBBLETRAIL2 = 41,
47
+ MOREBLOOD = 42,
48
+ HEATBEAM_SPARKS = 43,
49
+ HEATBEAM_STEAM = 44,
50
+ CHAINFIST_SMOKE = 45,
51
+ ELECTRIC_SPARKS = 46,
52
+ TRACKER_EXPLOSION = 47,
53
+ TELEPORT_EFFECT = 48,
54
+ DBALL_GOAL = 49,
55
+ WIDOWBEAMOUT = 50,
56
+ NUKEBLAST = 51,
57
+ WIDOWSPLASH = 52,
58
+ EXPLOSION1_BIG = 53,
59
+ EXPLOSION1_NP = 54,
60
+ FLECHETTE = 55,
61
+ BLUEHYPERBLASTER_KEX = 56,
62
+ BFG_ZAP = 57,
63
+ BERSERK_SLAM = 58,
64
+ GRAPPLE_CABLE_2 = 59,
65
+ POWER_SPLASH = 60,
66
+ LIGHTNING_BEAM = 61,
67
+ EXPLOSION1_NL = 62,
68
+ EXPLOSION2_NL = 63
69
+ }
@@ -0,0 +1,63 @@
1
+ import { angleMod } from '../math/angles.js';
2
+ import type { Vec3 } from '../math/vec3.js';
3
+ import { PlayerButton } from '../pmove/constants.js';
4
+
5
+ export interface UserCommand {
6
+ readonly msec: number;
7
+ readonly buttons: PlayerButton;
8
+ readonly angles: Vec3;
9
+ readonly forwardmove: number;
10
+ readonly sidemove: number;
11
+ readonly upmove: number;
12
+ readonly serverFrame?: number;
13
+ readonly sequence: number;
14
+ readonly lightlevel: number;
15
+ readonly impulse: number;
16
+ }
17
+
18
+ export interface MouseDelta {
19
+ readonly deltaX: number;
20
+ readonly deltaY: number;
21
+ }
22
+
23
+ export interface MouseLookOptions {
24
+ readonly sensitivity: number;
25
+ readonly invertY: boolean;
26
+ readonly sensitivityX?: number;
27
+ readonly sensitivityY?: number;
28
+ }
29
+
30
+ export const DEFAULT_FORWARD_SPEED = 200;
31
+ export const DEFAULT_SIDE_SPEED = 200;
32
+ export const DEFAULT_UP_SPEED = 200;
33
+ export const DEFAULT_YAW_SPEED = 140;
34
+ export const DEFAULT_PITCH_SPEED = 150;
35
+ export const DEFAULT_MOUSE_SENSITIVITY = 3;
36
+
37
+ function clampPitch(pitch: number): number {
38
+ const normalized = angleMod(pitch);
39
+
40
+ if (normalized > 89 && normalized < 180) return 89;
41
+ if (normalized < 271 && normalized >= 180) return 271;
42
+
43
+ return normalized;
44
+ }
45
+
46
+ export function addViewAngles(current: Vec3, delta: Vec3): Vec3 {
47
+ return {
48
+ x: clampPitch(current.x + delta.x),
49
+ y: angleMod(current.y + delta.y),
50
+ z: angleMod(current.z + delta.z),
51
+ } satisfies Vec3;
52
+ }
53
+
54
+ export function mouseDeltaToViewDelta(delta: MouseDelta, options: MouseLookOptions): Vec3 {
55
+ const yawScale = options.sensitivityX ?? options.sensitivity;
56
+ const pitchScale = (options.sensitivityY ?? options.sensitivity) * (options.invertY ? -1 : 1);
57
+
58
+ return {
59
+ x: delta.deltaY * pitchScale,
60
+ y: delta.deltaX * yawScale,
61
+ z: 0,
62
+ } satisfies Vec3;
63
+ }
@@ -0,0 +1,30 @@
1
+ import { BinaryWriter } from '../io/binaryWriter.js';
2
+ import { UserCommand } from './usercmd.js';
3
+
4
+ export function writeUserCommand(writer: BinaryWriter, cmd: UserCommand): void {
5
+ // msec (byte)
6
+ writer.writeByte(cmd.msec);
7
+
8
+ // buttons (byte)
9
+ writer.writeByte(cmd.buttons);
10
+
11
+ // angles (short * 3) - Scaled 360 -> 65536
12
+ writer.writeAngle16(cmd.angles.x);
13
+ writer.writeAngle16(cmd.angles.y);
14
+ writer.writeAngle16(cmd.angles.z);
15
+
16
+ // forwardmove (short)
17
+ writer.writeShort(cmd.forwardmove);
18
+
19
+ // sidemove (short)
20
+ writer.writeShort(cmd.sidemove);
21
+
22
+ // upmove (short)
23
+ writer.writeShort(cmd.upmove);
24
+
25
+ // impulse (byte)
26
+ writer.writeByte(cmd.impulse);
27
+
28
+ // lightlevel (byte)
29
+ writer.writeByte(0); // TODO: Lightlevel
30
+ }
@@ -0,0 +1,2 @@
1
+ export * from './schema.js';
2
+ export * from './io.js';
@@ -0,0 +1,37 @@
1
+ import { ReplaySession, ReplayFrame } from './schema.js';
2
+ import { UserCommand } from '../protocol/usercmd.js';
3
+
4
+ export function serializeReplay(session: ReplaySession): string {
5
+ return JSON.stringify(session, null, 2);
6
+ }
7
+
8
+ export function deserializeReplay(json: string): ReplaySession {
9
+ const session = JSON.parse(json);
10
+
11
+ // Validate structure lightly
12
+ if (!session.metadata || !Array.isArray(session.frames)) {
13
+ throw new Error('Invalid replay format: missing metadata or frames');
14
+ }
15
+
16
+ return session as ReplaySession;
17
+ }
18
+
19
+ export function createReplaySession(map: string, seed?: number): ReplaySession {
20
+ return {
21
+ metadata: {
22
+ map,
23
+ date: new Date().toISOString(),
24
+ version: '1.0',
25
+ seed
26
+ },
27
+ frames: []
28
+ };
29
+ }
30
+
31
+ export function addReplayFrame(session: ReplaySession, cmd: UserCommand, serverFrame: number, startTime: number) {
32
+ session.frames.push({
33
+ serverFrame,
34
+ cmd,
35
+ timestamp: Date.now() - startTime
36
+ });
37
+ }
@@ -0,0 +1,42 @@
1
+ import { UserCommand } from '../protocol/usercmd.js';
2
+
3
+ export interface ReplayFrame {
4
+ /**
5
+ * The server frame number this command was executed on.
6
+ */
7
+ serverFrame: number;
8
+
9
+ /**
10
+ * The command sent by the client.
11
+ */
12
+ cmd: UserCommand;
13
+
14
+ /**
15
+ * The timestamp (in ms) relative to start of recording.
16
+ */
17
+ timestamp: number;
18
+
19
+ /**
20
+ * The hash of the game state AFTER this frame was executed.
21
+ * Optional for backward compatibility or lightweight recording.
22
+ */
23
+ stateHash?: number;
24
+ }
25
+
26
+ export interface ReplaySession {
27
+ /**
28
+ * Metadata about the recording.
29
+ */
30
+ metadata: {
31
+ map: string;
32
+ date: string;
33
+ version: string;
34
+ seed?: number; // Random seed
35
+ gravity?: { x: number, y: number, z: number }; // Capture gravity
36
+ };
37
+
38
+ /**
39
+ * The sequence of inputs.
40
+ */
41
+ frames: ReplayFrame[];
42
+ }
package/src/testing.ts ADDED
@@ -0,0 +1,200 @@
1
+ import type { PmoveTraceFn, PmoveTraceResult } from './pmove/types.js';
2
+ import type { Vec3 } from './math/vec3.js';
3
+ import { CONTENTS_LADDER } from './bsp/contents.js';
4
+
5
+ export const intersects = (end: Vec3, maxs: Vec3, mins: Vec3, boxMins: Vec3, boxMaxs: Vec3): boolean => {
6
+ return (
7
+ end.x + maxs.x > boxMins.x &&
8
+ end.x + mins.x < boxMaxs.x &&
9
+ end.y + maxs.y > boxMins.y &&
10
+ end.y + mins.y < boxMaxs.y &&
11
+ end.z + maxs.z > boxMins.z &&
12
+ end.z + mins.z < boxMaxs.z
13
+ );
14
+ };
15
+
16
+ export const stairTrace: PmoveTraceFn = (start: Vec3, end: Vec3, mins?: Vec3, maxs?: Vec3): PmoveTraceResult => {
17
+ // Default bbox if not provided
18
+ const useMins = mins ?? { x: -16, y: -16, z: -24 };
19
+ const useMaxs = maxs ?? { x: 16, y: 16, z: 32 };
20
+
21
+ // Step: x from 0 forward, z from 0 to 8
22
+ const STEP_HEIGHT = 8;
23
+ const STEP_X_START = 0;
24
+
25
+ const isHorizontal = Math.abs(end.z - start.z) < 1;
26
+ const isMovingDown = end.z < start.z;
27
+
28
+ // Check if trying to go below the floor
29
+ const endMinZ = end.z + useMins.z;
30
+ const startMinZ = start.z + useMins.z;
31
+ const endMaxX = end.x + useMaxs.x;
32
+
33
+ // If moving horizontally, check if we'd hit the vertical face of the step
34
+ // The step only blocks if the player's origin is below the step height
35
+ if (isHorizontal && end.z < STEP_HEIGHT && endMaxX > STEP_X_START) {
36
+ // Check if we're crossing into the step area
37
+ const startMaxX = start.x + useMaxs.x;
38
+ if (startMaxX <= STEP_X_START) {
39
+ // We're moving from before the step to past it, block
40
+ return {
41
+ allsolid: false,
42
+ startsolid: false,
43
+ fraction: 0,
44
+ endpos: start,
45
+ planeNormal: { x: -1, y: 0, z: 0 },
46
+ contents: 1,
47
+ };
48
+ }
49
+ }
50
+
51
+ // If moving down and over the step area, land on the step surface
52
+ if (isMovingDown && end.x >= STEP_X_START) {
53
+ // The step surface is at z=STEP_HEIGHT in world space
54
+ // The player's bbox bottom reaches this plane when origin.z + mins.z = STEP_HEIGHT
55
+ // So the player's origin should be at z = STEP_HEIGHT - mins.z
56
+ const landZ = STEP_HEIGHT - useMins.z;
57
+
58
+ // Check if we'd pass through the step surface
59
+ // We cross the plane if start is above it and end would be below it
60
+ if (startMinZ > STEP_HEIGHT && endMinZ < STEP_HEIGHT) {
61
+ // Calculate the fraction along the ray where we intersect the plane
62
+ // The bbox bottom is at: start.z + useMins.z + t * (end.z - start.z + 0) = STEP_HEIGHT
63
+ // Solving for t: t = (STEP_HEIGHT - (start.z + useMins.z)) / ((end.z + useMins.z) - (start.z + useMins.z))
64
+ const fraction = (STEP_HEIGHT - startMinZ) / (endMinZ - startMinZ);
65
+
66
+ // Clamp to valid range [0, 1]
67
+ const clampedFraction = Math.max(0, Math.min(1, fraction));
68
+
69
+ // Calculate the endpos along the ray at this fraction
70
+ const finalX = start.x + clampedFraction * (end.x - start.x);
71
+ const finalY = start.y + clampedFraction * (end.y - start.y);
72
+ const finalZ = start.z + clampedFraction * (end.z - start.z);
73
+
74
+ return {
75
+ allsolid: false,
76
+ startsolid: false,
77
+ fraction: clampedFraction,
78
+ endpos: { x: finalX, y: finalY, z: finalZ },
79
+ planeNormal: { x: 0, y: 0, z: 1 },
80
+ contents: 1,
81
+ };
82
+ }
83
+ }
84
+
85
+ // If moving down and would go below floor level, block at floor
86
+ if (isMovingDown && endMinZ < 0) {
87
+ // Floor is at z=0, so player origin should be at z = -mins.z when landing
88
+ const landZ = -useMins.z;
89
+
90
+ // Only apply if we're crossing the floor plane
91
+ if (startMinZ >= 0) {
92
+ // Calculate fraction where bbox bottom hits z=0
93
+ const fraction = (0 - startMinZ) / (endMinZ - startMinZ);
94
+ const clampedFraction = Math.max(0, Math.min(1, fraction));
95
+
96
+ const finalX = start.x + clampedFraction * (end.x - start.x);
97
+ const finalY = start.y + clampedFraction * (end.y - start.y);
98
+ const finalZ = start.z + clampedFraction * (end.z - start.z);
99
+
100
+ return {
101
+ allsolid: false,
102
+ startsolid: false,
103
+ fraction: clampedFraction,
104
+ endpos: { x: finalX, y: finalY, z: finalZ },
105
+ planeNormal: { x: 0, y: 0, z: 1 },
106
+ contents: 1,
107
+ };
108
+ }
109
+
110
+ // Already below floor, block immediately
111
+ return {
112
+ allsolid: false,
113
+ startsolid: false,
114
+ fraction: 0,
115
+ endpos: start,
116
+ planeNormal: { x: 0, y: 0, z: 1 },
117
+ contents: 1,
118
+ };
119
+ }
120
+
121
+ // Free movement
122
+ return {
123
+ allsolid: false,
124
+ startsolid: false,
125
+ fraction: 1.0,
126
+ endpos: end,
127
+ contents: 0,
128
+ };
129
+ };
130
+
131
+ export const ladderTrace: PmoveTraceFn = (start: Vec3, end: Vec3, mins?: Vec3, maxs?: Vec3): PmoveTraceResult => {
132
+ // Default bbox if not provided
133
+ const useMins = mins ?? { x: -16, y: -16, z: -24 };
134
+ const useMaxs = maxs ?? { x: 16, y: 16, z: 32 };
135
+
136
+ // Define the ladder volume (x=0 to x=8, y=-16 to y=16, z=0 to z=100)
137
+ const LADDER_X_MIN = 0;
138
+ const LADDER_X_MAX = 8;
139
+ const LADDER_Y_MIN = -16;
140
+ const LADDER_Y_MAX = 16;
141
+ const LADDER_Z_MIN = 0;
142
+ const LADDER_Z_MAX = 100;
143
+
144
+ // Check if end position is within the ladder volume
145
+ const endInLadder =
146
+ end.x + useMins.x < LADDER_X_MAX &&
147
+ end.x + useMaxs.x > LADDER_X_MIN &&
148
+ end.y + useMins.y < LADDER_Y_MAX &&
149
+ end.y + useMaxs.y > LADDER_Y_MIN &&
150
+ end.z + useMins.z < LADDER_Z_MAX &&
151
+ end.z + useMaxs.z > LADDER_Z_MIN;
152
+
153
+ // If moving into the ladder from outside (moving forward into it)
154
+ const movingIntoLadder = start.x < LADDER_X_MIN && end.x >= LADDER_X_MIN;
155
+
156
+ // If moving horizontally into the ladder front face, block with ladder surface
157
+ if (movingIntoLadder && Math.abs(end.z - start.z) < 0.1) {
158
+ return {
159
+ allsolid: false,
160
+ startsolid: false,
161
+ fraction: 0,
162
+ endpos: start,
163
+ planeNormal: { x: -1, y: 0, z: 0 },
164
+ contents: CONTENTS_LADDER,
165
+ };
166
+ }
167
+
168
+ // If we're in the ladder volume, return success but with CONTENTS_LADDER
169
+ // This allows the player to detect they're on a ladder without blocking movement
170
+ if (endInLadder) {
171
+ return {
172
+ allsolid: false,
173
+ startsolid: false,
174
+ fraction: 1.0,
175
+ endpos: end,
176
+ contents: CONTENTS_LADDER,
177
+ };
178
+ }
179
+
180
+ // Floor at z=0
181
+ if (end.z + useMins.z <= 0) {
182
+ return {
183
+ allsolid: false,
184
+ startsolid: false,
185
+ fraction: 0,
186
+ endpos: start,
187
+ planeNormal: { x: 0, y: 0, z: 1 },
188
+ contents: 1,
189
+ };
190
+ }
191
+
192
+ // No collision - free movement
193
+ return {
194
+ allsolid: false,
195
+ startsolid: false,
196
+ fraction: 1.0,
197
+ endpos: end,
198
+ contents: 0,
199
+ };
200
+ };