@datamoc/mw_games 0.1.2 → 0.2.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 (320) hide show
  1. package/README.md +42 -17
  2. package/dist/actors/Aura.d.ts +29 -0
  3. package/dist/actors/Aura.d.ts.map +1 -0
  4. package/dist/actors/Aura.js +69 -0
  5. package/dist/actors/Aura.js.map +1 -0
  6. package/dist/actors/ItemStatusEffect.d.ts +27 -0
  7. package/dist/actors/ItemStatusEffect.d.ts.map +1 -0
  8. package/dist/actors/ItemStatusEffect.js +35 -0
  9. package/dist/actors/ItemStatusEffect.js.map +1 -0
  10. package/dist/actors/Support.d.ts +32 -0
  11. package/dist/actors/Support.d.ts.map +1 -0
  12. package/dist/actors/Support.js +60 -0
  13. package/dist/actors/Support.js.map +1 -0
  14. package/dist/actors/Traits.d.ts +29 -0
  15. package/dist/actors/Traits.d.ts.map +1 -0
  16. package/dist/actors/Traits.js +21 -0
  17. package/dist/actors/Traits.js.map +1 -0
  18. package/dist/actors/index.d.ts +8 -0
  19. package/dist/actors/index.d.ts.map +1 -1
  20. package/dist/actors/index.js +4 -0
  21. package/dist/actors/index.js.map +1 -1
  22. package/dist/assets/Streaming.d.ts +40 -0
  23. package/dist/assets/Streaming.d.ts.map +1 -0
  24. package/dist/assets/Streaming.js +79 -0
  25. package/dist/assets/Streaming.js.map +1 -0
  26. package/dist/assets/index.d.ts +13 -0
  27. package/dist/assets/index.d.ts.map +1 -1
  28. package/dist/assets/index.js +19 -0
  29. package/dist/assets/index.js.map +1 -1
  30. package/dist/audio/Captions.d.ts +15 -0
  31. package/dist/audio/Captions.d.ts.map +1 -0
  32. package/dist/audio/Captions.js +4 -0
  33. package/dist/audio/Captions.js.map +1 -0
  34. package/dist/audio/Midi.d.ts +75 -0
  35. package/dist/audio/Midi.d.ts.map +1 -0
  36. package/dist/audio/Midi.js +186 -0
  37. package/dist/audio/Midi.js.map +1 -0
  38. package/dist/audio/Music.d.ts.map +1 -1
  39. package/dist/audio/Music.js +5 -2
  40. package/dist/audio/Music.js.map +1 -1
  41. package/dist/audio/Orchestrator.d.ts +40 -0
  42. package/dist/audio/Orchestrator.d.ts.map +1 -0
  43. package/dist/audio/Orchestrator.js +48 -0
  44. package/dist/audio/Orchestrator.js.map +1 -0
  45. package/dist/audio/Sound.d.ts +3 -0
  46. package/dist/audio/Sound.d.ts.map +1 -1
  47. package/dist/audio/Sound.js +8 -1
  48. package/dist/audio/Sound.js.map +1 -1
  49. package/dist/audio/Synth.d.ts +35 -0
  50. package/dist/audio/Synth.d.ts.map +1 -0
  51. package/dist/audio/Synth.js +98 -0
  52. package/dist/audio/Synth.js.map +1 -0
  53. package/dist/audio/index.d.ts +8 -0
  54. package/dist/audio/index.d.ts.map +1 -1
  55. package/dist/audio/index.js +4 -0
  56. package/dist/audio/index.js.map +1 -1
  57. package/dist/board/Army.d.ts +17 -0
  58. package/dist/board/Army.d.ts.map +1 -0
  59. package/dist/board/Army.js +48 -0
  60. package/dist/board/Army.js.map +1 -0
  61. package/dist/board/FogOfWar.d.ts +23 -0
  62. package/dist/board/FogOfWar.d.ts.map +1 -0
  63. package/dist/board/FogOfWar.js +40 -0
  64. package/dist/board/FogOfWar.js.map +1 -0
  65. package/dist/board/Tactics.d.ts +2 -0
  66. package/dist/board/Tactics.d.ts.map +1 -1
  67. package/dist/board/Tactics.js +29 -7
  68. package/dist/board/Tactics.js.map +1 -1
  69. package/dist/board/index.d.ts +5 -1
  70. package/dist/board/index.d.ts.map +1 -1
  71. package/dist/board/index.js +3 -1
  72. package/dist/board/index.js.map +1 -1
  73. package/dist/core/Feedback.d.ts +23 -0
  74. package/dist/core/Feedback.d.ts.map +1 -0
  75. package/dist/core/Feedback.js +35 -0
  76. package/dist/core/Feedback.js.map +1 -0
  77. package/dist/core/Game.d.ts +11 -0
  78. package/dist/core/Game.d.ts.map +1 -1
  79. package/dist/core/Game.js +54 -0
  80. package/dist/core/Game.js.map +1 -1
  81. package/dist/core/Input.d.ts +58 -0
  82. package/dist/core/Input.d.ts.map +1 -1
  83. package/dist/core/Input.js +135 -0
  84. package/dist/core/Input.js.map +1 -1
  85. package/dist/core/Loading.d.ts +48 -0
  86. package/dist/core/Loading.d.ts.map +1 -0
  87. package/dist/core/Loading.js +102 -0
  88. package/dist/core/Loading.js.map +1 -0
  89. package/dist/core/News.d.ts +46 -0
  90. package/dist/core/News.d.ts.map +1 -0
  91. package/dist/core/News.js +79 -0
  92. package/dist/core/News.js.map +1 -0
  93. package/dist/core/PlayerInput.d.ts +38 -0
  94. package/dist/core/PlayerInput.d.ts.map +1 -0
  95. package/dist/core/PlayerInput.js +57 -0
  96. package/dist/core/PlayerInput.js.map +1 -0
  97. package/dist/core/RunHistory.d.ts +43 -0
  98. package/dist/core/RunHistory.d.ts.map +1 -0
  99. package/dist/core/RunHistory.js +41 -0
  100. package/dist/core/RunHistory.js.map +1 -0
  101. package/dist/core/Sanitize.d.ts +56 -0
  102. package/dist/core/Sanitize.d.ts.map +1 -0
  103. package/dist/core/Sanitize.js +87 -0
  104. package/dist/core/Sanitize.js.map +1 -0
  105. package/dist/core/Save.d.ts +31 -7
  106. package/dist/core/Save.d.ts.map +1 -1
  107. package/dist/core/Save.js +56 -0
  108. package/dist/core/Save.js.map +1 -1
  109. package/dist/core/SaveSync.d.ts +26 -0
  110. package/dist/core/SaveSync.d.ts.map +1 -0
  111. package/dist/core/SaveSync.js +52 -0
  112. package/dist/core/SaveSync.js.map +1 -0
  113. package/dist/core/Scramble.d.ts +13 -0
  114. package/dist/core/Scramble.d.ts.map +1 -0
  115. package/dist/core/Scramble.js +44 -0
  116. package/dist/core/Scramble.js.map +1 -0
  117. package/dist/core/Spawner.d.ts +48 -0
  118. package/dist/core/Spawner.d.ts.map +1 -0
  119. package/dist/core/Spawner.js +65 -0
  120. package/dist/core/Spawner.js.map +1 -0
  121. package/dist/core/Tween.d.ts +25 -0
  122. package/dist/core/Tween.d.ts.map +1 -0
  123. package/dist/core/Tween.js +60 -0
  124. package/dist/core/Tween.js.map +1 -0
  125. package/dist/core/UndoHistory.d.ts +37 -0
  126. package/dist/core/UndoHistory.d.ts.map +1 -0
  127. package/dist/core/UndoHistory.js +49 -0
  128. package/dist/core/UndoHistory.js.map +1 -0
  129. package/dist/core/index.d.ts +20 -0
  130. package/dist/core/index.d.ts.map +1 -1
  131. package/dist/core/index.js +11 -0
  132. package/dist/core/index.js.map +1 -1
  133. package/dist/i18n/Fluent.d.ts +7 -0
  134. package/dist/i18n/Fluent.d.ts.map +1 -0
  135. package/dist/i18n/Fluent.js +86 -0
  136. package/dist/i18n/Fluent.js.map +1 -0
  137. package/dist/i18n/index.d.ts +15 -1
  138. package/dist/i18n/index.d.ts.map +1 -1
  139. package/dist/i18n/index.js +17 -2
  140. package/dist/i18n/index.js.map +1 -1
  141. package/dist/index.d.ts +1 -0
  142. package/dist/index.d.ts.map +1 -1
  143. package/dist/index.js +1 -0
  144. package/dist/index.js.map +1 -1
  145. package/dist/mw_games.global.js +351 -79
  146. package/dist/render/Camera.d.ts +29 -2
  147. package/dist/render/Camera.d.ts.map +1 -1
  148. package/dist/render/Camera.js +44 -13
  149. package/dist/render/Camera.js.map +1 -1
  150. package/dist/render/Capabilities.d.ts +51 -0
  151. package/dist/render/Capabilities.d.ts.map +1 -0
  152. package/dist/render/Capabilities.js +56 -0
  153. package/dist/render/Capabilities.js.map +1 -0
  154. package/dist/render/ColorBlindness.d.ts +16 -0
  155. package/dist/render/ColorBlindness.d.ts.map +1 -0
  156. package/dist/render/ColorBlindness.js +41 -0
  157. package/dist/render/ColorBlindness.js.map +1 -0
  158. package/dist/render/Minimap.d.ts +52 -0
  159. package/dist/render/Minimap.d.ts.map +1 -0
  160. package/dist/render/Minimap.js +95 -0
  161. package/dist/render/Minimap.js.map +1 -0
  162. package/dist/render/Viewport.d.ts +46 -0
  163. package/dist/render/Viewport.d.ts.map +1 -0
  164. package/dist/render/Viewport.js +52 -0
  165. package/dist/render/Viewport.js.map +1 -0
  166. package/dist/render/index.d.ts +9 -1
  167. package/dist/render/index.d.ts.map +1 -1
  168. package/dist/render/index.js +5 -1
  169. package/dist/render/index.js.map +1 -1
  170. package/dist/roguelike/Stealth.d.ts +34 -0
  171. package/dist/roguelike/Stealth.d.ts.map +1 -0
  172. package/dist/roguelike/Stealth.js +35 -0
  173. package/dist/roguelike/Stealth.js.map +1 -0
  174. package/dist/roguelike/index.d.ts +2 -0
  175. package/dist/roguelike/index.d.ts.map +1 -1
  176. package/dist/roguelike/index.js +1 -0
  177. package/dist/roguelike/index.js.map +1 -1
  178. package/dist/rpg/Collision.d.ts +42 -0
  179. package/dist/rpg/Collision.d.ts.map +1 -0
  180. package/dist/rpg/Collision.js +77 -0
  181. package/dist/rpg/Collision.js.map +1 -0
  182. package/dist/rpg/FreeMover.d.ts +45 -0
  183. package/dist/rpg/FreeMover.d.ts.map +1 -0
  184. package/dist/rpg/FreeMover.js +68 -0
  185. package/dist/rpg/FreeMover.js.map +1 -0
  186. package/dist/rpg/Marshal.d.ts +65 -0
  187. package/dist/rpg/Marshal.d.ts.map +1 -0
  188. package/dist/rpg/Marshal.js +393 -0
  189. package/dist/rpg/Marshal.js.map +1 -0
  190. package/dist/rpg/Quest.d.ts +41 -0
  191. package/dist/rpg/Quest.d.ts.map +1 -1
  192. package/dist/rpg/Quest.js +39 -6
  193. package/dist/rpg/Quest.js.map +1 -1
  194. package/dist/rpg/index.d.ts +7 -1
  195. package/dist/rpg/index.d.ts.map +1 -1
  196. package/dist/rpg/index.js +3 -0
  197. package/dist/rpg/index.js.map +1 -1
  198. package/dist/stage/DialogueStage.d.ts +1 -2
  199. package/dist/stage/DialogueStage.d.ts.map +1 -1
  200. package/dist/stage/DialogueStage.js +7 -22
  201. package/dist/stage/DialogueStage.js.map +1 -1
  202. package/dist/stage/index.d.ts +1 -1
  203. package/dist/stage/index.d.ts.map +1 -1
  204. package/dist/stage/script.d.ts +29 -0
  205. package/dist/stage/script.d.ts.map +1 -1
  206. package/dist/stage/script.js +0 -0
  207. package/dist/stage/script.js.map +1 -1
  208. package/dist/three-d/Character3D.d.ts +48 -0
  209. package/dist/three-d/Character3D.d.ts.map +1 -0
  210. package/dist/three-d/Character3D.js +96 -0
  211. package/dist/three-d/Character3D.js.map +1 -0
  212. package/dist/three-d/Engine3D.d.ts +23 -0
  213. package/dist/three-d/Engine3D.d.ts.map +1 -0
  214. package/dist/three-d/Engine3D.js +45 -0
  215. package/dist/three-d/Engine3D.js.map +1 -0
  216. package/dist/three-d/Grid.d.ts +14 -0
  217. package/dist/three-d/Grid.d.ts.map +1 -0
  218. package/dist/three-d/Grid.js +13 -0
  219. package/dist/three-d/Grid.js.map +1 -0
  220. package/dist/three-d/Heightmap.d.ts +35 -0
  221. package/dist/three-d/Heightmap.d.ts.map +1 -0
  222. package/dist/three-d/Heightmap.js +29 -0
  223. package/dist/three-d/Heightmap.js.map +1 -0
  224. package/dist/three-d/Models.d.ts +12 -0
  225. package/dist/three-d/Models.d.ts.map +1 -0
  226. package/dist/three-d/Models.js +14 -0
  227. package/dist/three-d/Models.js.map +1 -0
  228. package/dist/three-d/TileGrid3D.d.ts +21 -0
  229. package/dist/three-d/TileGrid3D.d.ts.map +1 -0
  230. package/dist/three-d/TileGrid3D.js +65 -0
  231. package/dist/three-d/TileGrid3D.js.map +1 -0
  232. package/dist/three-d/Vox.d.ts +24 -0
  233. package/dist/three-d/Vox.d.ts.map +1 -0
  234. package/dist/three-d/Vox.js +93 -0
  235. package/dist/three-d/Vox.js.map +1 -0
  236. package/dist/three-d/index.d.ts +13 -0
  237. package/dist/three-d/index.d.ts.map +1 -0
  238. package/dist/three-d/index.js +7 -0
  239. package/dist/three-d/index.js.map +1 -0
  240. package/dist/ui/Bar.d.ts +49 -0
  241. package/dist/ui/Bar.d.ts.map +1 -0
  242. package/dist/ui/Bar.js +70 -0
  243. package/dist/ui/Bar.js.map +1 -0
  244. package/dist/ui/BitmapLabel.d.ts +42 -0
  245. package/dist/ui/BitmapLabel.d.ts.map +1 -0
  246. package/dist/ui/BitmapLabel.js +59 -0
  247. package/dist/ui/BitmapLabel.js.map +1 -0
  248. package/dist/ui/Button.d.ts +52 -0
  249. package/dist/ui/Button.d.ts.map +1 -0
  250. package/dist/ui/Button.js +147 -0
  251. package/dist/ui/Button.js.map +1 -0
  252. package/dist/ui/FloatingText.d.ts +28 -0
  253. package/dist/ui/FloatingText.d.ts.map +1 -0
  254. package/dist/ui/FloatingText.js +39 -0
  255. package/dist/ui/FloatingText.js.map +1 -0
  256. package/dist/ui/HelpScreen.d.ts +29 -0
  257. package/dist/ui/HelpScreen.d.ts.map +1 -0
  258. package/dist/ui/HelpScreen.js +39 -0
  259. package/dist/ui/HelpScreen.js.map +1 -0
  260. package/dist/ui/IconGrid.d.ts +8 -0
  261. package/dist/ui/IconGrid.d.ts.map +1 -1
  262. package/dist/ui/IconGrid.js +27 -1
  263. package/dist/ui/IconGrid.js.map +1 -1
  264. package/dist/ui/Label.d.ts +9 -0
  265. package/dist/ui/Label.d.ts.map +1 -1
  266. package/dist/ui/Label.js +24 -1
  267. package/dist/ui/Label.js.map +1 -1
  268. package/dist/ui/ListView.d.ts +11 -0
  269. package/dist/ui/ListView.d.ts.map +1 -1
  270. package/dist/ui/ListView.js +55 -1
  271. package/dist/ui/ListView.js.map +1 -1
  272. package/dist/ui/LoadingScreen.d.ts +31 -0
  273. package/dist/ui/LoadingScreen.d.ts.map +1 -0
  274. package/dist/ui/LoadingScreen.js +60 -0
  275. package/dist/ui/LoadingScreen.js.map +1 -0
  276. package/dist/ui/MessageBox.d.ts +26 -0
  277. package/dist/ui/MessageBox.d.ts.map +1 -1
  278. package/dist/ui/MessageBox.js +62 -13
  279. package/dist/ui/MessageBox.js.map +1 -1
  280. package/dist/ui/RebindScreen.d.ts +55 -0
  281. package/dist/ui/RebindScreen.d.ts.map +1 -0
  282. package/dist/ui/RebindScreen.js +114 -0
  283. package/dist/ui/RebindScreen.js.map +1 -0
  284. package/dist/ui/Toast.d.ts +44 -0
  285. package/dist/ui/Toast.d.ts.map +1 -0
  286. package/dist/ui/Toast.js +89 -0
  287. package/dist/ui/Toast.js.map +1 -0
  288. package/dist/ui/VerticalLabel.d.ts +5 -0
  289. package/dist/ui/VerticalLabel.d.ts.map +1 -1
  290. package/dist/ui/VerticalLabel.js +18 -7
  291. package/dist/ui/VerticalLabel.js.map +1 -1
  292. package/dist/ui/Window.d.ts +6 -0
  293. package/dist/ui/Window.d.ts.map +1 -1
  294. package/dist/ui/Window.js +16 -1
  295. package/dist/ui/Window.js.map +1 -1
  296. package/dist/ui/WindowStack.d.ts +1 -0
  297. package/dist/ui/WindowStack.d.ts.map +1 -1
  298. package/dist/ui/WindowStack.js +4 -1
  299. package/dist/ui/WindowStack.js.map +1 -1
  300. package/dist/ui/index.d.ts +17 -1
  301. package/dist/ui/index.d.ts.map +1 -1
  302. package/dist/ui/index.js +9 -1
  303. package/dist/ui/index.js.map +1 -1
  304. package/dist/ui/theme.d.ts +21 -0
  305. package/dist/ui/theme.d.ts.map +1 -1
  306. package/dist/ui/theme.js +42 -0
  307. package/dist/ui/theme.js.map +1 -1
  308. package/dist/version.d.ts +13 -0
  309. package/dist/version.d.ts.map +1 -0
  310. package/dist/version.js +13 -0
  311. package/dist/version.js.map +1 -0
  312. package/dist/world/Environment.d.ts +35 -0
  313. package/dist/world/Environment.d.ts.map +1 -0
  314. package/dist/world/Environment.js +55 -0
  315. package/dist/world/Environment.js.map +1 -0
  316. package/dist/world/index.d.ts +2 -0
  317. package/dist/world/index.d.ts.map +1 -1
  318. package/dist/world/index.js +1 -0
  319. package/dist/world/index.js.map +1 -1
  320. package/package.json +40 -4
