@datamoc/mw_games 0.7.6 → 0.7.7
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.
- package/CHANGELOG.md +270 -0
- package/dist/actors/Inventory.d.ts +7 -1
- package/dist/actors/Inventory.d.ts.map +1 -1
- package/dist/actors/Inventory.js.map +1 -1
- package/dist/actors/craft.d.ts +35 -14
- package/dist/actors/craft.d.ts.map +1 -1
- package/dist/actors/craft.js +56 -24
- package/dist/actors/craft.js.map +1 -1
- package/dist/actors/index.d.ts +1 -1
- package/dist/actors/index.d.ts.map +1 -1
- package/dist/ai/Heuristics.d.ts +232 -0
- package/dist/ai/Heuristics.d.ts.map +1 -0
- package/dist/ai/Heuristics.js +298 -0
- package/dist/ai/Heuristics.js.map +1 -0
- package/dist/ai/index.d.ts +4 -0
- package/dist/ai/index.d.ts.map +1 -1
- package/dist/ai/index.js +2 -0
- package/dist/ai/index.js.map +1 -1
- package/dist/ai/score.d.ts +126 -0
- package/dist/ai/score.d.ts.map +1 -0
- package/dist/ai/score.js +121 -0
- package/dist/ai/score.js.map +1 -0
- package/dist/assets/loader.d.ts +18 -4
- package/dist/assets/loader.d.ts.map +1 -1
- package/dist/assets/loader.js +40 -11
- package/dist/assets/loader.js.map +1 -1
- package/dist/audio/Positional.d.ts +125 -0
- package/dist/audio/Positional.d.ts.map +1 -0
- package/dist/audio/Positional.js +135 -0
- package/dist/audio/Positional.js.map +1 -0
- package/dist/audio/Sound.d.ts +7 -1
- package/dist/audio/Sound.d.ts.map +1 -1
- package/dist/audio/Sound.js +8 -2
- package/dist/audio/Sound.js.map +1 -1
- package/dist/audio/index.d.ts +2 -0
- package/dist/audio/index.d.ts.map +1 -1
- package/dist/audio/index.js +1 -0
- package/dist/audio/index.js.map +1 -1
- package/dist/battle/AttackDialog.d.ts +75 -0
- package/dist/battle/AttackDialog.d.ts.map +1 -0
- package/dist/battle/AttackDialog.js +115 -0
- package/dist/battle/AttackDialog.js.map +1 -0
- package/dist/battle/AttackPreview.d.ts +70 -0
- package/dist/battle/AttackPreview.d.ts.map +1 -0
- package/dist/battle/AttackPreview.js +78 -0
- package/dist/battle/AttackPreview.js.map +1 -0
- package/dist/battle/BattleStats.d.ts +45 -0
- package/dist/battle/BattleStats.d.ts.map +1 -0
- package/dist/battle/BattleStats.js +63 -0
- package/dist/battle/BattleStats.js.map +1 -0
- package/dist/battle/UnitSelector.d.ts +72 -0
- package/dist/battle/UnitSelector.d.ts.map +1 -0
- package/dist/battle/UnitSelector.js +117 -0
- package/dist/battle/UnitSelector.js.map +1 -0
- package/dist/battle/Whiteboard.d.ts +50 -0
- package/dist/battle/Whiteboard.d.ts.map +1 -0
- package/dist/battle/Whiteboard.js +94 -0
- package/dist/battle/Whiteboard.js.map +1 -0
- package/dist/battle/index.d.ts +10 -0
- package/dist/battle/index.d.ts.map +1 -1
- package/dist/battle/index.js +5 -0
- package/dist/battle/index.js.map +1 -1
- package/dist/board/FogOfWar.d.ts +32 -2
- package/dist/board/FogOfWar.d.ts.map +1 -1
- package/dist/board/FogOfWar.js +70 -6
- package/dist/board/FogOfWar.js.map +1 -1
- package/dist/core/Achievements.d.ts +40 -7
- package/dist/core/Achievements.d.ts.map +1 -1
- package/dist/core/Achievements.js +51 -7
- package/dist/core/Achievements.js.map +1 -1
- package/dist/core/Hex.d.ts +26 -6
- package/dist/core/Hex.d.ts.map +1 -1
- package/dist/core/Hex.js +40 -13
- package/dist/core/Hex.js.map +1 -1
- package/dist/core/Input.d.ts +36 -0
- package/dist/core/Input.d.ts.map +1 -1
- package/dist/core/Input.js +63 -1
- package/dist/core/Input.js.map +1 -1
- package/dist/core/Random.d.ts +93 -0
- package/dist/core/Random.d.ts.map +1 -1
- package/dist/core/Random.js +177 -0
- package/dist/core/Random.js.map +1 -1
- package/dist/core/SyncGuard.d.ts +57 -0
- package/dist/core/SyncGuard.d.ts.map +1 -0
- package/dist/core/SyncGuard.js +116 -0
- package/dist/core/SyncGuard.js.map +1 -0
- package/dist/core/index.d.ts +3 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -1
- package/dist/core/index.js.map +1 -1
- package/dist/i18n/Po.d.ts +51 -0
- package/dist/i18n/Po.d.ts.map +1 -0
- package/dist/i18n/Po.js +176 -0
- package/dist/i18n/Po.js.map +1 -0
- package/dist/i18n/index.d.ts +2 -0
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/i18n/index.js +1 -0
- package/dist/i18n/index.js.map +1 -1
- package/dist/mw_games.global.js +104 -98
- package/dist/mwl/MapFile.d.ts +41 -0
- package/dist/mwl/MapFile.d.ts.map +1 -0
- package/dist/mwl/MapFile.js +43 -0
- package/dist/mwl/MapFile.js.map +1 -0
- package/dist/mwl/campaign.d.ts +51 -0
- package/dist/mwl/campaign.d.ts.map +1 -0
- package/dist/mwl/campaign.js +58 -0
- package/dist/mwl/campaign.js.map +1 -0
- package/dist/mwl/carryover.d.ts +79 -0
- package/dist/mwl/carryover.d.ts.map +1 -0
- package/dist/mwl/carryover.js +63 -0
- package/dist/mwl/carryover.js.map +1 -0
- package/dist/mwl/content.d.ts +10 -0
- package/dist/mwl/content.d.ts.map +1 -1
- package/dist/mwl/content.js +8 -0
- package/dist/mwl/content.js.map +1 -1
- package/dist/mwl/hooks.d.ts +6 -5
- package/dist/mwl/hooks.d.ts.map +1 -1
- package/dist/mwl/hooks.js.map +1 -1
- package/dist/mwl/index.d.ts +8 -2
- package/dist/mwl/index.d.ts.map +1 -1
- package/dist/mwl/index.js +4 -1
- package/dist/mwl/index.js.map +1 -1
- package/dist/mwl/runtime.d.ts +140 -7
- package/dist/mwl/runtime.d.ts.map +1 -1
- package/dist/mwl/runtime.js +427 -33
- package/dist/mwl/runtime.js.map +1 -1
- package/dist/mwl/schema.d.ts.map +1 -1
- package/dist/mwl/schema.js +97 -18
- package/dist/mwl/schema.js.map +1 -1
- package/dist/roguelike/MultiTurnBeam.d.ts +50 -12
- package/dist/roguelike/MultiTurnBeam.d.ts.map +1 -1
- package/dist/roguelike/MultiTurnBeam.js +100 -53
- package/dist/roguelike/MultiTurnBeam.js.map +1 -1
- package/dist/roguelike/Targeting.d.ts +94 -0
- package/dist/roguelike/Targeting.d.ts.map +1 -1
- package/dist/roguelike/Targeting.js +123 -0
- package/dist/roguelike/Targeting.js.map +1 -1
- package/dist/roguelike/index.d.ts +3 -1
- package/dist/roguelike/index.d.ts.map +1 -1
- package/dist/roguelike/index.js +1 -0
- package/dist/roguelike/index.js.map +1 -1
- package/dist/simulation/CampaignSave.d.ts +68 -0
- package/dist/simulation/CampaignSave.d.ts.map +1 -0
- package/dist/simulation/CampaignSave.js +54 -0
- package/dist/simulation/CampaignSave.js.map +1 -0
- package/dist/simulation/EventPresentation.d.ts +111 -0
- package/dist/simulation/EventPresentation.d.ts.map +1 -0
- package/dist/simulation/EventPresentation.js +120 -0
- package/dist/simulation/EventPresentation.js.map +1 -0
- package/dist/simulation/index.d.ts +4 -0
- package/dist/simulation/index.d.ts.map +1 -1
- package/dist/simulation/index.js +2 -0
- package/dist/simulation/index.js.map +1 -1
- package/dist/two-d/pixi-interop.d.ts +18 -6
- package/dist/two-d/pixi-interop.d.ts.map +1 -1
- package/dist/two-d/pixi-interop.js +18 -6
- package/dist/two-d/pixi-interop.js.map +1 -1
- package/dist/two-d/render/AnimatedSprite.d.ts +74 -7
- package/dist/two-d/render/AnimatedSprite.d.ts.map +1 -1
- package/dist/two-d/render/AnimatedSprite.js +88 -26
- package/dist/two-d/render/AnimatedSprite.js.map +1 -1
- package/dist/two-d/render/Camera.d.ts +64 -4
- package/dist/two-d/render/Camera.d.ts.map +1 -1
- package/dist/two-d/render/Camera.js +132 -11
- package/dist/two-d/render/Camera.js.map +1 -1
- package/dist/two-d/render/Halo.d.ts +58 -0
- package/dist/two-d/render/Halo.d.ts.map +1 -0
- package/dist/two-d/render/Halo.js +48 -0
- package/dist/two-d/render/Halo.js.map +1 -0
- package/dist/two-d/render/ImageModifiers.d.ts +174 -0
- package/dist/two-d/render/ImageModifiers.d.ts.map +1 -1
- package/dist/two-d/render/ImageModifiers.js +438 -1
- package/dist/two-d/render/ImageModifiers.js.map +1 -1
- package/dist/two-d/render/LightningArc.d.ts +57 -0
- package/dist/two-d/render/LightningArc.d.ts.map +1 -0
- package/dist/two-d/render/LightningArc.js +91 -0
- package/dist/two-d/render/LightningArc.js.map +1 -0
- package/dist/two-d/render/PaletteRemap.d.ts +135 -0
- package/dist/two-d/render/PaletteRemap.d.ts.map +1 -0
- package/dist/two-d/render/PaletteRemap.js +181 -0
- package/dist/two-d/render/PaletteRemap.js.map +1 -0
- package/dist/two-d/render/Particles.d.ts +26 -0
- package/dist/two-d/render/Particles.d.ts.map +1 -1
- package/dist/two-d/render/Particles.js +23 -2
- package/dist/two-d/render/Particles.js.map +1 -1
- package/dist/two-d/render/SpriteAttachment.d.ts +55 -0
- package/dist/two-d/render/SpriteAttachment.d.ts.map +1 -0
- package/dist/two-d/render/SpriteAttachment.js +60 -0
- package/dist/two-d/render/SpriteAttachment.js.map +1 -0
- package/dist/two-d/render/TerrainGraphics.d.ts +142 -0
- package/dist/two-d/render/TerrainGraphics.d.ts.map +1 -0
- package/dist/two-d/render/TerrainGraphics.js +161 -0
- package/dist/two-d/render/TerrainGraphics.js.map +1 -0
- package/dist/two-d/render/index.d.ts +13 -3
- package/dist/two-d/render/index.d.ts.map +1 -1
- package/dist/two-d/render/index.js +6 -1
- package/dist/two-d/render/index.js.map +1 -1
- package/dist/two-d/stage/StoryScreen.d.ts +110 -0
- package/dist/two-d/stage/StoryScreen.d.ts.map +1 -0
- package/dist/two-d/stage/StoryScreen.js +178 -0
- package/dist/two-d/stage/StoryScreen.js.map +1 -0
- package/dist/two-d/stage/index.d.ts +2 -0
- package/dist/two-d/stage/index.d.ts.map +1 -1
- package/dist/two-d/stage/index.js +1 -0
- package/dist/two-d/stage/index.js.map +1 -1
- package/dist/two-d/ui/Checkbox.d.ts +44 -0
- package/dist/two-d/ui/Checkbox.d.ts.map +1 -0
- package/dist/two-d/ui/Checkbox.js +89 -0
- package/dist/two-d/ui/Checkbox.js.map +1 -0
- package/dist/two-d/ui/DataTable.d.ts +79 -0
- package/dist/two-d/ui/DataTable.d.ts.map +1 -0
- package/dist/two-d/ui/DataTable.js +164 -0
- package/dist/two-d/ui/DataTable.js.map +1 -0
- package/dist/two-d/ui/Dropdown.d.ts +72 -0
- package/dist/two-d/ui/Dropdown.d.ts.map +1 -0
- package/dist/two-d/ui/Dropdown.js +126 -0
- package/dist/two-d/ui/Dropdown.js.map +1 -0
- package/dist/two-d/ui/FloatingText.d.ts +43 -4
- package/dist/two-d/ui/FloatingText.d.ts.map +1 -1
- package/dist/two-d/ui/FloatingText.js +52 -5
- package/dist/two-d/ui/FloatingText.js.map +1 -1
- package/dist/two-d/ui/FloatingTextStack.d.ts +86 -13
- package/dist/two-d/ui/FloatingTextStack.d.ts.map +1 -1
- package/dist/two-d/ui/FloatingTextStack.js +89 -18
- package/dist/two-d/ui/FloatingTextStack.js.map +1 -1
- package/dist/two-d/ui/Layout.d.ts +105 -0
- package/dist/two-d/ui/Layout.d.ts.map +1 -0
- package/dist/two-d/ui/Layout.js +174 -0
- package/dist/two-d/ui/Layout.js.map +1 -0
- package/dist/two-d/ui/ScrollBox.d.ts +67 -0
- package/dist/two-d/ui/ScrollBox.d.ts.map +1 -0
- package/dist/two-d/ui/ScrollBox.js +127 -0
- package/dist/two-d/ui/ScrollBox.js.map +1 -0
- package/dist/two-d/ui/Skins.d.ts +59 -0
- package/dist/two-d/ui/Skins.d.ts.map +1 -0
- package/dist/two-d/ui/Skins.js +77 -0
- package/dist/two-d/ui/Skins.js.map +1 -0
- package/dist/two-d/ui/Slider.d.ts +91 -0
- package/dist/two-d/ui/Slider.d.ts.map +1 -0
- package/dist/two-d/ui/Slider.js +170 -0
- package/dist/two-d/ui/Slider.js.map +1 -0
- package/dist/two-d/ui/Spinner.d.ts +67 -0
- package/dist/two-d/ui/Spinner.d.ts.map +1 -0
- package/dist/two-d/ui/Spinner.js +137 -0
- package/dist/two-d/ui/Spinner.js.map +1 -0
- package/dist/two-d/ui/TabbedList.d.ts +118 -0
- package/dist/two-d/ui/TabbedList.d.ts.map +1 -0
- package/dist/two-d/ui/TabbedList.js +221 -0
- package/dist/two-d/ui/TabbedList.js.map +1 -0
- package/dist/two-d/ui/TextModel.d.ts +65 -0
- package/dist/two-d/ui/TextModel.d.ts.map +1 -0
- package/dist/two-d/ui/TextModel.js +126 -0
- package/dist/two-d/ui/TextModel.js.map +1 -0
- package/dist/two-d/ui/TreeView.d.ts +68 -0
- package/dist/two-d/ui/TreeView.d.ts.map +1 -0
- package/dist/two-d/ui/TreeView.js +133 -0
- package/dist/two-d/ui/TreeView.js.map +1 -0
- package/dist/two-d/ui/Window.d.ts +13 -0
- package/dist/two-d/ui/Window.d.ts.map +1 -1
- package/dist/two-d/ui/Window.js +23 -0
- package/dist/two-d/ui/Window.js.map +1 -1
- package/dist/two-d/ui/WindowStack.d.ts.map +1 -1
- package/dist/two-d/ui/WindowStack.js +2 -0
- package/dist/two-d/ui/WindowStack.js.map +1 -1
- package/dist/two-d/ui/index.d.ts +27 -3
- package/dist/two-d/ui/index.d.ts.map +1 -1
- package/dist/two-d/ui/index.js +14 -2
- package/dist/two-d/ui/index.js.map +1 -1
- package/dist/two-d/ui/markup.d.ts +127 -0
- package/dist/two-d/ui/markup.d.ts.map +1 -0
- package/dist/two-d/ui/markup.js +322 -0
- package/dist/two-d/ui/markup.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/world/SideTurns.d.ts +115 -0
- package/dist/world/SideTurns.d.ts.map +1 -0
- package/dist/world/SideTurns.js +138 -0
- package/dist/world/SideTurns.js.map +1 -0
- package/dist/world/index.d.ts +2 -0
- package/dist/world/index.d.ts.map +1 -1
- package/dist/world/index.js +1 -0
- package/dist/world/index.js.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The numbers behind a decision, for an AI that weighs outcomes instead of matching a behaviour.
|
|
3
|
+
*
|
|
4
|
+
* A rules agent decides from an opaque `perception` the game fills and from its own `state`, and a
|
|
5
|
+
* score exists only inside `alphaBetaSearch`, as the `evaluate` callback the game hands its own
|
|
6
|
+
* search. So a mind that wants to compare attacking against holding has nothing to weigh, and has
|
|
7
|
+
* to re-derive the game's valuation out of the perception blob, which is the duplication the
|
|
8
|
+
* envelope exists to prevent. These functions are that missing half: the framework owns how a view
|
|
9
|
+
* is assembled, the game owns what anything is worth (`scoreOf`), and the mind owns what it cares
|
|
10
|
+
* about (`ScorePersonality`).
|
|
11
|
+
*
|
|
12
|
+
* Both granularities the contract already names (`AIAgentDefinition.scope` is `'actor' |
|
|
13
|
+
* 'controller'`) run the same code and differ in one thing only: the visibility set they are handed.
|
|
14
|
+
* A unit's own sight for an actor, `FactionFog.sees(side)` for a whole side.
|
|
15
|
+
*/
|
|
16
|
+
/** A subject as a score view needs it: the game's own handle, its side, and where it stands. */
|
|
17
|
+
export interface ScoreSubject<T> {
|
|
18
|
+
readonly id: T;
|
|
19
|
+
/** who it fights for; sides are compared by name, so allies share one */
|
|
20
|
+
readonly side: string;
|
|
21
|
+
readonly x: number;
|
|
22
|
+
readonly y: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* What one mind can see, and what it is worth.
|
|
26
|
+
*
|
|
27
|
+
* The totals stay separate because which of them matter, and how much, is the mind's business -
|
|
28
|
+
* see `scoreWith`. Plain numbers, so a view crosses the AI boundary like any other `AIValue`.
|
|
29
|
+
*/
|
|
30
|
+
export interface ScoreView {
|
|
31
|
+
/** the subject's own score; 0 for a whole side, which has no body of its own */
|
|
32
|
+
readonly own: number;
|
|
33
|
+
/** the summed score of everyone on the subject's side that it can see, itself excluded */
|
|
34
|
+
readonly allies: number;
|
|
35
|
+
/** the summed score of everyone on another side that it can see */
|
|
36
|
+
readonly enemies: number;
|
|
37
|
+
/** how many subjects it could see at all, itself included: a diagnostic, never a score */
|
|
38
|
+
readonly seen: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A personality, as data: how much of each total is its owner's business.
|
|
42
|
+
*
|
|
43
|
+
* Weights rather than code, on purpose. A selfish scout is `{ own: 1, allies: 0, enemies: 0 }` and
|
|
44
|
+
* a loyal one `{ own: 0.5, allies: 1, enemies: 0 }`, both authored as content, and neither of them
|
|
45
|
+
* needing an AI implementation of its own. An enemy weight is usually negative, since an enemy's
|
|
46
|
+
* score rising is usually the subject's loss.
|
|
47
|
+
*/
|
|
48
|
+
export interface ScorePersonality {
|
|
49
|
+
readonly own: number;
|
|
50
|
+
readonly allies: number;
|
|
51
|
+
readonly enemies: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The view one subject has: its own score, and the scores of the allies and enemies it can see.
|
|
55
|
+
*
|
|
56
|
+
* `sees` is the visibility set, injected rather than assumed: a unit's own sight for an actor, the
|
|
57
|
+
* union over a side's units for a whole-side mind. The subject sees its own score whether or not it
|
|
58
|
+
* can see where it stands, so `own` is always read and `seen` always counts it.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* import { personalScoreView, scoreWith } from '@datamoc/mw_games/ai';
|
|
63
|
+
*
|
|
64
|
+
* const hero = { id: 'hero', side: 'blue', x: 2, y: 2 };
|
|
65
|
+
* const world = [
|
|
66
|
+
* hero,
|
|
67
|
+
* { id: 'ally', side: 'blue', x: 3, y: 2 },
|
|
68
|
+
* { id: 'rat', side: 'red', x: 4, y: 2 },
|
|
69
|
+
* { id: 'dragon', side: 'red', x: 9, y: 9 },
|
|
70
|
+
* ];
|
|
71
|
+
* const worth = (id: string) => ({ hero: 10, ally: 4, rat: -2, dragon: -40 })[id] ?? 0;
|
|
72
|
+
*
|
|
73
|
+
* // what a scout with a two-cell reach can see
|
|
74
|
+
* const sees = (x: number, y: number) => Math.abs(x - hero.x) + Math.abs(y - hero.y) <= 2;
|
|
75
|
+
*
|
|
76
|
+
* const view = personalScoreView(hero, world, worth, sees);
|
|
77
|
+
* console.log(view); // { own: 10, allies: 4, enemies: -2, seen: 3 } - the dragon is out of reach
|
|
78
|
+
* console.log(scoreWith(view, { own: 1, allies: 1, enemies: -1 })); // 16 - the rat is a nuisance
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export declare function personalScoreView<T>(subject: ScoreSubject<T>, world: readonly ScoreSubject<T>[], scoreOf: (id: T) => number, sees: (x: number, y: number) => boolean): ScoreView;
|
|
82
|
+
/**
|
|
83
|
+
* The view a whole side has: every unit on it that the side can see, and every enemy it can see.
|
|
84
|
+
*
|
|
85
|
+
* `own` is 0, because a side has no body of its own - its units are what `allies` counts. Hand this
|
|
86
|
+
* the union of a side's sight (`FactionFog.sees(side)` after syncing every unit of it) and the same
|
|
87
|
+
* arithmetic that reads for one character reads for the player.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```ts
|
|
91
|
+
* import { FactionFog } from '@datamoc/mw_games/board';
|
|
92
|
+
* import { scoreWith, sideScoreView } from '@datamoc/mw_games/ai';
|
|
93
|
+
*
|
|
94
|
+
* const fog = new FactionFog(10, 10);
|
|
95
|
+
* const world = [
|
|
96
|
+
* { id: 'scout', side: 'blue', x: 1, y: 1 },
|
|
97
|
+
* { id: 'guard', side: 'blue', x: 7, y: 7 },
|
|
98
|
+
* { id: 'rat', side: 'red', x: 3, y: 1 },
|
|
99
|
+
* ];
|
|
100
|
+
* const worth = (id: string) => ({ scout: 6, guard: 9, rat: -3 })[id] ?? 0;
|
|
101
|
+
*
|
|
102
|
+
* // the side reads what its units see: the scout alone would miss the rat at (3, 1)
|
|
103
|
+
* fog.sync('blue', [{ x: 4, y: 1 }], (source) => [source]);
|
|
104
|
+
* console.log(sideScoreView('blue', world, worth, fog.sees('blue')).enemies); // -3
|
|
105
|
+
* fog.sync('blue', [], () => []); // nobody watching, so nothing is read
|
|
106
|
+
* console.log(sideScoreView('blue', world, worth, fog.sees('blue')).enemies); // 0
|
|
107
|
+
* console.log(scoreWith(sideScoreView('blue', world, worth, fog.sees('blue')), { own: 0, allies: 1, enemies: -1 }));
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
export declare function sideScoreView<T>(side: string, world: readonly ScoreSubject<T>[], scoreOf: (id: T) => number, sees: (x: number, y: number) => boolean): ScoreView;
|
|
111
|
+
/**
|
|
112
|
+
* A view collapsed into the one number a mind compares, according to what that mind cares about.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```ts
|
|
116
|
+
* import { scoreWith, type ScoreView } from '@datamoc/mw_games/ai';
|
|
117
|
+
*
|
|
118
|
+
* const view: ScoreView = { own: 10, allies: 4, enemies: -2, seen: 3 };
|
|
119
|
+
*
|
|
120
|
+
* console.log(scoreWith(view, { own: 1, allies: 0, enemies: 0 })); // 10 - out for itself
|
|
121
|
+
* console.log(scoreWith(view, { own: 0, allies: 1, enemies: 0 })); // 4 - holds for the line
|
|
122
|
+
* console.log(scoreWith(view, { own: 1, allies: 1, enemies: -1 })); // 16 - the enemy is the point
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
export declare function scoreWith(view: ScoreView, personality: ScorePersonality): number;
|
|
126
|
+
//# sourceMappingURL=score.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"score.d.ts","sourceRoot":"","sources":["../../src/ai/score.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,gGAAgG;AAChG,MAAM,WAAW,YAAY,CAAC,CAAC;IAC9B,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAC;IAEf,yEAAyE;IACzE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACzB,gFAAgF;IAChF,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAErB,0FAA0F;IAC1F,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAExB,mEAAmE;IACnE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAEzB,0FAA0F;IAC1F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,gBAAgB;IAChC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAClC,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,EACxB,KAAK,EAAE,SAAS,YAAY,CAAC,CAAC,CAAC,EAAE,EACjC,OAAO,EAAE,CAAC,EAAE,EAAE,CAAC,KAAK,MAAM,EAC1B,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,OAAO,GACrC,SAAS,CAcX;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAC9B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,YAAY,CAAC,CAAC,CAAC,EAAE,EACjC,OAAO,EAAE,CAAC,EAAE,EAAE,CAAC,KAAK,MAAM,EAC1B,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,OAAO,GACrC,SAAS,CAaX;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,gBAAgB,GAAG,MAAM,CAEhF"}
|
package/dist/ai/score.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The numbers behind a decision, for an AI that weighs outcomes instead of matching a behaviour.
|
|
3
|
+
*
|
|
4
|
+
* A rules agent decides from an opaque `perception` the game fills and from its own `state`, and a
|
|
5
|
+
* score exists only inside `alphaBetaSearch`, as the `evaluate` callback the game hands its own
|
|
6
|
+
* search. So a mind that wants to compare attacking against holding has nothing to weigh, and has
|
|
7
|
+
* to re-derive the game's valuation out of the perception blob, which is the duplication the
|
|
8
|
+
* envelope exists to prevent. These functions are that missing half: the framework owns how a view
|
|
9
|
+
* is assembled, the game owns what anything is worth (`scoreOf`), and the mind owns what it cares
|
|
10
|
+
* about (`ScorePersonality`).
|
|
11
|
+
*
|
|
12
|
+
* Both granularities the contract already names (`AIAgentDefinition.scope` is `'actor' |
|
|
13
|
+
* 'controller'`) run the same code and differ in one thing only: the visibility set they are handed.
|
|
14
|
+
* A unit's own sight for an actor, `FactionFog.sees(side)` for a whole side.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* The view one subject has: its own score, and the scores of the allies and enemies it can see.
|
|
18
|
+
*
|
|
19
|
+
* `sees` is the visibility set, injected rather than assumed: a unit's own sight for an actor, the
|
|
20
|
+
* union over a side's units for a whole-side mind. The subject sees its own score whether or not it
|
|
21
|
+
* can see where it stands, so `own` is always read and `seen` always counts it.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* import { personalScoreView, scoreWith } from '@datamoc/mw_games/ai';
|
|
26
|
+
*
|
|
27
|
+
* const hero = { id: 'hero', side: 'blue', x: 2, y: 2 };
|
|
28
|
+
* const world = [
|
|
29
|
+
* hero,
|
|
30
|
+
* { id: 'ally', side: 'blue', x: 3, y: 2 },
|
|
31
|
+
* { id: 'rat', side: 'red', x: 4, y: 2 },
|
|
32
|
+
* { id: 'dragon', side: 'red', x: 9, y: 9 },
|
|
33
|
+
* ];
|
|
34
|
+
* const worth = (id: string) => ({ hero: 10, ally: 4, rat: -2, dragon: -40 })[id] ?? 0;
|
|
35
|
+
*
|
|
36
|
+
* // what a scout with a two-cell reach can see
|
|
37
|
+
* const sees = (x: number, y: number) => Math.abs(x - hero.x) + Math.abs(y - hero.y) <= 2;
|
|
38
|
+
*
|
|
39
|
+
* const view = personalScoreView(hero, world, worth, sees);
|
|
40
|
+
* console.log(view); // { own: 10, allies: 4, enemies: -2, seen: 3 } - the dragon is out of reach
|
|
41
|
+
* console.log(scoreWith(view, { own: 1, allies: 1, enemies: -1 })); // 16 - the rat is a nuisance
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export function personalScoreView(subject, world, scoreOf, sees) {
|
|
45
|
+
let allies = 0;
|
|
46
|
+
let enemies = 0;
|
|
47
|
+
let seen = 1;
|
|
48
|
+
for (const other of world) {
|
|
49
|
+
if (other.id === subject.id)
|
|
50
|
+
continue;
|
|
51
|
+
if (!sees(other.x, other.y))
|
|
52
|
+
continue;
|
|
53
|
+
seen++;
|
|
54
|
+
if (other.side === subject.side)
|
|
55
|
+
allies += scoreOf(other.id);
|
|
56
|
+
else
|
|
57
|
+
enemies += scoreOf(other.id);
|
|
58
|
+
}
|
|
59
|
+
return { own: scoreOf(subject.id), allies, enemies, seen };
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The view a whole side has: every unit on it that the side can see, and every enemy it can see.
|
|
63
|
+
*
|
|
64
|
+
* `own` is 0, because a side has no body of its own - its units are what `allies` counts. Hand this
|
|
65
|
+
* the union of a side's sight (`FactionFog.sees(side)` after syncing every unit of it) and the same
|
|
66
|
+
* arithmetic that reads for one character reads for the player.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* import { FactionFog } from '@datamoc/mw_games/board';
|
|
71
|
+
* import { scoreWith, sideScoreView } from '@datamoc/mw_games/ai';
|
|
72
|
+
*
|
|
73
|
+
* const fog = new FactionFog(10, 10);
|
|
74
|
+
* const world = [
|
|
75
|
+
* { id: 'scout', side: 'blue', x: 1, y: 1 },
|
|
76
|
+
* { id: 'guard', side: 'blue', x: 7, y: 7 },
|
|
77
|
+
* { id: 'rat', side: 'red', x: 3, y: 1 },
|
|
78
|
+
* ];
|
|
79
|
+
* const worth = (id: string) => ({ scout: 6, guard: 9, rat: -3 })[id] ?? 0;
|
|
80
|
+
*
|
|
81
|
+
* // the side reads what its units see: the scout alone would miss the rat at (3, 1)
|
|
82
|
+
* fog.sync('blue', [{ x: 4, y: 1 }], (source) => [source]);
|
|
83
|
+
* console.log(sideScoreView('blue', world, worth, fog.sees('blue')).enemies); // -3
|
|
84
|
+
* fog.sync('blue', [], () => []); // nobody watching, so nothing is read
|
|
85
|
+
* console.log(sideScoreView('blue', world, worth, fog.sees('blue')).enemies); // 0
|
|
86
|
+
* console.log(scoreWith(sideScoreView('blue', world, worth, fog.sees('blue')), { own: 0, allies: 1, enemies: -1 }));
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export function sideScoreView(side, world, scoreOf, sees) {
|
|
90
|
+
let allies = 0;
|
|
91
|
+
let enemies = 0;
|
|
92
|
+
let seen = 0;
|
|
93
|
+
for (const other of world) {
|
|
94
|
+
if (!sees(other.x, other.y))
|
|
95
|
+
continue;
|
|
96
|
+
seen++;
|
|
97
|
+
if (other.side === side)
|
|
98
|
+
allies += scoreOf(other.id);
|
|
99
|
+
else
|
|
100
|
+
enemies += scoreOf(other.id);
|
|
101
|
+
}
|
|
102
|
+
return { own: 0, allies, enemies, seen };
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* A view collapsed into the one number a mind compares, according to what that mind cares about.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```ts
|
|
109
|
+
* import { scoreWith, type ScoreView } from '@datamoc/mw_games/ai';
|
|
110
|
+
*
|
|
111
|
+
* const view: ScoreView = { own: 10, allies: 4, enemies: -2, seen: 3 };
|
|
112
|
+
*
|
|
113
|
+
* console.log(scoreWith(view, { own: 1, allies: 0, enemies: 0 })); // 10 - out for itself
|
|
114
|
+
* console.log(scoreWith(view, { own: 0, allies: 1, enemies: 0 })); // 4 - holds for the line
|
|
115
|
+
* console.log(scoreWith(view, { own: 1, allies: 1, enemies: -1 })); // 16 - the enemy is the point
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
export function scoreWith(view, personality) {
|
|
119
|
+
return view.own * personality.own + view.allies * personality.allies + view.enemies * personality.enemies;
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=score.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"score.js","sourceRoot":"","sources":["../../src/ai/score.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AA+CH;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,UAAU,iBAAiB,CAChC,OAAwB,EACxB,KAAiC,EACjC,OAA0B,EAC1B,IAAuC;IAEvC,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,IAAI,IAAI,GAAG,CAAC,CAAC;IAEb,KAAK,MAAM,KAAK,IAAI,KAAK,EAAE,CAAC;QAC3B,IAAI,KAAK,CAAC,EAAE,KAAK,OAAO,CAAC,EAAE;YAAE,SAAS;QACtC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;YAAE,SAAS;QACtC,IAAI,EAAE,CAAC;QACP,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,CAAC,IAAI;YAAE,MAAM,IAAI,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;;YACxD,OAAO,IAAI,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACnC,CAAC;IAED,OAAO,EAAE,GAAG,EAAE,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAC5D,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,UAAU,aAAa,CAC5B,IAAY,EACZ,KAAiC,EACjC,OAA0B,EAC1B,IAAuC;IAEvC,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,IAAI,IAAI,GAAG,CAAC,CAAC;IAEb,KAAK,MAAM,KAAK,IAAI,KAAK,EAAE,CAAC;QAC3B,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;YAAE,SAAS;QACtC,IAAI,EAAE,CAAC;QACP,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI;YAAE,MAAM,IAAI,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;;YAChD,OAAO,IAAI,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACnC,CAAC;IAED,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAC1C,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,SAAS,CAAC,IAAe,EAAE,WAA6B;IACvE,OAAO,IAAI,CAAC,GAAG,GAAG,WAAW,CAAC,GAAG,GAAG,IAAI,CAAC,MAAM,GAAG,WAAW,CAAC,MAAM,GAAG,IAAI,CAAC,OAAO,GAAG,WAAW,CAAC,OAAO,CAAC;AAC3G,CAAC"}
|
package/dist/assets/loader.d.ts
CHANGED
|
@@ -29,6 +29,16 @@ export interface LoadAssetsOptions {
|
|
|
29
29
|
* own default, which is the SVG's intrinsic size.
|
|
30
30
|
*/
|
|
31
31
|
resolution?: number;
|
|
32
|
+
/**
|
|
33
|
+
* Paths a game can do without - a portrait variant, an optional decal - loaded separately
|
|
34
|
+
* from the rest of the batch so one missing file never aborts every other asset the batch
|
|
35
|
+
* was loading. A path in here that fails to load is simply left unloaded: `isLoaded` reports
|
|
36
|
+
* `false` for it and `texture`'s fallback argument (or `get`'s) is what a caller reads
|
|
37
|
+
* instead, rather than every caller writing its own try/catch around `load()`.
|
|
38
|
+
*/
|
|
39
|
+
optional?: readonly string[];
|
|
40
|
+
/** called once per `optional` path that failed to load, with whatever `Assets.load` threw */
|
|
41
|
+
onMissing?: (path: string, error: unknown) => void;
|
|
32
42
|
}
|
|
33
43
|
/**
|
|
34
44
|
* Loads assets by path, giving each one an alias equal to its path.
|
|
@@ -38,10 +48,14 @@ export interface LoadAssetsOptions {
|
|
|
38
48
|
* (the original signature) or a `LoadAssetsOptions` object for the resolution option.
|
|
39
49
|
*/
|
|
40
50
|
export declare function load(paths: string[], options?: AssetProgress | LoadAssetsOptions): Promise<void>;
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
51
|
+
/**
|
|
52
|
+
* A loaded texture, by the same path it was loaded with. `fallback` is what an `optional`
|
|
53
|
+
* `load()` path was for: pass it (typically `Texture.EMPTY`) to get that back instead of a
|
|
54
|
+
* throw when the path never loaded, rather than every caller writing its own `isLoaded` guard.
|
|
55
|
+
*/
|
|
56
|
+
export declare function texture(path: string, fallback?: Texture): Texture;
|
|
57
|
+
/** a loaded asset of any other kind, such as parsed JSON; `fallback` behaves as it does on `texture` */
|
|
58
|
+
export declare function get<T>(path: string, fallback?: T): T;
|
|
45
59
|
/** true once `load` has resolved for this path and it has not since been `release`d */
|
|
46
60
|
export declare function isLoaded(path: string): boolean;
|
|
47
61
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/assets/loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,OAAO,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,EAAW,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAEzD;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,WAAW,iBAAiB;IACjC,2EAA2E;IAC3E,UAAU,CAAC,EAAE,aAAa,CAAC;IAE3B;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/assets/loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,OAAO,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,EAAW,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAEzD;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,WAAW,iBAAiB;IACjC,2EAA2E;IAC3E,UAAU,CAAC,EAAE,aAAa,CAAC;IAE3B;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAE7B,6FAA6F;IAC7F,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACnD;AASD;;;;;;GAMG;AACH,wBAAsB,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,aAAa,GAAG,iBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsC1G;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,OAAO,CAMjE;AAED,wGAAwG;AACxG,wBAAgB,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC,GAAG,CAAC,CAMpD;AAED,uFAAuF;AACvF,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE9C;AAED;;;;;;;GAOG;AACH,wBAAsB,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAI5D"}
|
package/dist/assets/loader.js
CHANGED
|
@@ -8,30 +8,59 @@ import { resolve } from './paths.js';
|
|
|
8
8
|
* (the original signature) or a `LoadAssetsOptions` object for the resolution option.
|
|
9
9
|
*/
|
|
10
10
|
export async function load(paths, options = {}) {
|
|
11
|
-
const { onProgress, resolution } = typeof options === 'function'
|
|
11
|
+
const { onProgress, resolution, optional, onMissing } = typeof options === 'function'
|
|
12
|
+
? { onProgress: options, resolution: undefined, optional: undefined, onMissing: undefined }
|
|
13
|
+
: options;
|
|
14
|
+
const optionalPaths = new Set(optional ?? []);
|
|
12
15
|
const pending = paths.filter((path) => !Assets.cache.has(path));
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
for (const path of pending) {
|
|
16
|
+
const requiredPending = pending.filter((path) => !optionalPaths.has(path));
|
|
17
|
+
const optionalPending = pending.filter((path) => optionalPaths.has(path));
|
|
18
|
+
const add = (path) => {
|
|
18
19
|
const descriptor = { alias: path, src: resolve(path) };
|
|
19
20
|
if (resolution !== undefined)
|
|
20
21
|
descriptor.data = { resolution };
|
|
21
22
|
Assets.add(descriptor);
|
|
23
|
+
};
|
|
24
|
+
if (optionalPending.length === 0 && requiredPending.length === 0) {
|
|
25
|
+
onProgress?.(1);
|
|
26
|
+
return;
|
|
22
27
|
}
|
|
23
|
-
|
|
28
|
+
for (const path of requiredPending)
|
|
29
|
+
add(path);
|
|
30
|
+
const requiredLoad = requiredPending.length > 0 ? Assets.load(requiredPending, onProgress) : Promise.resolve();
|
|
31
|
+
//loaded one at a time and never rejected past this function: one missing optional asset
|
|
32
|
+
//must never take the required batch (or any other optional path) down with it
|
|
33
|
+
const optionalLoads = optionalPending.map(async (path) => {
|
|
34
|
+
add(path);
|
|
35
|
+
try {
|
|
36
|
+
await Assets.load(path);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
onMissing?.(path, error);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
await Promise.all([requiredLoad, ...optionalLoads]);
|
|
43
|
+
if (optionalPending.length > 0 && requiredPending.length === 0)
|
|
44
|
+
onProgress?.(1);
|
|
24
45
|
}
|
|
25
|
-
/**
|
|
26
|
-
|
|
46
|
+
/**
|
|
47
|
+
* A loaded texture, by the same path it was loaded with. `fallback` is what an `optional`
|
|
48
|
+
* `load()` path was for: pass it (typically `Texture.EMPTY`) to get that back instead of a
|
|
49
|
+
* throw when the path never loaded, rather than every caller writing its own `isLoaded` guard.
|
|
50
|
+
*/
|
|
51
|
+
export function texture(path, fallback) {
|
|
27
52
|
if (!Assets.cache.has(path)) {
|
|
53
|
+
if (fallback !== undefined)
|
|
54
|
+
return fallback;
|
|
28
55
|
throw new Error(`texture "${path}" has not been loaded - pass it to load() first`);
|
|
29
56
|
}
|
|
30
57
|
return Assets.get(path);
|
|
31
58
|
}
|
|
32
|
-
/** a loaded asset of any other kind, such as parsed JSON */
|
|
33
|
-
export function get(path) {
|
|
59
|
+
/** a loaded asset of any other kind, such as parsed JSON; `fallback` behaves as it does on `texture` */
|
|
60
|
+
export function get(path, fallback) {
|
|
34
61
|
if (!Assets.cache.has(path)) {
|
|
62
|
+
if (fallback !== undefined)
|
|
63
|
+
return fallback;
|
|
35
64
|
throw new Error(`asset "${path}" has not been loaded - pass it to load() first`);
|
|
36
65
|
}
|
|
37
66
|
return Assets.get(path);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loader.js","sourceRoot":"","sources":["../../src/assets/loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAsB,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"loader.js","sourceRoot":"","sources":["../../src/assets/loader.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { Sound } from './Sound.ts';
|
|
2
|
+
/** A point in the same 2D space the game's camera works in, in world units. */
|
|
3
|
+
export interface AudioPoint {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* How far away a source can be heard from, and how fast it fades.
|
|
9
|
+
*
|
|
10
|
+
* `refDistance` is full volume, `maxDistance` is silence, and `rolloff` shapes the drop
|
|
11
|
+
* between them: 1 is inverse-linear, 2 inverse-square. These are the names the Web Audio
|
|
12
|
+
* `PannerNode` uses for the same three numbers, so a game that later moves to a real panner
|
|
13
|
+
* does not have to re-author them.
|
|
14
|
+
*/
|
|
15
|
+
export interface AudioFalloff {
|
|
16
|
+
/** distance at which the source is at full volume; defaults to 1 */
|
|
17
|
+
refDistance?: number;
|
|
18
|
+
/** distance at which the source is silent; defaults to Infinity */
|
|
19
|
+
maxDistance?: number;
|
|
20
|
+
/** exponent of the inverse-distance drop; defaults to 1 */
|
|
21
|
+
rolloff?: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The gain a listener at `distance` hears a source at, clamped to [0, 1]: 1 at or inside
|
|
25
|
+
* `refDistance`, 0 at or beyond `maxDistance`, an inverse-distance curve between them.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* import { audioGain } from '@datamoc/mw_games/audio';
|
|
30
|
+
*
|
|
31
|
+
* audioGain(0, { refDistance: 2 }); // 1 - inside the full-volume radius
|
|
32
|
+
* audioGain(2, { refDistance: 2 }); // 1 - the reference distance itself
|
|
33
|
+
* audioGain(4, { refDistance: 2 }); // 0.5 - one doubling away
|
|
34
|
+
* audioGain(4, { refDistance: 2, maxDistance: 4 }); // 0 - at the edge of hearing
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare function audioGain(distance: number, falloff?: AudioFalloff): number;
|
|
38
|
+
/**
|
|
39
|
+
* Stereo pan in [-1, 1] for a source: -1 is hard left of the listener, 1 hard right, 0
|
|
40
|
+
* directly ahead or behind. The listener's `facing` is its heading in radians (0 faces +x,
|
|
41
|
+
* increasing clockwise on a screen where y grows downward), so turning the listener turns
|
|
42
|
+
* what counts as "right" with it - a sound to the world's east is centred when the listener
|
|
43
|
+
* faces east and hard right when the listener faces north.
|
|
44
|
+
*
|
|
45
|
+
* A game with a stereo backend applies this itself; the framework has no panner node of its
|
|
46
|
+
* own, so it reports the number rather than pretending to pan.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* import { audioPan } from '@datamoc/mw_games/audio';
|
|
51
|
+
*
|
|
52
|
+
* const listener = { x: 0, y: 0, facing: 0 }; // facing east
|
|
53
|
+
* audioPan(listener, { x: 10, y: 0 }); // 0 - straight ahead
|
|
54
|
+
* audioPan(listener, { x: 0, y: 10 }); // 1 - to the listener's right
|
|
55
|
+
* audioPan(listener, { x: 0, y: -10 }); // -1 - to its left
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export declare function audioPan(listener: AudioPoint & {
|
|
59
|
+
facing: number;
|
|
60
|
+
}, source: AudioPoint): number;
|
|
61
|
+
/**
|
|
62
|
+
* Where sounds are heard from: a game moves one listener to its camera or its party leader
|
|
63
|
+
* each frame, and every `SoundSource` is mixed against it.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```ts
|
|
67
|
+
* import { AudioListener } from '@datamoc/mw_games/audio';
|
|
68
|
+
*
|
|
69
|
+
* const listener = new AudioListener({ x: 40, y: 12 });
|
|
70
|
+
* listener.moveTo(41, 12); // following the camera
|
|
71
|
+
* listener.face(Math.PI / 2); // now facing the world's south
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export declare class AudioListener implements AudioPoint {
|
|
75
|
+
x: number;
|
|
76
|
+
y: number;
|
|
77
|
+
/** heading in radians: 0 faces +x, increasing clockwise on a y-down screen */
|
|
78
|
+
facing: number;
|
|
79
|
+
constructor(options?: {
|
|
80
|
+
x?: number;
|
|
81
|
+
y?: number;
|
|
82
|
+
facing?: number;
|
|
83
|
+
});
|
|
84
|
+
moveTo(x: number, y: number): void;
|
|
85
|
+
face(radians: number): void;
|
|
86
|
+
}
|
|
87
|
+
export interface SoundSourceOptions extends AudioFalloff {
|
|
88
|
+
x?: number;
|
|
89
|
+
y?: number;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* One positional one-shot: a `Sound` attached to a point in the world, heard from a
|
|
93
|
+
* listener with the distance gain `audioGain` computes. `playFor` applies that gain through
|
|
94
|
+
* `Sound.play(gain)`, and stays silent past `maxDistance` rather than playing at volume 0.
|
|
95
|
+
*
|
|
96
|
+
* Long-running position (a waterfall, an engine) is the game's own loop calling `playFor` on
|
|
97
|
+
* a cadence it chooses; this is the placement and the mix, not a scheduler.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```ts
|
|
101
|
+
* import { AudioListener, Sound, SoundSource } from '@datamoc/mw_games/audio';
|
|
102
|
+
*
|
|
103
|
+
* const listener = new AudioListener({ x: 0, y: 0 });
|
|
104
|
+
* const arrow = new Sound('sounds/arrow.mp3');
|
|
105
|
+
* const source = new SoundSource(arrow, { x: 6, y: 8, refDistance: 4, maxDistance: 20 });
|
|
106
|
+
*
|
|
107
|
+
* const gain = source.gainTo(listener); // 0.4 at distance 10
|
|
108
|
+
* source.playFor(listener); // plays at that gain, or not at all when silent
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
export declare class SoundSource implements AudioPoint {
|
|
112
|
+
x: number;
|
|
113
|
+
y: number;
|
|
114
|
+
private readonly sound;
|
|
115
|
+
private readonly falloff;
|
|
116
|
+
constructor(sound: Sound, options?: SoundSourceOptions);
|
|
117
|
+
moveTo(x: number, y: number): void;
|
|
118
|
+
gainTo(listener: AudioPoint): number;
|
|
119
|
+
panTo(listener: AudioPoint & {
|
|
120
|
+
facing: number;
|
|
121
|
+
}): number;
|
|
122
|
+
/** Plays the source at the gain the listener hears it; returns the gain actually used. */
|
|
123
|
+
playFor(listener: AudioPoint): number;
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=Positional.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Positional.d.ts","sourceRoot":"","sources":["../../src/audio/Positional.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAExC,+EAA+E;AAC/E,MAAM,WAAW,UAAU;IAC1B,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACV;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY;IAC5B,oEAAoE;IACpE,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,mEAAmE;IACnE,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,MAAM,CAU9E;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,UAAU,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,EAAE,MAAM,EAAE,UAAU,GAAG,MAAM,CAW9F;AAED;;;;;;;;;;;;GAYG;AACH,qBAAa,aAAc,YAAW,UAAU;IAC/C,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IAEV,8EAA8E;IAC9E,MAAM,EAAE,MAAM,CAAC;IAEf,YAAY,OAAO,GAAE;QAAE,CAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAO,EAIpE;IAED,MAAM,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAGjC;IAED,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE1B;CACD;AAED,MAAM,WAAW,kBAAmB,SAAQ,YAAY;IACvD,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,CAAC,CAAC,EAAE,MAAM,CAAC;CACX;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,WAAY,YAAW,UAAU;IAC7C,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IAEV,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAe;IAEvC,YAAY,KAAK,EAAE,KAAK,EAAE,OAAO,GAAE,kBAAuB,EASzD;IAED,MAAM,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAGjC;IAED,MAAM,CAAC,QAAQ,EAAE,UAAU,GAAG,MAAM,CAEnC;IAED,KAAK,CAAC,QAAQ,EAAE,UAAU,GAAG;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,CAEvD;IAED,0FAA0F;IAC1F,OAAO,CAAC,QAAQ,EAAE,UAAU,GAAG,MAAM,CAKpC;CACD"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gain a listener at `distance` hears a source at, clamped to [0, 1]: 1 at or inside
|
|
3
|
+
* `refDistance`, 0 at or beyond `maxDistance`, an inverse-distance curve between them.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { audioGain } from '@datamoc/mw_games/audio';
|
|
8
|
+
*
|
|
9
|
+
* audioGain(0, { refDistance: 2 }); // 1 - inside the full-volume radius
|
|
10
|
+
* audioGain(2, { refDistance: 2 }); // 1 - the reference distance itself
|
|
11
|
+
* audioGain(4, { refDistance: 2 }); // 0.5 - one doubling away
|
|
12
|
+
* audioGain(4, { refDistance: 2, maxDistance: 4 }); // 0 - at the edge of hearing
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function audioGain(distance, falloff = {}) {
|
|
16
|
+
const refDistance = falloff.refDistance ?? 1;
|
|
17
|
+
const maxDistance = falloff.maxDistance ?? Infinity;
|
|
18
|
+
const rolloff = falloff.rolloff ?? 1;
|
|
19
|
+
if (!(distance > refDistance))
|
|
20
|
+
return 1;
|
|
21
|
+
if (!(distance < maxDistance))
|
|
22
|
+
return 0;
|
|
23
|
+
const gain = Math.pow(refDistance / distance, rolloff);
|
|
24
|
+
return Math.max(0, Math.min(1, gain));
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Stereo pan in [-1, 1] for a source: -1 is hard left of the listener, 1 hard right, 0
|
|
28
|
+
* directly ahead or behind. The listener's `facing` is its heading in radians (0 faces +x,
|
|
29
|
+
* increasing clockwise on a screen where y grows downward), so turning the listener turns
|
|
30
|
+
* what counts as "right" with it - a sound to the world's east is centred when the listener
|
|
31
|
+
* faces east and hard right when the listener faces north.
|
|
32
|
+
*
|
|
33
|
+
* A game with a stereo backend applies this itself; the framework has no panner node of its
|
|
34
|
+
* own, so it reports the number rather than pretending to pan.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* import { audioPan } from '@datamoc/mw_games/audio';
|
|
39
|
+
*
|
|
40
|
+
* const listener = { x: 0, y: 0, facing: 0 }; // facing east
|
|
41
|
+
* audioPan(listener, { x: 10, y: 0 }); // 0 - straight ahead
|
|
42
|
+
* audioPan(listener, { x: 0, y: 10 }); // 1 - to the listener's right
|
|
43
|
+
* audioPan(listener, { x: 0, y: -10 }); // -1 - to its left
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export function audioPan(listener, source) {
|
|
47
|
+
const dx = source.x - listener.x;
|
|
48
|
+
const dy = source.y - listener.y;
|
|
49
|
+
const distance = Math.hypot(dx, dy);
|
|
50
|
+
if (distance === 0)
|
|
51
|
+
return 0;
|
|
52
|
+
//the listener's right-hand direction at heading `facing`, in screen coordinates
|
|
53
|
+
const rightX = Math.sin(listener.facing);
|
|
54
|
+
const rightY = Math.cos(listener.facing);
|
|
55
|
+
const pan = (dx * rightX + dy * rightY) / distance;
|
|
56
|
+
return Math.max(-1, Math.min(1, pan));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Where sounds are heard from: a game moves one listener to its camera or its party leader
|
|
60
|
+
* each frame, and every `SoundSource` is mixed against it.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```ts
|
|
64
|
+
* import { AudioListener } from '@datamoc/mw_games/audio';
|
|
65
|
+
*
|
|
66
|
+
* const listener = new AudioListener({ x: 40, y: 12 });
|
|
67
|
+
* listener.moveTo(41, 12); // following the camera
|
|
68
|
+
* listener.face(Math.PI / 2); // now facing the world's south
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export class AudioListener {
|
|
72
|
+
constructor(options = {}) {
|
|
73
|
+
this.x = options.x ?? 0;
|
|
74
|
+
this.y = options.y ?? 0;
|
|
75
|
+
this.facing = options.facing ?? 0;
|
|
76
|
+
}
|
|
77
|
+
moveTo(x, y) {
|
|
78
|
+
this.x = x;
|
|
79
|
+
this.y = y;
|
|
80
|
+
}
|
|
81
|
+
face(radians) {
|
|
82
|
+
this.facing = radians;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* One positional one-shot: a `Sound` attached to a point in the world, heard from a
|
|
87
|
+
* listener with the distance gain `audioGain` computes. `playFor` applies that gain through
|
|
88
|
+
* `Sound.play(gain)`, and stays silent past `maxDistance` rather than playing at volume 0.
|
|
89
|
+
*
|
|
90
|
+
* Long-running position (a waterfall, an engine) is the game's own loop calling `playFor` on
|
|
91
|
+
* a cadence it chooses; this is the placement and the mix, not a scheduler.
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```ts
|
|
95
|
+
* import { AudioListener, Sound, SoundSource } from '@datamoc/mw_games/audio';
|
|
96
|
+
*
|
|
97
|
+
* const listener = new AudioListener({ x: 0, y: 0 });
|
|
98
|
+
* const arrow = new Sound('sounds/arrow.mp3');
|
|
99
|
+
* const source = new SoundSource(arrow, { x: 6, y: 8, refDistance: 4, maxDistance: 20 });
|
|
100
|
+
*
|
|
101
|
+
* const gain = source.gainTo(listener); // 0.4 at distance 10
|
|
102
|
+
* source.playFor(listener); // plays at that gain, or not at all when silent
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export class SoundSource {
|
|
106
|
+
constructor(sound, options = {}) {
|
|
107
|
+
this.sound = sound;
|
|
108
|
+
this.x = options.x ?? 0;
|
|
109
|
+
this.y = options.y ?? 0;
|
|
110
|
+
this.falloff = {
|
|
111
|
+
refDistance: options.refDistance,
|
|
112
|
+
maxDistance: options.maxDistance,
|
|
113
|
+
rolloff: options.rolloff,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
moveTo(x, y) {
|
|
117
|
+
this.x = x;
|
|
118
|
+
this.y = y;
|
|
119
|
+
}
|
|
120
|
+
gainTo(listener) {
|
|
121
|
+
return audioGain(Math.hypot(this.x - listener.x, this.y - listener.y), this.falloff);
|
|
122
|
+
}
|
|
123
|
+
panTo(listener) {
|
|
124
|
+
return audioPan(listener, this);
|
|
125
|
+
}
|
|
126
|
+
/** Plays the source at the gain the listener hears it; returns the gain actually used. */
|
|
127
|
+
playFor(listener) {
|
|
128
|
+
const gain = this.gainTo(listener);
|
|
129
|
+
if (gain <= 0)
|
|
130
|
+
return 0;
|
|
131
|
+
this.sound.play(gain);
|
|
132
|
+
return gain;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=Positional.js.map
|