@@ -0,0 +1,102 @@
1
+ import { Signal } from './Signal.js';
2
+ /**
3
+ * Runs named, weighted asynchronous work while exposing truthful loading progress.
4
+ *
5
+ * Tasks run in order, which makes dependency ordering explicit and leaves a game free to
6
+ * start independent work concurrently inside one task. A task that cannot measure itself
7
+ * simply never calls `report`, so consumers can display an indeterminate current stage
8
+ * instead of inventing a percentage.
9
+ */
10
+ export class LoadQueue {
11
+ constructor() {
12
+ this.changed = new Signal();
13
+ this.tasks = [];
14
+ this.progress = new Map();
15
+ this.status_ = 'idle';
16
+ this.current_ = null;
17
+ this.error_ = null;
18
+ this.cancelled = false;
19
+ }
20
+ add(task) {
21
+ if (!task.id)
22
+ throw new Error('load task needs an id');
23
+ if (this.tasks.some((candidate) => candidate.id === task.id))
24
+ throw new Error(`duplicate load task "${task.id}"`);
25
+ if (task.weight !== undefined && !(task.weight > 0))
26
+ throw new Error('load task weight must be positive');
27
+ if (this.status_ === 'loading')
28
+ throw new Error('cannot add a task while loading');
29
+ this.tasks.push(task);
30
+ this.progress.set(task.id, 0);
31
+ this.emit();
32
+ return this;
33
+ }
34
+ get snapshot() {
35
+ const total = this.tasks.reduce((sum, task) => sum + (task.weight ?? 1), 0);
36
+ const complete = this.tasks.reduce((sum, task) => sum + (task.weight ?? 1) * (this.progress.get(task.id) ?? 0), 0);
37
+ return { status: this.status_, completed: complete, total, current: this.current_, error: this.error_ };
38
+ }
39
+ /** starts queued work and resolves only after every task succeeds */
40
+ async start() {
41
+ if (this.status_ === 'loading')
42
+ throw new Error('load queue is already running');
43
+ this.status_ = 'loading';
44
+ this.cancelled = false;
45
+ this.error_ = null;
46
+ this.emit();
47
+ try {
48
+ for (const task of this.tasks) {
49
+ if (this.cancelled)
50
+ break;
51
+ this.current_ = task.id;
52
+ this.emit();
53
+ const queue = this;
54
+ await task.run({
55
+ get cancelled() {
56
+ return queue.cancelled;
57
+ },
58
+ report: (fraction) => this.report(task.id, fraction),
59
+ });
60
+ if (!this.cancelled)
61
+ this.report(task.id, 1);
62
+ }
63
+ this.current_ = null;
64
+ this.status_ = this.cancelled ? 'cancelled' : 'ready';
65
+ this.emit();
66
+ }
67
+ catch (error) {
68
+ this.current_ = null;
69
+ this.error_ = error;
70
+ this.status_ = 'failed';
71
+ this.emit();
72
+ throw error;
73
+ }
74
+ }
75
+ /** asks the current task to stop; cancellation is cooperative */
76
+ cancel() {
77
+ if (this.status_ === 'loading')
78
+ this.cancelled = true;
79
+ }
80
+ /** clears failure/cancellation state and task progress for a new attempt */
81
+ retry() {
82
+ if (this.status_ === 'loading')
83
+ throw new Error('cannot retry while loading');
84
+ this.status_ = 'idle';
85
+ this.current_ = null;
86
+ this.error_ = null;
87
+ this.cancelled = false;
88
+ for (const task of this.tasks)
89
+ this.progress.set(task.id, 0);
90
+ this.emit();
91
+ }
92
+ report(id, fraction) {
93
+ if (!Number.isFinite(fraction))
94
+ throw new Error('load progress must be finite');
95
+ this.progress.set(id, Math.max(0, Math.min(1, fraction)));
96
+ this.emit();
97
+ }
98
+ emit() {
99
+ this.changed.dispatch(this.snapshot);
100
+ }
101
+ }
102
+ //# sourceMappingURL=Loading.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Loading.js","sourceRoot":"","sources":["../../src/core/Loading.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ import { type SaveStorage } from './Save.ts';
2
+ /**
3
+ * Fetches a game's own news/announcements feed - the inbound counterpart to
4
+ * `FeedbackClient`, following the same injectable-transport shape (a game's own HTTPS
5
+ * endpoint, no backend shipped or assumed) rather than a second pattern.
6
+ */
7
+ export interface NewsItem {
8
+ id: string;
9
+ title: string;
10
+ body: string;
11
+ publishedAt?: number;
12
+ }
13
+ export interface NewsOptions {
14
+ endpoint: string;
15
+ timeoutMs?: number;
16
+ fetch?: typeof globalThis.fetch;
17
+ }
18
+ export declare class NewsClient {
19
+ private readonly endpoint;
20
+ private readonly timeoutMs;
21
+ private readonly fetchFn;
22
+ constructor(options: NewsOptions);
23
+ /**
24
+ * Fetches and validates the feed; a malformed item throws rather than reaching a game as
25
+ * trusted data. The response is read as text and run through `sanitizeInboundText`
26
+ * (a size cap, no embedded control characters) before `JSON.parse` ever sees it, ahead of
27
+ * `normalizeItem`'s own per-field shape check below.
28
+ */
29
+ fetchItems(): Promise<NewsItem[]>;
30
+ }
31
+ export interface NewsSeenOptions {
32
+ namespace: string;
33
+ storage?: SaveStorage;
34
+ }
35
+ /** which news items a player has already dismissed, so a returning player is not shown the same announcement forever */
36
+ export declare class NewsSeenTracker {
37
+ private readonly storage;
38
+ private readonly key;
39
+ constructor(options: NewsSeenOptions);
40
+ private readSeen;
41
+ isSeen(id: string): boolean;
42
+ markSeen(id: string): void;
43
+ /** `items` filtered down to whichever have not been marked seen yet */
44
+ unseen(items: readonly NewsItem[]): NewsItem[];
45
+ }
46
+ //# sourceMappingURL=News.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"News.d.ts","sourceRoot":"","sources":["../../src/core/News.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AAG7D;;;;GAIG;AACH,MAAM,WAAW,QAAQ;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;CAChC;AAED,qBAAa,UAAU;IACtB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0B;IAElD,YAAY,OAAO,EAAE,WAAW,EAO/B;IAED;;;;;OAKG;IACG,UAAU,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC,CAYtC;CACD;AAgBD,MAAM,WAAW,eAAe;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,wHAAwH;AACxH,qBAAa,eAAe;IAC3B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAc;IACtC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAE7B,YAAY,OAAO,EAAE,eAAe,EAGnC;IAED,OAAO,CAAC,QAAQ;IAKhB,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAE1B;IAED,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAIzB;IAED,uEAAuE;IACvE,MAAM,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,GAAG,QAAQ,EAAE,CAG7C;CACD"}
@@ -0,0 +1,79 @@
1
+ import { defaultStorage } from './Save.js';
2
+ import { sanitizeInboundText } from './Sanitize.js';
3
+ export class NewsClient {
4
+ constructor(options) {
5
+ if (!options.endpoint)
6
+ throw new Error('news endpoint is required');
7
+ this.endpoint = options.endpoint;
8
+ this.timeoutMs = options.timeoutMs ?? 10000;
9
+ if (!(this.timeoutMs > 0))
10
+ throw new Error('news timeout must be positive');
11
+ this.fetchFn = options.fetch ?? globalThis.fetch;
12
+ if (!this.fetchFn)
13
+ throw new Error('fetch is unavailable; provide NewsOptions.fetch');
14
+ }
15
+ /**
16
+ * Fetches and validates the feed; a malformed item throws rather than reaching a game as
17
+ * trusted data. The response is read as text and run through `sanitizeInboundText`
18
+ * (a size cap, no embedded control characters) before `JSON.parse` ever sees it, ahead of
19
+ * `normalizeItem`'s own per-field shape check below.
20
+ */
21
+ async fetchItems() {
22
+ const controller = new AbortController();
23
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
24
+ try {
25
+ const response = await this.fetchFn(this.endpoint, { signal: controller.signal });
26
+ if (!response.ok)
27
+ throw new Error(`news request failed with HTTP ${response.status}`);
28
+ const data = JSON.parse(sanitizeInboundText(await response.text()));
29
+ if (!Array.isArray(data))
30
+ throw new Error('news response was not an array');
31
+ return data.map((raw, index) => normalizeItem(raw, index));
32
+ }
33
+ finally {
34
+ clearTimeout(timer);
35
+ }
36
+ }
37
+ }
38
+ function normalizeItem(raw, index) {
39
+ if (typeof raw !== 'object' || raw === null)
40
+ throw new Error(`news item ${index} was not an object`);
41
+ const item = raw;
42
+ if (typeof item.id !== 'string' || !item.id)
43
+ throw new Error(`news item ${index} is missing a string id`);
44
+ if (typeof item.title !== 'string')
45
+ throw new Error(`news item ${index} is missing a string title`);
46
+ if (typeof item.body !== 'string')
47
+ throw new Error(`news item ${index} is missing a string body`);
48
+ return {
49
+ id: item.id,
50
+ title: item.title,
51
+ body: item.body,
52
+ publishedAt: typeof item.publishedAt === 'number' ? item.publishedAt : undefined,
53
+ };
54
+ }
55
+ /** which news items a player has already dismissed, so a returning player is not shown the same announcement forever */
56
+ export class NewsSeenTracker {
57
+ constructor(options) {
58
+ this.storage = options.storage ?? defaultStorage();
59
+ this.key = `mwg-news-seen:${options.namespace}`;
60
+ }
61
+ readSeen() {
62
+ const raw = this.storage.read(this.key);
63
+ return new Set(raw ? JSON.parse(raw) : []);
64
+ }
65
+ isSeen(id) {
66
+ return this.readSeen().has(id);
67
+ }
68
+ markSeen(id) {
69
+ const seen = this.readSeen();
70
+ seen.add(id);
71
+ this.storage.write(this.key, JSON.stringify([...seen]));
72
+ }
73
+ /** `items` filtered down to whichever have not been marked seen yet */
74
+ unseen(items) {
75
+ const seen = this.readSeen();
76
+ return items.filter((item) => !seen.has(item.id));
77
+ }
78
+ }
79
+ //# sourceMappingURL=News.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"News.js","sourceRoot":"","sources":["../../src/core/News.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import type { Action } from './Input.ts';
2
+ export interface PlayerInputOptions {
3
+ /** this player's own gamepad, when they have one; omit for a keyboard-only player */
4
+ padIndex?: number;
5
+ }
6
+ /**
7
+ * A player-scoped view over `mwg/core`'s `Input` module for same-screen local multiplayer.
8
+ *
9
+ * `Input` has no concept of a player: `Action` is a bare string, and its `bindings`/`held`/
10
+ * `onAction` all live at module scope. Binding the same action name to two pads directly -
11
+ * `Input.bindButton('confirm', 0, [0])` and `Input.bindButton('confirm', 1, [0])` - does not
12
+ * give two independent "confirm"s; both bindings land in the very same action's key set, so
13
+ * either pad pressing it fires the one shared `onAction`, with no way to tell which pad did.
14
+ *
15
+ * This does not change `Input` itself - it is a thin, purely additive naming convention:
16
+ * every action a `PlayerInput` binds or queries is transparently prefixed with this
17
+ * player's own `id`, so two players' "confirm" become two distinct actions
18
+ * (`"p1:confirm"`, `"p2:confirm"`) that never collide. A single-player game using `Input`'s
19
+ * bare action names directly is entirely unaffected.
20
+ */
21
+ export declare class PlayerInput {
22
+ readonly id: string;
23
+ readonly padIndex?: number;
24
+ constructor(id: string, options?: PlayerInputOptions);
25
+ private scoped;
26
+ /** binds this player's own action to keyboard keys, independent of any other player's binding for the same action name */
27
+ bind(action: Action, keys: readonly string[]): void;
28
+ /** binds this player's own action to a button on their own `padIndex` */
29
+ bindButton(action: Action, buttons: readonly number[]): void;
30
+ /** binds this player's own action to an axis direction on their own `padIndex` */
31
+ bindAxis(action: Action, axis: number, direction: 1 | -1): void;
32
+ isDown(action: Action): boolean;
33
+ justPressed(action: Action): boolean;
34
+ justReleased(action: Action): boolean;
35
+ /** every key/pad binding for this player's own action, in the same shape `Input.exportBindings` uses */
36
+ keysFor(action: Action): string[];
37
+ }
38
+ //# sourceMappingURL=PlayerInput.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PlayerInput.d.ts","sourceRoot":"","sources":["../../src/core/PlayerInput.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEzC,MAAM,WAAW,kBAAkB;IAClC,qFAAqF;IACrF,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;;GAcG;AACH,qBAAa,WAAW;IACvB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAE3B,YAAY,EAAE,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB,EAIvD;IAED,OAAO,CAAC,MAAM;IAId,0HAA0H;IAC1H,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAElD;IAED,yEAAyE;IACzE,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAG3D;IAED,kFAAkF;IAClF,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAG9D;IAED,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAE9B;IAED,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEnC;IAED,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEpC;IAED,wGAAwG;IACxG,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAEhC;CACD"}
@@ -0,0 +1,57 @@
1
+ import * as Input from './Input.js';
2
+ /**
3
+ * A player-scoped view over `mwg/core`'s `Input` module for same-screen local multiplayer.
4
+ *
5
+ * `Input` has no concept of a player: `Action` is a bare string, and its `bindings`/`held`/
6
+ * `onAction` all live at module scope. Binding the same action name to two pads directly -
7
+ * `Input.bindButton('confirm', 0, [0])` and `Input.bindButton('confirm', 1, [0])` - does not
8
+ * give two independent "confirm"s; both bindings land in the very same action's key set, so
9
+ * either pad pressing it fires the one shared `onAction`, with no way to tell which pad did.
10
+ *
11
+ * This does not change `Input` itself - it is a thin, purely additive naming convention:
12
+ * every action a `PlayerInput` binds or queries is transparently prefixed with this
13
+ * player's own `id`, so two players' "confirm" become two distinct actions
14
+ * (`"p1:confirm"`, `"p2:confirm"`) that never collide. A single-player game using `Input`'s
15
+ * bare action names directly is entirely unaffected.
16
+ */
17
+ export class PlayerInput {
18
+ constructor(id, options = {}) {
19
+ if (!id)
20
+ throw new Error('a player needs a non-empty id');
21
+ this.id = id;
22
+ this.padIndex = options.padIndex;
23
+ }
24
+ scoped(action) {
25
+ return `${this.id}:${action}`;
26
+ }
27
+ /** binds this player's own action to keyboard keys, independent of any other player's binding for the same action name */
28
+ bind(action, keys) {
29
+ Input.bind(this.scoped(action), keys);
30
+ }
31
+ /** binds this player's own action to a button on their own `padIndex` */
32
+ bindButton(action, buttons) {
33
+ if (this.padIndex === undefined)
34
+ throw new Error(`player "${this.id}" has no padIndex to bind a gamepad button to`);
35
+ Input.bindButton(this.scoped(action), this.padIndex, buttons);
36
+ }
37
+ /** binds this player's own action to an axis direction on their own `padIndex` */
38
+ bindAxis(action, axis, direction) {
39
+ if (this.padIndex === undefined)
40
+ throw new Error(`player "${this.id}" has no padIndex to bind a gamepad axis to`);
41
+ Input.bindAxis(this.scoped(action), this.padIndex, axis, direction);
42
+ }
43
+ isDown(action) {
44
+ return Input.isDown(this.scoped(action));
45
+ }
46
+ justPressed(action) {
47
+ return Input.justPressed(this.scoped(action));
48
+ }
49
+ justReleased(action) {
50
+ return Input.justReleased(this.scoped(action));
51
+ }
52
+ /** every key/pad binding for this player's own action, in the same shape `Input.exportBindings` uses */
53
+ keysFor(action) {
54
+ return Input.keysFor(this.scoped(action));
55
+ }
56
+ }
57
+ //# sourceMappingURL=PlayerInput.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PlayerInput.js","sourceRoot":"","sources":["../../src/core/PlayerInput.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,YAAY,CAAC;AAQpC;;;;;;;;;;;;;;GAcG;AACH,MAAM,OAAO,WAAW;IAIvB,YAAY,EAAU,EAAE,OAAO,GAAuB,EAAE;QACvD,IAAI,CAAC,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;QAC1D,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;IAClC,CAAC;IAEO,MAAM,CAAC,MAAc;QAC5B,OAAO,GAAG,IAAI,CAAC,EAAE,IAAI,MAAM,EAAE,CAAC;IAC/B,CAAC;IAED,0HAA0H;IAC1H,IAAI,CAAC,MAAc,EAAE,IAAuB;QAC3C,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC;IACvC,CAAC;IAED,yEAAyE;IACzE,UAAU,CAAC,MAAc,EAAE,OAA0B;QACpD,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,WAAW,IAAI,CAAC,EAAE,+CAA+C,CAAC,CAAC;QACpH,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAC/D,CAAC;IAED,kFAAkF;IAClF,QAAQ,CAAC,MAAc,EAAE,IAAY,EAAE,SAAiB;QACvD,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,WAAW,IAAI,CAAC,EAAE,6CAA6C,CAAC,CAAC;QAClH,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC;IACrE,CAAC;IAED,MAAM,CAAC,MAAc;QACpB,OAAO,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IAC1C,CAAC;IAED,WAAW,CAAC,MAAc;QACzB,OAAO,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/C,CAAC;IAED,YAAY,CAAC,MAAc;QAC1B,OAAO,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IAChD,CAAC;IAED,wGAAwG;IACxG,OAAO,CAAC,MAAc;QACrB,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IAC3C,CAAC;CACD"}
@@ -0,0 +1,43 @@
1
+ import { type SaveStorage } from './Save.ts';
2
+ /**
3
+ * A personal, local record of completed runs - what `core.Session` (launch count) and
4
+ * `core.Achievements` (derived unlocks, no per-run record) don't keep, and distinct from
5
+ * `SaveSystem.list()`, which enumerates *continuable* slots, not runs already over.
6
+ *
7
+ * `mwg` stays agnostic about what a run's own report actually contains, the same way
8
+ * `SaveSystem` stays agnostic about what "the save" contains: a game supplies its own
9
+ * `summary` shape (score, cause of death, floor reached, whatever it considers a run's own
10
+ * stats) and this only ever stores, lists, and sorts it.
11
+ */
12
+ export interface RunHistoryEntry<T> {
13
+ id: string;
14
+ endedAt: number;
15
+ summary: T;
16
+ }
17
+ export interface RunHistoryOptions {
18
+ /** namespaces the stored history, so two games sharing an origin never collide */
19
+ namespace: string;
20
+ storage?: SaveStorage;
21
+ /** oldest runs are dropped once this many are stored; omit for unlimited */
22
+ limit?: number;
23
+ }
24
+ export declare class RunHistory<T> {
25
+ private readonly storage;
26
+ private readonly limit?;
27
+ private readonly key;
28
+ constructor(options: RunHistoryOptions);
29
+ private readAll;
30
+ /** appends a completed run's summary, oldest first; drops the oldest entry once `limit` is exceeded */
31
+ record(summary: T): RunHistoryEntry<T>;
32
+ /** every recorded run, oldest first */
33
+ all(): readonly RunHistoryEntry<T>[];
34
+ /**
35
+ * Every recorded run sorted by a field of its own summary - explicitly a local, personal
36
+ * ranking over this player's own history, not a networked leaderboard comparing players
37
+ * against each other, which `mwg` does not attempt here.
38
+ */
39
+ ranked(by: (summary: T) => number, order?: 'asc' | 'desc'): readonly RunHistoryEntry<T>[];
40
+ /** drops every recorded run */
41
+ clear(): void;
42
+ }
43
+ //# sourceMappingURL=RunHistory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RunHistory.d.ts","sourceRoot":"","sources":["../../src/core/RunHistory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AAE7D;;;;;;;;;GASG;AACH,MAAM,WAAW,eAAe,CAAC,CAAC;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,CAAC,CAAC;CACX;AAED,MAAM,WAAW,iBAAiB;IACjC,kFAAkF;IAClF,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,4EAA4E;IAC5E,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,UAAU,CAAC,CAAC;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAc;IACtC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAE7B,YAAY,OAAO,EAAE,iBAAiB,EAIrC;IAED,OAAO,CAAC,OAAO;IAKf,uGAAuG;IACvG,MAAM,CAAC,OAAO,EAAE,CAAC,GAAG,eAAe,CAAC,CAAC,CAAC,CAOrC;IAED,uCAAuC;IACvC,GAAG,IAAI,SAAS,eAAe,CAAC,CAAC,CAAC,EAAE,CAEnC;IAED;;;;OAIG;IACH,MAAM,CAAC,EAAE,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,MAAM,EAAE,KAAK,GAAE,KAAK,GAAG,MAAe,GAAG,SAAS,eAAe,CAAC,CAAC,CAAC,EAAE,CAIhG;IAED,+BAA+B;IAC/B,KAAK,IAAI,IAAI,CAEZ;CACD"}
@@ -0,0 +1,41 @@
1
+ import { defaultStorage } from './Save.js';
2
+ export class RunHistory {
3
+ constructor(options) {
4
+ this.storage = options.storage ?? defaultStorage();
5
+ this.limit = options.limit;
6
+ this.key = `mwg-runs:${options.namespace}`;
7
+ }
8
+ readAll() {
9
+ const raw = this.storage.read(this.key);
10
+ return raw ? JSON.parse(raw) : [];
11
+ }
12
+ /** appends a completed run's summary, oldest first; drops the oldest entry once `limit` is exceeded */
13
+ record(summary) {
14
+ const entries = this.readAll();
15
+ const entry = { id: `${Date.now()}-${entries.length}`, endedAt: Date.now(), summary };
16
+ entries.push(entry);
17
+ if (this.limit !== undefined && entries.length > this.limit)
18
+ entries.splice(0, entries.length - this.limit);
19
+ this.storage.write(this.key, JSON.stringify(entries));
20
+ return entry;
21
+ }
22
+ /** every recorded run, oldest first */
23
+ all() {
24
+ return this.readAll();
25
+ }
26
+ /**
27
+ * Every recorded run sorted by a field of its own summary - explicitly a local, personal
28
+ * ranking over this player's own history, not a networked leaderboard comparing players
29
+ * against each other, which `mwg` does not attempt here.
30
+ */
31
+ ranked(by, order = 'desc') {
32
+ const entries = this.readAll();
33
+ const sign = order === 'desc' ? -1 : 1;
34
+ return entries.sort((a, b) => sign * (by(a.summary) - by(b.summary)));
35
+ }
36
+ /** drops every recorded run */
37
+ clear() {
38
+ this.storage.remove(this.key);
39
+ }
40
+ }
41
+ //# sourceMappingURL=RunHistory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RunHistory.js","sourceRoot":"","sources":["../../src/core/RunHistory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAoB,MAAM,WAAW,CAAC;AA0B7D,MAAM,OAAO,UAAU;IAKtB,YAAY,OAA0B;QACrC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,cAAc,EAAE,CAAC;QACnD,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;QAC3B,IAAI,CAAC,GAAG,GAAG,YAAY,OAAO,CAAC,SAAS,EAAE,CAAC;IAC5C,CAAC;IAEO,OAAO;QACd,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACxC,OAAO,GAAG,CAAC,CAAC,CAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAA0B,CAAC,CAAC,CAAC,EAAE,CAAC;IAC7D,CAAC;IAED,uGAAuG;IACvG,MAAM,CAAC,OAAU;QAChB,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC;QAC/B,MAAM,KAAK,GAAuB,EAAE,EAAE,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,OAAO,CAAC,MAAM,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,CAAC;QAC1G,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACpB,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK;YAAE,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;QAC5G,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC;QACtD,OAAO,KAAK,CAAC;IACd,CAAC;IAED,uCAAuC;IACvC,GAAG;QACF,OAAO,IAAI,CAAC,OAAO,EAAE,CAAC;IACvB,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,EAA0B,EAAE,KAAK,GAAmB,MAAM;QAChE,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC;QAC/B,MAAM,IAAI,GAAG,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACvC,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IACvE,CAAC;IAED,+BAA+B;IAC/B,KAAK;QACJ,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC/B,CAAC;CACD"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Cheap, structural checks over inbound data - a save imported from another device or
3
+ * server (`SaveSystem.importSlot`), a news feed response (`NewsClient`), an external save
4
+ * format (`rpg.decodeMarshal`) - run before any of it is parsed or handed to a game as
5
+ * trusted state. Deliberately narrow: a size cap and a control-character check catch a
6
+ * truncated, corrupted, or hostile payload cheaply, ahead of and distinct from validating
7
+ * that a parsed value's own fields have the shape a game actually expects (`validateSchema`,
8
+ * below), which is a separate, deeper pass over already-parsed data.
9
+ */
10
+ export interface SizeLimitOptions {
11
+ /** bytes; defaults to 10 MB */
12
+ maxBytes?: number;
13
+ }
14
+ /** throws once `data`'s size (UTF-8 byte length for a string, byte length for a buffer) exceeds the cap */
15
+ export declare function checkSize(data: string | Uint8Array, options?: SizeLimitOptions): void;
16
+ /**
17
+ * Throws on an embedded NUL byte or a control character outside ordinary whitespace
18
+ * (tab, newline, carriage return) - the kind of byte a well-formed save or news response
19
+ * never legitimately contains, and a cheap tell that a payload was truncated, corrupted, or
20
+ * deliberately malformed before it ever reaches `JSON.parse`.
21
+ */
22
+ export declare function checkNoControlCharacters(text: string): void;
23
+ /** `checkSize` then `checkNoControlCharacters` - the minimum bar before any inbound text is parsed */
24
+ export declare function sanitizeInboundText(text: string, options?: SizeLimitOptions): string;
25
+ /**
26
+ * A small schema for validating an already-parsed value's shape - deliberately not a full
27
+ * JSON Schema implementation, only what `SaveSystem`/`NewsClient`-shaped data actually needs:
28
+ * primitives, arrays, and objects, with `__proto__`/`constructor`/`prototype` keys always
29
+ * rejected regardless of what a schema itself asks for, since no legitimate save or feed
30
+ * payload ever needs to set one and a parsed JSON object having one is exactly the
31
+ * prototype-pollution shape this exists to catch.
32
+ */
33
+ export type Schema = {
34
+ type: 'string';
35
+ optional?: boolean;
36
+ } | {
37
+ type: 'number';
38
+ optional?: boolean;
39
+ min?: number;
40
+ max?: number;
41
+ } | {
42
+ type: 'boolean';
43
+ optional?: boolean;
44
+ } | {
45
+ type: 'object';
46
+ optional?: boolean;
47
+ fields: Record<string, Schema>;
48
+ } | {
49
+ type: 'array';
50
+ optional?: boolean;
51
+ items: Schema;
52
+ maxLength?: number;
53
+ };
54
+ /** throws with a path-qualified message at the first field that does not match `schema` */
55
+ export declare function validateSchema(value: unknown, schema: Schema, path?: string): void;
56
+ //# sourceMappingURL=Sanitize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Sanitize.d.ts","sourceRoot":"","sources":["../../src/core/Sanitize.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,gBAAgB;IAChC,+BAA+B;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAID,2GAA2G;AAC3G,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,EAAE,OAAO,GAAE,gBAAqB,GAAG,IAAI,CAIzF;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAQ3D;AAED,sGAAsG;AACtG,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM,CAIxF;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,MAAM,GACf;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,GACtC;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAClE;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,GACvC;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GACtE;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAI5E,2FAA2F;AAC3F,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,SAAM,GAAG,IAAI,CAoC/E"}
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Cheap, structural checks over inbound data - a save imported from another device or
3
+ * server (`SaveSystem.importSlot`), a news feed response (`NewsClient`), an external save
4
+ * format (`rpg.decodeMarshal`) - run before any of it is parsed or handed to a game as
5
+ * trusted state. Deliberately narrow: a size cap and a control-character check catch a
6
+ * truncated, corrupted, or hostile payload cheaply, ahead of and distinct from validating
7
+ * that a parsed value's own fields have the shape a game actually expects (`validateSchema`,
8
+ * below), which is a separate, deeper pass over already-parsed data.
9
+ */
10
+ const DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
11
+ /** throws once `data`'s size (UTF-8 byte length for a string, byte length for a buffer) exceeds the cap */
12
+ export function checkSize(data, options = {}) {
13
+ const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
14
+ const size = typeof data === 'string' ? new TextEncoder().encode(data).length : data.length;
15
+ if (size > maxBytes)
16
+ throw new Error(`inbound data exceeds the ${maxBytes}-byte limit (${size} bytes)`);
17
+ }
18
+ /**
19
+ * Throws on an embedded NUL byte or a control character outside ordinary whitespace
20
+ * (tab, newline, carriage return) - the kind of byte a well-formed save or news response
21
+ * never legitimately contains, and a cheap tell that a payload was truncated, corrupted, or
22
+ * deliberately malformed before it ever reaches `JSON.parse`.
23
+ */
24
+ export function checkNoControlCharacters(text) {
25
+ for (let i = 0; i < text.length; i++) {
26
+ const code = text.charCodeAt(i);
27
+ if (code === 0x09 || code === 0x0a || code === 0x0d)
28
+ continue; //tab, LF, CR
29
+ if (code <= 0x1f || code === 0x7f) {
30
+ throw new Error(`inbound data contains a control character (0x${code.toString(16).padStart(2, '0')}) at index ${i}`);
31
+ }
32
+ }
33
+ }
34
+ /** `checkSize` then `checkNoControlCharacters` - the minimum bar before any inbound text is parsed */
35
+ export function sanitizeInboundText(text, options = {}) {
36
+ checkSize(text, options);
37
+ checkNoControlCharacters(text);
38
+ return text;
39
+ }
40
+ const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
41
+ /** throws with a path-qualified message at the first field that does not match `schema` */
42
+ export function validateSchema(value, schema, path = '$') {
43
+ if (value === undefined) {
44
+ if (schema.optional)
45
+ return;
46
+ throw new Error(`${path} is required`);
47
+ }
48
+ switch (schema.type) {
49
+ case 'string':
50
+ if (typeof value !== 'string')
51
+ throw new Error(`${path} must be a string`);
52
+ return;
53
+ case 'number':
54
+ if (typeof value !== 'number' || !Number.isFinite(value))
55
+ throw new Error(`${path} must be a finite number`);
56
+ if (schema.min !== undefined && value < schema.min)
57
+ throw new Error(`${path} must be >= ${schema.min}`);
58
+ if (schema.max !== undefined && value > schema.max)
59
+ throw new Error(`${path} must be <= ${schema.max}`);
60
+ return;
61
+ case 'boolean':
62
+ if (typeof value !== 'boolean')
63
+ throw new Error(`${path} must be a boolean`);
64
+ return;
65
+ case 'array':
66
+ if (!Array.isArray(value))
67
+ throw new Error(`${path} must be an array`);
68
+ if (schema.maxLength !== undefined && value.length > schema.maxLength) {
69
+ throw new Error(`${path} exceeds its maximum length of ${schema.maxLength}`);
70
+ }
71
+ value.forEach((item, index) => validateSchema(item, schema.items, `${path}[${index}]`));
72
+ return;
73
+ case 'object': {
74
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
75
+ throw new Error(`${path} must be an object`);
76
+ for (const key of Object.keys(value)) {
77
+ if (FORBIDDEN_KEYS.has(key))
78
+ throw new Error(`${path} contains a forbidden key "${key}"`);
79
+ }
80
+ for (const [key, fieldSchema] of Object.entries(schema.fields)) {
81
+ validateSchema(value[key], fieldSchema, `${path}.${key}`);
82
+ }
83
+ return;
84
+ }
85
+ }
86
+ }
87
+ //# sourceMappingURL=Sanitize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Sanitize.js","sourceRoot":"","sources":["../../src/core/Sanitize.ts"],"names":[],"mappings":"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"}