ecspresso 0.9.0 → 0.10.0
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/README.md +36 -0
- package/dist/bundles/renderers/renderer2D.d.ts +4 -4
- package/dist/bundles/renderers/renderer2D.js +2 -2
- package/dist/bundles/renderers/renderer2D.js.map +4 -8
- package/dist/bundles/utils/bounds.d.ts +2 -2
- package/dist/bundles/utils/bounds.js +4 -0
- package/dist/bundles/utils/bounds.js.map +10 -0
- package/dist/bundles/utils/collision.d.ts +2 -2
- package/dist/bundles/utils/collision.js +4 -0
- package/dist/bundles/utils/collision.js.map +10 -0
- package/dist/bundles/utils/input.d.ts +133 -0
- package/dist/bundles/utils/input.js +4 -0
- package/dist/bundles/utils/input.js.map +10 -0
- package/dist/bundles/utils/movement.d.ts +2 -2
- package/dist/bundles/utils/movement.js +4 -0
- package/dist/bundles/utils/movement.js.map +10 -0
- package/dist/bundles/utils/timers.d.ts +2 -2
- package/dist/bundles/utils/timers.js +2 -2
- package/dist/bundles/utils/timers.js.map +4 -6
- package/dist/bundles/utils/transform.d.ts +2 -2
- package/dist/bundles/utils/transform.js +4 -0
- package/dist/bundles/utils/transform.js.map +10 -0
- package/dist/ecspresso.d.ts +19 -2
- package/dist/entity-manager.d.ts +5 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +7 -7
- package/dist/reactive-query-manager.d.ts +22 -3
- package/dist/system-builder.d.ts +7 -3
- package/dist/types.d.ts +15 -5
- package/package.json +22 -2
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input Bundle for ECSpresso
|
|
3
|
+
*
|
|
4
|
+
* Provides frame-accurate keyboard, pointer (mouse + touch via PointerEvent),
|
|
5
|
+
* and action mapping input. Resource-only bundle — input is polled via the
|
|
6
|
+
* `inputState` resource. No ECS components or events.
|
|
7
|
+
*
|
|
8
|
+
* DOM events are accumulated between frames and snapshotted once per frame
|
|
9
|
+
* in the system's process step, so all systems see consistent state.
|
|
10
|
+
*/
|
|
11
|
+
import { Bundle } from 'ecspresso';
|
|
12
|
+
import type { SystemPhase } from 'ecspresso';
|
|
13
|
+
export interface Vec2 {
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
}
|
|
17
|
+
type LowercaseLetter = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z';
|
|
18
|
+
type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
|
|
19
|
+
type Punctuation = '`' | '~' | '!' | '@' | '#' | '$' | '%' | '^' | '&' | '*' | '(' | ')' | '-' | '_' | '=' | '+' | '[' | '{' | ']' | '}' | '\\' | '|' | ';' | ':' | "'" | '"' | ',' | '<' | '.' | '>' | '/' | '?';
|
|
20
|
+
type ModifierKey = 'Alt' | 'AltGraph' | 'CapsLock' | 'Control' | 'Fn' | 'FnLock' | 'Hyper' | 'Meta' | 'NumLock' | 'ScrollLock' | 'Shift' | 'Super' | 'Symbol' | 'SymbolLock';
|
|
21
|
+
type WhitespaceKey = 'Enter' | 'Tab' | ' ';
|
|
22
|
+
type NavigationKey = `Arrow${'Down' | 'Left' | 'Right' | 'Up'}` | 'End' | 'Home' | 'PageDown' | 'PageUp';
|
|
23
|
+
type EditingKey = 'Backspace' | 'Clear' | 'Copy' | 'CrSel' | 'Cut' | 'Delete' | 'EraseEof' | 'ExSel' | 'Insert' | 'Paste' | 'Redo' | 'Undo';
|
|
24
|
+
type UIKey = 'Accept' | 'Again' | 'Attn' | 'Cancel' | 'ContextMenu' | 'Escape' | 'Execute' | 'Find' | 'Finish' | 'Help' | 'Pause' | 'Play' | 'Props' | 'Select' | 'ZoomIn' | 'ZoomOut';
|
|
25
|
+
type DeviceKey = 'BrightnessDown' | 'BrightnessUp' | 'Eject' | 'Hibernate' | 'LogOff' | 'Power' | 'PowerOff' | 'PrintScreen' | 'Standby' | 'WakeUp';
|
|
26
|
+
type IMEKey = 'AllCandidates' | 'Alphanumeric' | 'CodeInput' | 'Compose' | 'Convert' | 'FinalMode' | 'GroupFirst' | 'GroupLast' | 'GroupNext' | 'GroupPrevious' | 'ModeChange' | 'NextCandidate' | 'NonConvert' | 'PreviousCandidate' | 'Process' | 'SingleCandidate' | 'HangulMode' | 'HanjaMode' | 'JunjaMode' | 'Eisu' | 'Hankaku' | 'Hiragana' | 'HiraganaKatakana' | 'KanaMode' | 'KanjiMode' | 'Katakana' | 'Romaji' | 'Zenkaku' | 'ZenkakuHankaku';
|
|
27
|
+
type FunctionKey = `F${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24}` | 'Soft1' | 'Soft2' | 'Soft3' | 'Soft4';
|
|
28
|
+
type PhoneKey = 'AppSwitch' | 'Call' | 'Camera' | 'CameraFocus' | 'EndCall' | 'GoBack' | 'GoHome' | 'HeadsetHook' | 'LastNumberRedial' | 'Notification' | 'MannerMode' | 'VoiceDial';
|
|
29
|
+
type MultimediaKey = 'ChannelDown' | 'ChannelUp' | `Media${'FastForward' | 'Pause' | 'Play' | 'PlayPause' | 'Record' | 'Rewind' | 'Stop' | 'TrackNext' | 'TrackPrevious'}`;
|
|
30
|
+
type AudioKey = `Audio${'BalanceLeft' | 'BalanceRight' | 'BassDown' | 'BassBoostDown' | 'BassBoostToggle' | 'BassBoostUp' | 'BassUp' | 'FaderFront' | 'FaderRear' | 'SurroundModeNext' | 'TrebleDown' | 'TrebleUp' | 'VolumeDown' | 'VolumeMute' | 'VolumeUp'}` | `Microphone${'Toggle' | 'VolumeDown' | 'VolumeMute' | 'VolumeUp'}`;
|
|
31
|
+
type TVKey = 'TV' | `TV${'3DMode' | 'AntennaCable' | 'AudioDescription' | 'AudioDescriptionMixDown' | 'AudioDescriptionMixUp' | 'ContentsMenu' | 'DataService' | 'Input' | 'InputComponent1' | 'InputComponent2' | 'InputComposite1' | 'InputComposite2' | 'InputHDMI1' | 'InputHDMI2' | 'InputHDMI3' | 'InputHDMI4' | 'InputVGA1' | 'MediaContext' | 'Network' | 'NumberEntry' | 'Power' | 'RadioService' | 'Satellite' | 'SatelliteBS' | 'SatelliteCS' | 'SatelliteToggle' | 'TerrestrialAnalog' | 'TerrestrialDigital' | 'Timer'}`;
|
|
32
|
+
type MediaControllerKey = 'AVRInput' | 'AVRPower' | `Color${'F0Red' | 'F1Green' | 'F2Yellow' | 'F3Blue' | 'F4Grey' | 'F5Brown'}` | 'ClosedCaptionToggle' | 'Dimmer' | 'DisplaySwap' | 'DVR' | 'Exit' | `Favorite${'Clear' | 'Recall' | 'Store'}${0 | 1 | 2 | 3}` | 'Guide' | 'GuideNextDay' | 'GuidePreviousDay' | 'Info' | 'InstantReplay' | 'Link' | 'ListProgram' | 'LiveContent' | 'Lock' | `Media${'Apps' | 'AudioTrack' | 'Last' | 'SkipBackward' | 'SkipForward' | 'StepBackward' | 'StepForward' | 'TopMenu'}` | `Navigate${'In' | 'Next' | 'Out' | 'Previous'}` | 'NextFavoriteChannel' | 'NextUserProfile' | 'OnDemand' | 'Pairing' | `PinP${'Down' | 'Move' | 'Toggle' | 'Up'}` | `PlaySpeed${'Down' | 'Reset' | 'Up'}` | 'RandomToggle' | 'RcLowBattery' | 'RecordSpeedNext' | 'RfBypass' | 'ScanChannelsToggle' | 'ScreenModeNext' | 'Settings' | 'SplitScreenToggle' | 'STBInput' | 'STBPower' | 'Subtitle' | 'Teletext' | 'VideoModeNext' | 'Wink' | 'ZoomToggle';
|
|
33
|
+
type SpeechKey = 'SpeechCorrectionList' | 'SpeechInputToggle';
|
|
34
|
+
type DocumentKey = 'Close' | 'New' | 'Open' | 'Print' | 'Save' | 'SpellCheck' | 'MailForward' | 'MailReply' | 'MailSend';
|
|
35
|
+
type LaunchKey = `Launch${'Calculator' | 'Calendar' | 'Contacts' | 'Mail' | 'MediaPlayer' | 'MusicPlayer' | 'MyComputer' | 'Phone' | 'ScreenSaver' | 'Spreadsheet' | 'WebBrowser' | 'WebCam' | 'WordProcessor' | `Application${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16}`}`;
|
|
36
|
+
type BrowserKey = `Browser${'Back' | 'Favorites' | 'Forward' | 'Home' | 'Refresh' | 'Search' | 'Stop'}`;
|
|
37
|
+
type NumpadKey = 'Decimal' | 'Key11' | 'Key12' | 'Multiply' | 'Add' | 'Divide' | 'Subtract' | 'Separator';
|
|
38
|
+
export type KeyCode = LowercaseLetter | Uppercase<LowercaseLetter> | Digit | Punctuation | ModifierKey | WhitespaceKey | NavigationKey | EditingKey | UIKey | DeviceKey | IMEKey | FunctionKey | PhoneKey | MultimediaKey | AudioKey | TVKey | MediaControllerKey | SpeechKey | DocumentKey | LaunchKey | BrowserKey | NumpadKey | 'Unidentified' | 'Dead';
|
|
39
|
+
export interface KeyboardState {
|
|
40
|
+
isDown(key: KeyCode): boolean;
|
|
41
|
+
justPressed(key: KeyCode): boolean;
|
|
42
|
+
justReleased(key: KeyCode): boolean;
|
|
43
|
+
}
|
|
44
|
+
export interface PointerState {
|
|
45
|
+
readonly position: Readonly<Vec2>;
|
|
46
|
+
readonly delta: Readonly<Vec2>;
|
|
47
|
+
isDown(button: number): boolean;
|
|
48
|
+
justPressed(button: number): boolean;
|
|
49
|
+
justReleased(button: number): boolean;
|
|
50
|
+
}
|
|
51
|
+
export interface ActionState {
|
|
52
|
+
isActive(action: string): boolean;
|
|
53
|
+
justActivated(action: string): boolean;
|
|
54
|
+
justDeactivated(action: string): boolean;
|
|
55
|
+
}
|
|
56
|
+
export interface InputState {
|
|
57
|
+
readonly keyboard: KeyboardState;
|
|
58
|
+
readonly pointer: PointerState;
|
|
59
|
+
readonly actions: ActionState;
|
|
60
|
+
setActionMap(actions: ActionMap): void;
|
|
61
|
+
getActionMap(): Readonly<ActionMap>;
|
|
62
|
+
}
|
|
63
|
+
export interface ActionBinding {
|
|
64
|
+
keys?: KeyCode[];
|
|
65
|
+
buttons?: number[];
|
|
66
|
+
}
|
|
67
|
+
export type ActionMap = Record<string, ActionBinding>;
|
|
68
|
+
export interface InputResourceTypes {
|
|
69
|
+
inputState: InputState;
|
|
70
|
+
}
|
|
71
|
+
export interface InputBundleOptions {
|
|
72
|
+
/** System group name (default: 'input') */
|
|
73
|
+
systemGroup?: string;
|
|
74
|
+
/** Priority for input system (default: 100) */
|
|
75
|
+
priority?: number;
|
|
76
|
+
/** Execution phase (default: 'preUpdate') */
|
|
77
|
+
phase?: SystemPhase;
|
|
78
|
+
/** Initial action mappings */
|
|
79
|
+
actions?: ActionMap;
|
|
80
|
+
/** EventTarget to attach listeners to (default: globalThis). Pass a custom target for testability. */
|
|
81
|
+
target?: EventTarget;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Define an action map with proper typing.
|
|
85
|
+
*
|
|
86
|
+
* @param map Object mapping action names to bindings
|
|
87
|
+
* @returns The same map (identity, for convenience/documentation)
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```typescript
|
|
91
|
+
* const actions = defineActionMap({
|
|
92
|
+
* jump: { keys: [' ', 'ArrowUp'] },
|
|
93
|
+
* shoot: { keys: ['z'], buttons: [0] },
|
|
94
|
+
* });
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
export declare function defineActionMap<T extends ActionMap>(map: T): T;
|
|
98
|
+
/**
|
|
99
|
+
* Create a single action binding.
|
|
100
|
+
*
|
|
101
|
+
* @param binding The binding configuration
|
|
102
|
+
* @returns The same binding object
|
|
103
|
+
*/
|
|
104
|
+
export declare function createActionBinding(binding: ActionBinding): ActionBinding;
|
|
105
|
+
/**
|
|
106
|
+
* Create an input bundle for ECSpresso.
|
|
107
|
+
*
|
|
108
|
+
* This bundle provides:
|
|
109
|
+
* - Frame-accurate keyboard state (isDown, justPressed, justReleased)
|
|
110
|
+
* - Pointer position/delta and button state (mouse + touch via PointerEvent)
|
|
111
|
+
* - Named action mapping with runtime remapping
|
|
112
|
+
* - Automatic listener cleanup on detach
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```typescript
|
|
116
|
+
* const ecs = ECSpresso
|
|
117
|
+
* .create<Components, Events, Resources>()
|
|
118
|
+
* .withBundle(createInputBundle({
|
|
119
|
+
* actions: {
|
|
120
|
+
* jump: { keys: [' ', 'ArrowUp'] },
|
|
121
|
+
* shoot: { keys: ['z'], buttons: [0] },
|
|
122
|
+
* },
|
|
123
|
+
* }))
|
|
124
|
+
* .build();
|
|
125
|
+
*
|
|
126
|
+
* // In a system:
|
|
127
|
+
* const input = ecs.getResource('inputState');
|
|
128
|
+
* if (input.actions.justActivated('jump')) { ... }
|
|
129
|
+
* if (input.keyboard.isDown('ArrowRight')) { ... }
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
export declare function createInputBundle(options?: InputBundleOptions): Bundle<{}, {}, InputResourceTypes>;
|
|
133
|
+
export {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
var _=((q)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(q,{get:(O,Q)=>(typeof require<"u"?require:O)[Q]}):q)(function(q){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+q+'" is not supported')});import{Bundle as Y}from"ecspresso";function M(q){return q}function T(q){return q}function A(){return{keysDown:new Set,keysPressed:[],keysReleased:[],buttonsDown:new Set,buttonsPressed:[],buttonsReleased:[],pointerX:0,pointerY:0,pointerDeltaX:0,pointerDeltaY:0,lastPointerX:0,lastPointerY:0,pointerMoved:!1}}var K=new Set,B=new Set;function D(){return{keysDown:K,keysPressed:K,keysReleased:K,buttonsDown:B,buttonsPressed:B,buttonsReleased:B,pointerX:0,pointerY:0,pointerDeltaX:0,pointerDeltaY:0,actionsActive:K,prevActionsActive:K}}function R(q,O,Q){let V=new Set;for(let[Z,W]of Object.entries(q)){let H=W.keys?.some((J)=>O.has(J))??!1,z=W.buttons?.some((J)=>Q.has(J))??!1;if(H||z)V.add(Z)}return V}function v(q,O,Q){let V=new Set(q.keysDown),Z=new Set(q.keysPressed),W=new Set(q.keysReleased),H=new Set(q.buttonsDown),z=new Set(q.buttonsPressed),J=new Set(q.buttonsReleased),$=q.pointerMoved?q.pointerX-q.lastPointerX:0,j=q.pointerMoved?q.pointerY-q.lastPointerY:0,x=R(Q,V,H),L={keysDown:V,keysPressed:Z,keysReleased:W,buttonsDown:H,buttonsPressed:z,buttonsReleased:J,pointerX:q.pointerX,pointerY:q.pointerY,pointerDeltaX:$,pointerDeltaY:j,actionsActive:x,prevActionsActive:O};return q.keysPressed=[],q.keysReleased=[],q.buttonsPressed=[],q.buttonsReleased=[],q.lastPointerX=q.pointerX,q.lastPointerY=q.pointerY,q.pointerMoved=!1,L}function l(q){let{systemGroup:O="input",priority:Q=100,phase:V="preUpdate",actions:Z={},target:W=globalThis}=q??{},H=A(),z=D(),J={...Z},$=[],j={x:0,y:0},x={x:0,y:0},g={keyboard:{isDown:(f)=>z.keysDown.has(f),justPressed:(f)=>z.keysPressed.has(f),justReleased:(f)=>z.keysReleased.has(f)},pointer:{position:j,delta:x,isDown:(f)=>z.buttonsDown.has(f),justPressed:(f)=>z.buttonsPressed.has(f),justReleased:(f)=>z.buttonsReleased.has(f)},actions:{isActive:(f)=>z.actionsActive.has(f),justActivated:(f)=>z.actionsActive.has(f)&&!z.prevActionsActive.has(f),justDeactivated:(f)=>!z.actionsActive.has(f)&&z.prevActionsActive.has(f)},setActionMap(f){J={...f}},getActionMap(){return{...J}}};function m(f){let C=f;if(C.repeat)return;H.keysDown.add(C.key),H.keysPressed.push(C.key)}function G(f){let C=f;H.keysDown.delete(C.key),H.keysReleased.push(C.key)}function I(f){let C=f;H.buttonsDown.add(C.button),H.buttonsPressed.push(C.button)}function U(f){let C=f;H.pointerX=C.clientX,H.pointerY=C.clientY,H.pointerMoved=!0}function N(f){let C=f;H.buttonsDown.delete(C.button),H.buttonsReleased.push(C.button)}function X(f,C){W.addEventListener(f,C),$.push(()=>W.removeEventListener(f,C))}let F=new Y("input");return F.addResource("inputState",g),F.addSystem("input-state").setPriority(Q).inPhase(V).inGroup(O).setOnInitialize(()=>{X("keydown",m),X("keyup",G),X("pointerdown",I),X("pointermove",U),X("pointerup",N)}).setOnDetach(()=>{for(let f of $)f();$.length=0}).setProcess(()=>{let f=z.actionsActive;z=v(H,f,J),j.x=z.pointerX,j.y=z.pointerY,x.x=z.pointerDeltaX,x.y=z.pointerDeltaY}).and(),F}export{M as defineActionMap,l as createInputBundle,T as createActionBinding};
|
|
2
|
+
|
|
3
|
+
//# debugId=47D459776A55946564756E2164756E21
|
|
4
|
+
//# sourceMappingURL=input.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/bundles/utils/input.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * Input Bundle for ECSpresso\n *\n * Provides frame-accurate keyboard, pointer (mouse + touch via PointerEvent),\n * and action mapping input. Resource-only bundle — input is polled via the\n * `inputState` resource. No ECS components or events.\n *\n * DOM events are accumulated between frames and snapshotted once per frame\n * in the system's process step, so all systems see consistent state.\n */\n\nimport { Bundle } from 'ecspresso';\nimport type { SystemPhase } from 'ecspresso';\n\n// ==================== Public Types ====================\n\nexport interface Vec2 {\n\tx: number;\n\ty: number;\n}\n\n// Key codes per the UI Events spec (KeyboardEvent.key values)\n// https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values\n\ntype LowercaseLetter =\n\t| 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm'\n\t| 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z';\n\ntype Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';\n\ntype Punctuation =\n\t| '`' | '~' | '!' | '@' | '#' | '$' | '%' | '^' | '&' | '*' | '(' | ')'\n\t| '-' | '_' | '=' | '+' | '[' | '{' | ']' | '}' | '\\\\' | '|'\n\t| ';' | ':' | \"'\" | '\"' | ',' | '<' | '.' | '>' | '/' | '?';\n\ntype ModifierKey =\n\t| 'Alt' | 'AltGraph' | 'CapsLock' | 'Control' | 'Fn' | 'FnLock'\n\t| 'Hyper' | 'Meta' | 'NumLock' | 'ScrollLock' | 'Shift'\n\t| 'Super' | 'Symbol' | 'SymbolLock';\n\ntype WhitespaceKey = 'Enter' | 'Tab' | ' ';\n\ntype NavigationKey =\n\t| `Arrow${'Down' | 'Left' | 'Right' | 'Up'}`\n\t| 'End' | 'Home' | 'PageDown' | 'PageUp';\n\ntype EditingKey =\n\t| 'Backspace' | 'Clear' | 'Copy' | 'CrSel' | 'Cut' | 'Delete'\n\t| 'EraseEof' | 'ExSel' | 'Insert' | 'Paste' | 'Redo' | 'Undo';\n\ntype UIKey =\n\t| 'Accept' | 'Again' | 'Attn' | 'Cancel' | 'ContextMenu' | 'Escape'\n\t| 'Execute' | 'Find' | 'Finish' | 'Help' | 'Pause' | 'Play'\n\t| 'Props' | 'Select' | 'ZoomIn' | 'ZoomOut';\n\ntype DeviceKey =\n\t| 'BrightnessDown' | 'BrightnessUp' | 'Eject' | 'Hibernate'\n\t| 'LogOff' | 'Power' | 'PowerOff' | 'PrintScreen' | 'Standby' | 'WakeUp';\n\ntype IMEKey =\n\t| 'AllCandidates' | 'Alphanumeric' | 'CodeInput' | 'Compose' | 'Convert'\n\t| 'FinalMode' | 'GroupFirst' | 'GroupLast' | 'GroupNext' | 'GroupPrevious'\n\t| 'ModeChange' | 'NextCandidate' | 'NonConvert' | 'PreviousCandidate'\n\t| 'Process' | 'SingleCandidate'\n\t| 'HangulMode' | 'HanjaMode' | 'JunjaMode'\n\t| 'Eisu' | 'Hankaku' | 'Hiragana' | 'HiraganaKatakana' | 'KanaMode'\n\t| 'KanjiMode' | 'Katakana' | 'Romaji' | 'Zenkaku' | 'ZenkakuHankaku';\n\ntype FunctionKey =\n\t| `F${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24}`\n\t| 'Soft1' | 'Soft2' | 'Soft3' | 'Soft4';\n\ntype PhoneKey =\n\t| 'AppSwitch' | 'Call' | 'Camera' | 'CameraFocus' | 'EndCall'\n\t| 'GoBack' | 'GoHome' | 'HeadsetHook' | 'LastNumberRedial'\n\t| 'Notification' | 'MannerMode' | 'VoiceDial';\n\ntype MultimediaKey =\n\t| 'ChannelDown' | 'ChannelUp'\n\t| `Media${\n\t\t'FastForward' | 'Pause' | 'Play' | 'PlayPause'\n\t\t| 'Record' | 'Rewind' | 'Stop' | 'TrackNext' | 'TrackPrevious'\n\t}`;\n\ntype AudioKey =\n\t| `Audio${\n\t\t'BalanceLeft' | 'BalanceRight' | 'BassDown' | 'BassBoostDown'\n\t\t| 'BassBoostToggle' | 'BassBoostUp' | 'BassUp' | 'FaderFront' | 'FaderRear'\n\t\t| 'SurroundModeNext' | 'TrebleDown' | 'TrebleUp'\n\t\t| 'VolumeDown' | 'VolumeMute' | 'VolumeUp'\n\t}`\n\t| `Microphone${'Toggle' | 'VolumeDown' | 'VolumeMute' | 'VolumeUp'}`;\n\ntype TVKey =\n\t| 'TV'\n\t| `TV${\n\t\t'3DMode' | 'AntennaCable' | 'AudioDescription' | 'AudioDescriptionMixDown'\n\t\t| 'AudioDescriptionMixUp' | 'ContentsMenu' | 'DataService' | 'Input'\n\t\t| 'InputComponent1' | 'InputComponent2' | 'InputComposite1' | 'InputComposite2'\n\t\t| 'InputHDMI1' | 'InputHDMI2' | 'InputHDMI3' | 'InputHDMI4' | 'InputVGA1'\n\t\t| 'MediaContext' | 'Network' | 'NumberEntry' | 'Power' | 'RadioService'\n\t\t| 'Satellite' | 'SatelliteBS' | 'SatelliteCS' | 'SatelliteToggle'\n\t\t| 'TerrestrialAnalog' | 'TerrestrialDigital' | 'Timer'\n\t}`;\n\ntype MediaControllerKey =\n\t| 'AVRInput' | 'AVRPower'\n\t| `Color${'F0Red' | 'F1Green' | 'F2Yellow' | 'F3Blue' | 'F4Grey' | 'F5Brown'}`\n\t| 'ClosedCaptionToggle' | 'Dimmer' | 'DisplaySwap' | 'DVR' | 'Exit'\n\t| `Favorite${'Clear' | 'Recall' | 'Store'}${0 | 1 | 2 | 3}`\n\t| 'Guide' | 'GuideNextDay' | 'GuidePreviousDay' | 'Info' | 'InstantReplay'\n\t| 'Link' | 'ListProgram' | 'LiveContent' | 'Lock'\n\t| `Media${\n\t\t'Apps' | 'AudioTrack' | 'Last' | 'SkipBackward'\n\t\t| 'SkipForward' | 'StepBackward' | 'StepForward' | 'TopMenu'\n\t}`\n\t| `Navigate${'In' | 'Next' | 'Out' | 'Previous'}`\n\t| 'NextFavoriteChannel' | 'NextUserProfile' | 'OnDemand' | 'Pairing'\n\t| `PinP${'Down' | 'Move' | 'Toggle' | 'Up'}`\n\t| `PlaySpeed${'Down' | 'Reset' | 'Up'}`\n\t| 'RandomToggle' | 'RcLowBattery' | 'RecordSpeedNext' | 'RfBypass'\n\t| 'ScanChannelsToggle' | 'ScreenModeNext' | 'Settings' | 'SplitScreenToggle'\n\t| 'STBInput' | 'STBPower' | 'Subtitle' | 'Teletext'\n\t| 'VideoModeNext' | 'Wink' | 'ZoomToggle';\n\ntype SpeechKey = 'SpeechCorrectionList' | 'SpeechInputToggle';\n\ntype DocumentKey =\n\t| 'Close' | 'New' | 'Open' | 'Print' | 'Save' | 'SpellCheck'\n\t| 'MailForward' | 'MailReply' | 'MailSend';\n\ntype LaunchKey = `Launch${\n\t| 'Calculator' | 'Calendar' | 'Contacts' | 'Mail' | 'MediaPlayer'\n\t| 'MusicPlayer' | 'MyComputer' | 'Phone' | 'ScreenSaver' | 'Spreadsheet'\n\t| 'WebBrowser' | 'WebCam' | 'WordProcessor'\n\t| `Application${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16}`\n}`;\n\ntype BrowserKey = `Browser${'Back' | 'Favorites' | 'Forward' | 'Home' | 'Refresh' | 'Search' | 'Stop'}`;\n\ntype NumpadKey = 'Decimal' | 'Key11' | 'Key12' | 'Multiply' | 'Add' | 'Divide' | 'Subtract' | 'Separator';\n\nexport type KeyCode =\n\t| LowercaseLetter | Uppercase<LowercaseLetter> | Digit | Punctuation\n\t| ModifierKey | WhitespaceKey | NavigationKey | EditingKey | UIKey | DeviceKey\n\t| IMEKey | FunctionKey | PhoneKey | MultimediaKey | AudioKey | TVKey\n\t| MediaControllerKey | SpeechKey | DocumentKey | LaunchKey | BrowserKey | NumpadKey\n\t| 'Unidentified' | 'Dead';\n\nexport interface KeyboardState {\n\tisDown(key: KeyCode): boolean;\n\tjustPressed(key: KeyCode): boolean;\n\tjustReleased(key: KeyCode): boolean;\n}\n\nexport interface PointerState {\n\treadonly position: Readonly<Vec2>;\n\treadonly delta: Readonly<Vec2>;\n\tisDown(button: number): boolean;\n\tjustPressed(button: number): boolean;\n\tjustReleased(button: number): boolean;\n}\n\nexport interface ActionState {\n\tisActive(action: string): boolean;\n\tjustActivated(action: string): boolean;\n\tjustDeactivated(action: string): boolean;\n}\n\nexport interface InputState {\n\treadonly keyboard: KeyboardState;\n\treadonly pointer: PointerState;\n\treadonly actions: ActionState;\n\tsetActionMap(actions: ActionMap): void;\n\tgetActionMap(): Readonly<ActionMap>;\n}\n\nexport interface ActionBinding {\n\tkeys?: KeyCode[];\n\tbuttons?: number[];\n}\n\nexport type ActionMap = Record<string, ActionBinding>;\n\nexport interface InputResourceTypes {\n\tinputState: InputState;\n}\n\nexport interface InputBundleOptions {\n\t/** System group name (default: 'input') */\n\tsystemGroup?: string;\n\t/** Priority for input system (default: 100) */\n\tpriority?: number;\n\t/** Execution phase (default: 'preUpdate') */\n\tphase?: SystemPhase;\n\t/** Initial action mappings */\n\tactions?: ActionMap;\n\t/** EventTarget to attach listeners to (default: globalThis). Pass a custom target for testability. */\n\ttarget?: EventTarget;\n}\n\n// ==================== Helper Functions ====================\n\n/**\n * Define an action map with proper typing.\n *\n * @param map Object mapping action names to bindings\n * @returns The same map (identity, for convenience/documentation)\n *\n * @example\n * ```typescript\n * const actions = defineActionMap({\n * jump: { keys: [' ', 'ArrowUp'] },\n * shoot: { keys: ['z'], buttons: [0] },\n * });\n * ```\n */\nexport function defineActionMap<T extends ActionMap>(map: T): T {\n\treturn map;\n}\n\n/**\n * Create a single action binding.\n *\n * @param binding The binding configuration\n * @returns The same binding object\n */\nexport function createActionBinding(binding: ActionBinding): ActionBinding {\n\treturn binding;\n}\n\n// ==================== Internal Types ====================\n\ninterface RawInputState {\n\tkeysDown: Set<string>;\n\tkeysPressed: string[];\n\tkeysReleased: string[];\n\tbuttonsDown: Set<number>;\n\tbuttonsPressed: number[];\n\tbuttonsReleased: number[];\n\tpointerX: number;\n\tpointerY: number;\n\tpointerDeltaX: number;\n\tpointerDeltaY: number;\n\tlastPointerX: number;\n\tlastPointerY: number;\n\tpointerMoved: boolean;\n}\n\ninterface FrameSnapshot {\n\tkeysDown: ReadonlySet<string>;\n\tkeysPressed: ReadonlySet<string>;\n\tkeysReleased: ReadonlySet<string>;\n\tbuttonsDown: ReadonlySet<number>;\n\tbuttonsPressed: ReadonlySet<number>;\n\tbuttonsReleased: ReadonlySet<number>;\n\tpointerX: number;\n\tpointerY: number;\n\tpointerDeltaX: number;\n\tpointerDeltaY: number;\n\tactionsActive: ReadonlySet<string>;\n\tprevActionsActive: ReadonlySet<string>;\n}\n\n// ==================== Bundle Factory ====================\n\nfunction createRawInputState(): RawInputState {\n\treturn {\n\t\tkeysDown: new Set(),\n\t\tkeysPressed: [],\n\t\tkeysReleased: [],\n\t\tbuttonsDown: new Set(),\n\t\tbuttonsPressed: [],\n\t\tbuttonsReleased: [],\n\t\tpointerX: 0,\n\t\tpointerY: 0,\n\t\tpointerDeltaX: 0,\n\t\tpointerDeltaY: 0,\n\t\tlastPointerX: 0,\n\t\tlastPointerY: 0,\n\t\tpointerMoved: false,\n\t};\n}\n\nconst EMPTY_SET_STRING: ReadonlySet<string> = new Set<string>();\nconst EMPTY_SET_NUMBER: ReadonlySet<number> = new Set<number>();\n\nfunction createEmptySnapshot(): FrameSnapshot {\n\treturn {\n\t\tkeysDown: EMPTY_SET_STRING,\n\t\tkeysPressed: EMPTY_SET_STRING,\n\t\tkeysReleased: EMPTY_SET_STRING,\n\t\tbuttonsDown: EMPTY_SET_NUMBER,\n\t\tbuttonsPressed: EMPTY_SET_NUMBER,\n\t\tbuttonsReleased: EMPTY_SET_NUMBER,\n\t\tpointerX: 0,\n\t\tpointerY: 0,\n\t\tpointerDeltaX: 0,\n\t\tpointerDeltaY: 0,\n\t\tactionsActive: EMPTY_SET_STRING,\n\t\tprevActionsActive: EMPTY_SET_STRING,\n\t};\n}\n\nfunction computeActiveActions(\n\tactionMap: ActionMap,\n\tkeysDown: ReadonlySet<string>,\n\tbuttonsDown: ReadonlySet<number>,\n): Set<string> {\n\tconst active = new Set<string>();\n\tfor (const [name, binding] of Object.entries(actionMap)) {\n\t\tconst keyActive = binding.keys?.some((k) => keysDown.has(k)) ?? false;\n\t\tconst buttonActive = binding.buttons?.some((b) => buttonsDown.has(b)) ?? false;\n\t\tif (keyActive || buttonActive) {\n\t\t\tactive.add(name);\n\t\t}\n\t}\n\treturn active;\n}\n\nfunction snapshotRaw(raw: RawInputState, prevActionsActive: ReadonlySet<string>, actionMap: ActionMap): FrameSnapshot {\n\tconst keysDown = new Set(raw.keysDown);\n\tconst keysPressed = new Set(raw.keysPressed);\n\tconst keysReleased = new Set(raw.keysReleased);\n\tconst buttonsDown = new Set(raw.buttonsDown);\n\tconst buttonsPressed = new Set(raw.buttonsPressed);\n\tconst buttonsReleased = new Set(raw.buttonsReleased);\n\n\tconst pointerDeltaX = raw.pointerMoved ? raw.pointerX - raw.lastPointerX : 0;\n\tconst pointerDeltaY = raw.pointerMoved ? raw.pointerY - raw.lastPointerY : 0;\n\n\tconst actionsActive = computeActiveActions(actionMap, keysDown, buttonsDown);\n\n\tconst snapshot: FrameSnapshot = {\n\t\tkeysDown,\n\t\tkeysPressed,\n\t\tkeysReleased,\n\t\tbuttonsDown,\n\t\tbuttonsPressed,\n\t\tbuttonsReleased,\n\t\tpointerX: raw.pointerX,\n\t\tpointerY: raw.pointerY,\n\t\tpointerDeltaX,\n\t\tpointerDeltaY,\n\t\tactionsActive,\n\t\tprevActionsActive,\n\t};\n\n\t// Clear accumulation buffers\n\traw.keysPressed = [];\n\traw.keysReleased = [];\n\traw.buttonsPressed = [];\n\traw.buttonsReleased = [];\n\traw.lastPointerX = raw.pointerX;\n\traw.lastPointerY = raw.pointerY;\n\traw.pointerMoved = false;\n\n\treturn snapshot;\n}\n\n/**\n * Create an input bundle for ECSpresso.\n *\n * This bundle provides:\n * - Frame-accurate keyboard state (isDown, justPressed, justReleased)\n * - Pointer position/delta and button state (mouse + touch via PointerEvent)\n * - Named action mapping with runtime remapping\n * - Automatic listener cleanup on detach\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso\n * .create<Components, Events, Resources>()\n * .withBundle(createInputBundle({\n * actions: {\n * jump: { keys: [' ', 'ArrowUp'] },\n * shoot: { keys: ['z'], buttons: [0] },\n * },\n * }))\n * .build();\n *\n * // In a system:\n * const input = ecs.getResource('inputState');\n * if (input.actions.justActivated('jump')) { ... }\n * if (input.keyboard.isDown('ArrowRight')) { ... }\n * ```\n */\nexport function createInputBundle(\n\toptions?: InputBundleOptions\n): Bundle<{}, {}, InputResourceTypes> {\n\tconst {\n\t\tsystemGroup = 'input',\n\t\tpriority = 100,\n\t\tphase = 'preUpdate',\n\t\tactions: initialActions = {},\n\t\ttarget = globalThis,\n\t} = options ?? {};\n\n\t// Closure state\n\tconst raw = createRawInputState();\n\tlet snapshot = createEmptySnapshot();\n\tlet actionMap: ActionMap = { ...initialActions };\n\tconst cleanupFns: Array<() => void> = [];\n\n\t// The position/delta objects exposed via the resource.\n\t// Updated in-place each frame to avoid allocations.\n\tconst position: Vec2 = { x: 0, y: 0 };\n\tconst delta: Vec2 = { x: 0, y: 0 };\n\n\t// Build the InputState resource that closes over snapshot\n\tconst keyboard: KeyboardState = {\n\t\tisDown: (key) => snapshot.keysDown.has(key),\n\t\tjustPressed: (key) => snapshot.keysPressed.has(key),\n\t\tjustReleased: (key) => snapshot.keysReleased.has(key),\n\t};\n\n\tconst pointer: PointerState = {\n\t\tposition,\n\t\tdelta,\n\t\tisDown: (button) => snapshot.buttonsDown.has(button),\n\t\tjustPressed: (button) => snapshot.buttonsPressed.has(button),\n\t\tjustReleased: (button) => snapshot.buttonsReleased.has(button),\n\t};\n\n\tconst actionState: ActionState = {\n\t\tisActive: (action) => snapshot.actionsActive.has(action),\n\t\tjustActivated: (action) =>\n\t\t\tsnapshot.actionsActive.has(action) && !snapshot.prevActionsActive.has(action),\n\t\tjustDeactivated: (action) =>\n\t\t\t!snapshot.actionsActive.has(action) && snapshot.prevActionsActive.has(action),\n\t};\n\n\tconst inputState: InputState = {\n\t\tkeyboard,\n\t\tpointer,\n\t\tactions: actionState,\n\t\tsetActionMap(newMap) {\n\t\t\tactionMap = { ...newMap };\n\t\t},\n\t\tgetActionMap() {\n\t\t\treturn { ...actionMap };\n\t\t},\n\t};\n\n\t// DOM event handlers\n\tfunction onKeyDown(e: Event) {\n\t\tconst ke = e as KeyboardEvent;\n\t\tif (ke.repeat) return;\n\t\traw.keysDown.add(ke.key);\n\t\traw.keysPressed.push(ke.key);\n\t}\n\n\tfunction onKeyUp(e: Event) {\n\t\tconst ke = e as KeyboardEvent;\n\t\traw.keysDown.delete(ke.key);\n\t\traw.keysReleased.push(ke.key);\n\t}\n\n\tfunction onPointerDown(e: Event) {\n\t\tconst pe = e as unknown as PointerEvent;\n\t\traw.buttonsDown.add(pe.button);\n\t\traw.buttonsPressed.push(pe.button);\n\t}\n\n\tfunction onPointerMove(e: Event) {\n\t\tconst pe = e as unknown as PointerEvent;\n\t\traw.pointerX = pe.clientX;\n\t\traw.pointerY = pe.clientY;\n\t\traw.pointerMoved = true;\n\t}\n\n\tfunction onPointerUp(e: Event) {\n\t\tconst pe = e as unknown as PointerEvent;\n\t\traw.buttonsDown.delete(pe.button);\n\t\traw.buttonsReleased.push(pe.button);\n\t}\n\n\tfunction addListener(type: string, handler: (e: Event) => void) {\n\t\ttarget.addEventListener(type, handler);\n\t\tcleanupFns.push(() => target.removeEventListener(type, handler));\n\t}\n\n\t// Build bundle\n\tconst bundle = new Bundle<{}, {}, InputResourceTypes>('input');\n\n\tbundle.addResource('inputState', inputState);\n\n\tbundle\n\t\t.addSystem('input-state')\n\t\t.setPriority(priority)\n\t\t.inPhase(phase)\n\t\t.inGroup(systemGroup)\n\t\t.setOnInitialize(() => {\n\t\t\taddListener('keydown', onKeyDown);\n\t\t\taddListener('keyup', onKeyUp);\n\t\t\taddListener('pointerdown', onPointerDown);\n\t\t\taddListener('pointermove', onPointerMove);\n\t\t\taddListener('pointerup', onPointerUp);\n\t\t})\n\t\t.setOnDetach(() => {\n\t\t\tfor (const cleanup of cleanupFns) {\n\t\t\t\tcleanup();\n\t\t\t}\n\t\t\tcleanupFns.length = 0;\n\t\t})\n\t\t.setProcess(() => {\n\t\t\tconst prevActionsActive = snapshot.actionsActive;\n\t\t\tsnapshot = snapshotRaw(raw, prevActionsActive, actionMap);\n\n\t\t\t// Update the exposed position/delta objects in-place\n\t\t\tposition.x = snapshot.pointerX;\n\t\t\tposition.y = snapshot.pointerY;\n\t\t\tdelta.x = snapshot.pointerDeltaX;\n\t\t\tdelta.y = snapshot.pointerDeltaY;\n\t\t})\n\t\t.and();\n\n\treturn bundle;\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "47D459776A55946564756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Provides velocity → localTransform integration for entities.
|
|
5
5
|
* Works with the transform bundle's localTransform/worldTransform system.
|
|
6
6
|
*/
|
|
7
|
-
import Bundle from '
|
|
8
|
-
import type { SystemPhase } from '
|
|
7
|
+
import { Bundle } from 'ecspresso';
|
|
8
|
+
import type { SystemPhase } from 'ecspresso';
|
|
9
9
|
import type { TransformComponentTypes } from './transform';
|
|
10
10
|
/**
|
|
11
11
|
* Velocity component data structure.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
var Q=((j)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(j,{get:(k,z)=>(typeof require<"u"?require:k)[z]}):j)(function(j){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+j+'" is not supported')});import{Bundle as O}from"ecspresso";function X(j,k){return{velocity:{x:j,y:k}}}function Y(j){let{systemGroup:k="physics",priority:z=1000,phase:J="fixedUpdate"}=j??{},A=new O("movement");return A.addSystem("movement").setPriority(z).inPhase(J).inGroup(k).addQuery("movingEntities",{with:["localTransform","velocity"]}).setProcess((K,D,L)=>{for(let F of K.movingEntities){let{localTransform:H,velocity:I}=F.components;H.x+=I.x*D,H.y+=I.y*D,L.markChanged(F.id,"localTransform")}}).and(),A}export{X as createVelocity,Y as createMovementBundle};
|
|
2
|
+
|
|
3
|
+
//# debugId=3E601B940224B76564756E2164756E21
|
|
4
|
+
//# sourceMappingURL=movement.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/bundles/utils/movement.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * Movement Bundle for ECSpresso\n *\n * Provides velocity → localTransform integration for entities.\n * Works with the transform bundle's localTransform/worldTransform system.\n */\n\nimport { Bundle } from 'ecspresso';\nimport type { SystemPhase } from 'ecspresso';\nimport type { TransformComponentTypes } from './transform';\n\n// ==================== Component Types ====================\n\n/**\n * Velocity component data structure.\n */\nexport interface Velocity {\n\tx: number;\n\ty: number;\n}\n\n/**\n * Component types provided by the movement bundle.\n * Extend your component types with this interface.\n *\n * @example\n * ```typescript\n * interface GameComponents extends TransformComponentTypes, MovementComponentTypes {\n * sprite: Sprite;\n * player: boolean;\n * }\n * ```\n */\nexport interface MovementComponentTypes extends TransformComponentTypes {\n\tvelocity: Velocity;\n}\n\n// ==================== Bundle Options ====================\n\n/**\n * Configuration options for the movement bundle.\n */\nexport interface MovementBundleOptions {\n\t/** System group name (default: 'physics') */\n\tsystemGroup?: string;\n\t/** Priority for movement update system (default: 1000, runs early before transform propagation) */\n\tpriority?: number;\n\t/** Execution phase (default: 'fixedUpdate') */\n\tphase?: SystemPhase;\n}\n\n// ==================== Helper Functions ====================\n\n/**\n * Create a velocity component.\n *\n * @param x The x velocity\n * @param y The y velocity\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createTransform(100, 200),\n * ...createVelocity(50, -25),\n * projectile: true,\n * });\n * ```\n */\nexport function createVelocity(x: number, y: number): Pick<MovementComponentTypes, 'velocity'> {\n\treturn {\n\t\tvelocity: { x, y },\n\t};\n}\n\n// ==================== Bundle Factory ====================\n\n/**\n * Create a movement bundle for ECSpresso.\n *\n * This bundle provides:\n * - Movement update system that integrates velocity into localTransform\n * - Processes all entities with both localTransform and velocity components\n *\n * Note: This bundle modifies localTransform. The transform bundle's propagation\n * system will then compute worldTransform for use by other systems.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso\n * .create<Components, Events, Resources>()\n * .withBundle(createTransformBundle())\n * .withBundle(createMovementBundle())\n * .build();\n *\n * // Spawn entity with movement\n * ecs.spawn({\n * ...createTransform(100, 200),\n * ...createVelocity(50, -25),\n * sprite,\n * });\n * ```\n */\nexport function createMovementBundle(\n\toptions?: MovementBundleOptions\n): Bundle<MovementComponentTypes, {}, {}> {\n\tconst {\n\t\tsystemGroup = 'physics',\n\t\tpriority = 1000,\n\t\tphase = 'fixedUpdate',\n\t} = options ?? {};\n\n\tconst bundle = new Bundle<MovementComponentTypes, {}, {}>('movement');\n\n\tbundle\n\t\t.addSystem('movement')\n\t\t.setPriority(priority)\n\t\t.inPhase(phase)\n\t\t.inGroup(systemGroup)\n\t\t.addQuery('movingEntities', {\n\t\t\twith: ['localTransform', 'velocity'],\n\t\t})\n\t\t.setProcess((queries, deltaTime, ecs) => {\n\t\t\tfor (const entity of queries.movingEntities) {\n\t\t\t\tconst { localTransform, velocity } = entity.components;\n\t\t\t\tlocalTransform.x += velocity.x * deltaTime;\n\t\t\t\tlocalTransform.y += velocity.y * deltaTime;\n\t\t\t\tecs.markChanged(entity.id, 'localTransform');\n\t\t\t}\n\t\t})\n\t\t.and();\n\n\treturn bundle;\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "2PAOA,iBAAS,kBA8DF,SAAS,CAAc,CAAC,EAAW,EAAqD,CAC9F,MAAO,CACN,SAAU,CAAE,IAAG,GAAE,CAClB,EA+BM,SAAS,CAAoB,CACnC,EACyC,CACzC,IACC,cAAc,UACd,WAAW,KACX,QAAQ,eACL,GAAW,CAAC,EAEV,EAAS,IAAI,EAAuC,UAAU,EAoBpE,OAlBA,EACE,UAAU,UAAU,EACpB,YAAY,CAAQ,EACpB,QAAQ,CAAK,EACb,QAAQ,CAAW,EACnB,SAAS,iBAAkB,CAC3B,KAAM,CAAC,iBAAkB,UAAU,CACpC,CAAC,EACA,WAAW,CAAC,EAAS,EAAW,IAAQ,CACxC,QAAW,KAAU,EAAQ,eAAgB,CAC5C,IAAQ,iBAAgB,YAAa,EAAO,WAC5C,EAAe,GAAK,EAAS,EAAI,EACjC,EAAe,GAAK,EAAS,EAAI,EACjC,EAAI,YAAY,EAAO,GAAI,gBAAgB,GAE5C,EACA,IAAI,EAEC",
|
|
8
|
+
"debugId": "3E601B940224B76564756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Provides ECS-native timers following the "data, not callbacks" philosophy.
|
|
5
5
|
* Timers are components processed each frame, automatically cleaned up when entities are removed.
|
|
6
6
|
*/
|
|
7
|
-
import Bundle from '
|
|
8
|
-
import type { SystemPhase } from '
|
|
7
|
+
import { Bundle } from 'ecspresso';
|
|
8
|
+
import type { SystemPhase } from 'ecspresso';
|
|
9
9
|
/**
|
|
10
10
|
* Data structure published when a timer completes.
|
|
11
11
|
* Use this type when defining timer completion events in your EventTypes interface.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
1
|
+
var Q=((k)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(k,{get:(z,F)=>(typeof require<"u"?require:z)[F]}):k)(function(k){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+k+'" is not supported')});import{Bundle as O}from"ecspresso";function V(k,z){return{timer:{elapsed:0,duration:k,repeat:!1,active:!0,justFinished:!1,onComplete:z?.onComplete}}}function W(k,z){return{timer:{elapsed:0,duration:k,repeat:!0,active:!0,justFinished:!1,onComplete:z?.onComplete}}}function X(k){let{systemGroup:z="timers",priority:F=0,phase:M="preUpdate"}=k??{},K=new O("timers");K.addSystem("timer-update").setPriority(F).inPhase(M).inGroup(z).addQuery("timers",{with:["timer"]}).setProcess((H,J,x)=>{for(let A of H.timers){let{timer:j}=A.components;if(j.justFinished=!1,!j.active)continue;if(j.elapsed+=J,j.elapsed<j.duration)continue;if(j.repeat)while(j.elapsed>=j.duration)j.justFinished=!0,L(x,A.id,j),j.elapsed-=j.duration;else j.justFinished=!0,L(x,A.id,j),j.active=!1,x.commands.removeEntity(A.id)}}).and();function L(H,J,x){if(!x.onComplete)return;let A={entityId:J,duration:x.duration,elapsed:x.elapsed};H.eventBus.publish(x.onComplete,A)}return K}export{X as createTimerBundle,V as createTimer,W as createRepeatingTimer};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=3CE4B9B931E088F464756E2164756E21
|
|
4
4
|
//# sourceMappingURL=timers.js.map
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/
|
|
3
|
+
"sources": ["../src/bundles/utils/timers.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"import Bundle from \"./bundle\";\nimport ECSpresso from \"./ecspresso\";\nimport type { FilteredEntity, System, SystemPhase } from \"./types\";\n\n/**\n * Builder class for creating type-safe ECS Systems with proper query inference\n */\nexport class SystemBuilder<\n\tComponentTypes extends Record<string, any> = Record<string, any>,\n\tEventTypes extends Record<string, any> = Record<string, any>,\n\tResourceTypes extends Record<string, any> = Record<string, any>,\n\tQueries extends Record<string, QueryDefinition<ComponentTypes>> = {},\n> {\n\tprivate queries: Queries = {} as Queries;\n\tprivate processFunction?: ProcessFunction<ComponentTypes, EventTypes, ResourceTypes, Queries>;\n\tprivate detachFunction?: LifecycleFunction<ComponentTypes, EventTypes, ResourceTypes>;\n\tprivate initializeFunction?: LifecycleFunction<ComponentTypes, EventTypes, ResourceTypes>;\n\tprivate eventHandlers?: {\n\t\t[EventName in keyof EventTypes]?: {\n\t\t\thandler(\n\t\t\t\tdata: EventTypes[EventName],\n\t\t\t\tecs: ECSpresso<\n\t\t\t\t\tComponentTypes,\n\t\t\t\t\tEventTypes,\n\t\t\t\t\tResourceTypes\n\t\t\t\t>,\n\t\t\t): void;\n\t\t};\n\t};\n\tprivate _priority = 0; // Default priority is 0\n\tprivate _phase: SystemPhase = 'update'; // Default phase is 'update'\n\tprivate _isRegistered = false; // Track if system has been auto-registered\n\tprivate _groups: string[] = [];\n\tprivate _inScreens?: string[];\n\tprivate _excludeScreens?: string[];\n\tprivate _requiredAssets?: string[];\n\n\tconstructor(\n\t\tprivate _label: string,\n\t\tprivate _ecspresso: ECSpresso<ComponentTypes, EventTypes, ResourceTypes> | null = null,\n\t\tprivate _bundle: Bundle<ComponentTypes, EventTypes, ResourceTypes> | null = null,\n\t) {}\n\n\tget label() {\n\t\treturn this._label;\n\t}\n\n\t/**\n\t * Returns the associated bundle if one was provided in the constructor\n\t */\n\tget bundle() {\n\t\treturn this._bundle;\n\t}\n\n\t/**\n\t * Returns the associated ECSpresso instance if one was provided in the constructor\n\t */\n\tget ecspresso() {\n\t\treturn this._ecspresso;\n\t}\n\n\t/**\n\t * Auto-register this system with its ECSpresso instance if not already registered\n\t * @private\n\t */\n\tprivate _autoRegister(): void {\n\t\tif (this._isRegistered || !this._ecspresso) return;\n\t\t\n\t\tconst system = this._buildSystemObject();\n\t\tregisterSystemWithEcspresso(system, this._ecspresso);\n\t\tthis._isRegistered = true;\n\t}\n\n\t/**\n\t * Create the system object without registering it\n\t * @private\n\t */\n\tprivate _buildSystemObject(): System<ComponentTypes, any, any, EventTypes, ResourceTypes> {\n\t\treturn this._createSystemObject();\n\t}\n\n\t/**\n\t * Create a system object with all configured properties\n\t * @private\n\t */\n\tprivate _createSystemObject(): System<ComponentTypes, any, any, EventTypes, ResourceTypes> {\n\t\tconst system: System<ComponentTypes, any, any, EventTypes, ResourceTypes> = {\n\t\t\tlabel: this._label,\n\t\t\tentityQueries: this.queries,\n\t\t\tpriority: this._priority,\n\t\t\tphase: this._phase,\n\t\t};\n\n\t\tif (this.processFunction) {\n\t\t\tsystem.process = this.processFunction;\n\t\t}\n\n\t\tif (this.detachFunction) {\n\t\t\tsystem.onDetach = this.detachFunction;\n\t\t}\n\n\t\tif (this.initializeFunction) {\n\t\t\tsystem.onInitialize = this.initializeFunction;\n\t\t}\n\n\t\tif (this.eventHandlers) {\n\t\t\tsystem.eventHandlers = this.eventHandlers;\n\t\t}\n\n\t\tif (this._groups.length > 0) {\n\t\t\tsystem.groups = [...this._groups];\n\t\t}\n\n\t\tif (this._inScreens) {\n\t\t\tsystem.inScreens = this._inScreens;\n\t\t}\n\n\t\tif (this._excludeScreens) {\n\t\t\tsystem.excludeScreens = this._excludeScreens;\n\t\t}\n\n\t\tif (this._requiredAssets) {\n\t\t\tsystem.requiredAssets = this._requiredAssets;\n\t\t}\n\n\t\treturn system;\n\t}\n\n\t// TODO: Should this be a setter?\n\t/**\n\t * Set the priority of this system. Systems with higher priority values\n\t * execute before those with lower values. Systems with the same priority\n\t * execute in the order they were registered.\n\t * @param priority The priority value (default: 0)\n\t * @returns This SystemBuilder instance for method chaining\n\t */\n\tsetPriority(priority: number): this {\n\t\tthis._priority = priority;\n\t\treturn this;\n\t}\n\n\t/**\n\t * Set the execution phase for this system.\n\t * Systems are grouped by phase and executed in order:\n\t * preUpdate -> fixedUpdate -> update -> postUpdate -> render\n\t * @param phase The phase to assign this system to (default: 'update')\n\t * @returns This SystemBuilder instance for method chaining\n\t */\n\tinPhase(phase: SystemPhase): this {\n\t\tthis._phase = phase;\n\t\treturn this;\n\t}\n\n\t/**\n\t * Add this system to a group. Systems can belong to multiple groups.\n\t * When any group a system belongs to is disabled, the system will be skipped.\n\t * @param groupName The name of the group to add the system to\n\t * @returns This SystemBuilder instance for method chaining\n\t */\n\tinGroup(groupName: string): this {\n\t\tif (!this._groups.includes(groupName)) {\n\t\t\tthis._groups.push(groupName);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * Restrict this system to only run in specified screens.\n\t * System will be skipped during update() when the current screen\n\t * is not in this list.\n\t * @param screens Array of screen names where this system should run\n\t * @returns This SystemBuilder instance for method chaining\n\t */\n\tinScreens(screens: ReadonlyArray<string>): this {\n\t\tthis._inScreens = [...screens];\n\t\treturn this;\n\t}\n\n\t/**\n\t * Exclude this system from running in specified screens.\n\t * System will be skipped during update() when the current screen\n\t * is in this list.\n\t * @param screens Array of screen names where this system should NOT run\n\t * @returns This SystemBuilder instance for method chaining\n\t */\n\texcludeScreens(screens: ReadonlyArray<string>): this {\n\t\tthis._excludeScreens = [...screens];\n\t\treturn this;\n\t}\n\n\t/**\n\t * Require specific assets to be loaded for this system to run.\n\t * System will be skipped during update() if any required asset\n\t * is not loaded.\n\t * @param assets Array of asset keys that must be loaded\n\t * @returns This SystemBuilder instance for method chaining\n\t */\n\trequiresAssets(assets: ReadonlyArray<string>): this {\n\t\tthis._requiredAssets = [...assets];\n\t\treturn this;\n\t}\n\n\t/**\n\t * Add a query definition to the system\n\t */\n\taddQuery<\n\t\tQueryName extends string,\n\t\tWithComponents extends keyof ComponentTypes,\n\t\tWithoutComponents extends keyof ComponentTypes = never,\n\t\tNewQueries extends Queries & Record<QueryName, QueryDefinition<ComponentTypes, WithComponents, WithoutComponents>> =\n\t\t\tQueries & Record<QueryName, QueryDefinition<ComponentTypes, WithComponents, WithoutComponents>>\n\t>(\n\t\tname: QueryName,\n\t\tdefinition: {\n\t\t\twith: ReadonlyArray<WithComponents>;\n\t\t\twithout?: ReadonlyArray<WithoutComponents>;\n\t\t\tchanged?: ReadonlyArray<WithComponents>;\n\t\t}\n\t): this extends SystemBuilderWithEcspresso<ComponentTypes, EventTypes, ResourceTypes, Queries>\n\t\t? SystemBuilderWithEcspresso<ComponentTypes, EventTypes, ResourceTypes, NewQueries>\n\t\t: this extends SystemBuilderWithBundle<ComponentTypes, EventTypes, ResourceTypes, Queries>\n\t\t\t? SystemBuilderWithBundle<ComponentTypes, EventTypes, ResourceTypes, NewQueries>\n\t\t\t: SystemBuilder<ComponentTypes, EventTypes, ResourceTypes, NewQueries> {\n\t\t// Cast is needed because TypeScript can't preserve the type information\n\t\t// when modifying an object property\n\t\tconst newBuilder = this as any;\n\t\tnewBuilder.queries = {\n\t\t\t...this.queries,\n\t\t\t[name]: definition,\n\t\t};\n\t\treturn newBuilder;\n\t}\n\n\t/**\n\t * Set the system's process function that runs each update\n\t * @param process Function to process entities matching the system's queries each update\n\t * @returns This SystemBuilder instance for method chaining\n\t */\n\tsetProcess(\n\t\tprocess: ProcessFunction<ComponentTypes, EventTypes, ResourceTypes, Queries>\n\t): this {\n\t\tthis.processFunction = process;\n\t\treturn this;\n\t}\n\n\t/**\n\t * Register this system with its ECSpresso instance and return the ECSpresso for chaining\n\t * This enables seamless method chaining: .registerAndContinue().addSystem(...)\n\t * @returns ECSpresso instance if attached to one, otherwise throws an error\n\t */\n\tregisterAndContinue(): ECSpresso<ComponentTypes, EventTypes, ResourceTypes> {\n\t\tif (!this._ecspresso) {\n\t\t\tthrow new Error(`Cannot register system '${this._label}': SystemBuilder is not attached to an ECSpresso instance. Use Bundle.addSystem() or ECSpresso.addSystem() instead.`);\n\t\t}\n\t\t\n\t\tthis._autoRegister();\n\t\treturn this._ecspresso;\n\t}\n\n\t/**\n\t * Complete this system and return the parent container for seamless chaining\n\t * - For ECSpresso-attached builders: registers the system and returns ECSpresso\n\t * - For Bundle-attached builders: returns the Bundle\n\t * This method is typed via the specialized interfaces (SystemBuilderWithEcspresso, SystemBuilderWithBundle)\n\t */\n\tand(): ECSpresso<ComponentTypes, EventTypes, ResourceTypes> | Bundle<ComponentTypes, EventTypes, ResourceTypes> {\n\t\tif (this._ecspresso) {\n\t\t\tthis._autoRegister();\n\t\t\treturn this._ecspresso;\n\t\t}\n\n\t\tif (this._bundle) {\n\t\t\treturn this._bundle;\n\t\t}\n\n\t\tthrow new Error(`Cannot use and() on system '${this._label}': not attached to ECSpresso or Bundle.`);\n\t}\n\n\t/**\n\t * Set the onDetach lifecycle hook\n\t * Called when the system is removed from the ECS\n\t * @param onDetach Function to run when this system is detached from the ECS\n\t * @returns This SystemBuilder instance for method chaining\n\t */\n\tsetOnDetach(\n\t\tonDetach: LifecycleFunction<ComponentTypes, EventTypes, ResourceTypes>\n\t): this {\n\t\tthis.detachFunction = onDetach;\n\t\treturn this;\n\t}\n\n\t/**\n\t * Set the onInitialize lifecycle hook\n\t * Called when the system is initialized via ECSpresso.initialize() method\n\t * @param onInitialize Function to run when this system is initialized\n\t * @returns This SystemBuilder instance for method chaining\n\t */\n\tsetOnInitialize(\n\t\tonInitialize: LifecycleFunction<ComponentTypes, EventTypes, ResourceTypes>\n\t): this {\n\t\tthis.initializeFunction = onInitialize;\n\t\treturn this;\n\t}\n\n\t/**\n\t * Set event handlers for the system\n\t * These handlers will be automatically subscribed when the system is attached\n\t * @param handlers Object mapping event names to handler functions\n\t * @returns This SystemBuilder instance for method chaining\n\t */\n\tsetEventHandlers(\n\t\thandlers: {\n\t\t\t[EventName in keyof EventTypes]?: {\n\t\t\t\thandler(\n\t\t\t\t\tdata: EventTypes[EventName],\n\t\t\t\t\tecs: ECSpresso<ComponentTypes, EventTypes, ResourceTypes>\n\t\t\t\t): void;\n\t\t\t};\n\t\t}\n\t): this {\n\t\tthis.eventHandlers = handlers;\n\t\treturn this;\n\t}\n\n\t/**\n\t * Build the final system object\n\t */\n\tbuild(ecspresso?: ECSpresso<ComponentTypes, EventTypes, ResourceTypes>) {\n\t\tconst system = this._createSystemObject();\n\n\t\tif (this._ecspresso) {\n\t\t\tregisterSystemWithEcspresso(system, this._ecspresso);\n\t\t}\n\n\t\tif(ecspresso) {\n\t\t\tregisterSystemWithEcspresso(system, ecspresso);\n\t\t}\n\n\t\treturn this;\n\t}\n}\n\n/**\n * Helper function to register a system with an ECSpresso instance\n * This handles attaching the system and setting up event handlers\n * @internal Used by SystemBuilder and Bundle\n */\nexport function registerSystemWithEcspresso<\n\tComponentTypes extends Record<string, any>,\n\tEventTypes extends Record<string, any>,\n\tResourceTypes extends Record<string, any>\n>(\n\tsystem: System<ComponentTypes, any, any, EventTypes, ResourceTypes>,\n\tecspresso: ECSpresso<ComponentTypes, EventTypes, ResourceTypes>\n) {\n\t// Use the new internal registration method instead of direct property access\n\tecspresso._registerSystem(system);\n}\n\n// Helper type definitions\ntype QueryDefinition<\n\tComponentTypes,\n\tWithComponents extends keyof ComponentTypes = any,\n\tWithoutComponents extends keyof ComponentTypes = any,\n> = {\n\twith: ReadonlyArray<WithComponents>;\n\twithout?: ReadonlyArray<WithoutComponents>;\n\tchanged?: ReadonlyArray<WithComponents>;\n};\n\ntype QueryResults<\n\tComponentTypes,\n\tQueries extends Record<string, QueryDefinition<ComponentTypes>>,\n> = {\n\t[QueryName in keyof Queries]: QueryName extends string\n\t\t? FilteredEntity<\n\t\t\tComponentTypes,\n\t\t\tQueries[QueryName] extends QueryDefinition<ComponentTypes, infer W, any> ? W : never,\n\t\t\tQueries[QueryName] extends QueryDefinition<ComponentTypes, any, infer WO> ? WO : never\n\t\t>[]\n\t\t: never;\n};\n\n/**\n * Function signature for system process methods\n * @param queries Results of entity queries defined by the system\n * @param deltaTime Time elapsed since last update in seconds\n * @param ecs The ECSpresso instance providing access to all ECS functionality\n */\ntype ProcessFunction<\n\tComponentTypes extends Record<string, any>,\n\tEventTypes extends Record<string, any>,\n\tResourceTypes extends Record<string, any>,\n\tQueries extends Record<string, QueryDefinition<ComponentTypes>>,\n> = (\n\tqueries: QueryResults<ComponentTypes, Queries>,\n\tdeltaTime: number,\n\tecs: ECSpresso<\n\t\tComponentTypes,\n\t\tEventTypes,\n\t\tResourceTypes\n\t>\n) => void;\n\n/**\n * Type for system initialization functions\n * These can be asynchronous\n */\ntype LifecycleFunction<\n\tComponentTypes extends Record<string, any>,\n\tEventTypes extends Record<string, any>,\n\tResourceTypes extends Record<string, any>,\n> = (\n\tecs: ECSpresso<\n\t\tComponentTypes,\n\t\tEventTypes,\n\t\tResourceTypes\n\t>,\n) => void | Promise<void>;\n\n/**\n * Create a SystemBuilder attached to an ECSpresso instance\n * Helper function used by ECSpresso.addSystem\n */\nexport function createEcspressoSystemBuilder<\n\tComponentTypes extends Record<string, any>,\n\tEventTypes extends Record<string, any>,\n\tResourceTypes extends Record<string, any>\n>(\n\tlabel: string,\n\tecspresso: ECSpresso<ComponentTypes, EventTypes, ResourceTypes>\n): SystemBuilderWithEcspresso<ComponentTypes, EventTypes, ResourceTypes> {\n\treturn new SystemBuilder<ComponentTypes, EventTypes, ResourceTypes>(\n\t\tlabel,\n\t\tecspresso\n\t) as SystemBuilderWithEcspresso<ComponentTypes, EventTypes, ResourceTypes>;\n}\n\n/**\n * Create a SystemBuilder attached to a Bundle\n * Helper function used by Bundle.addSystem\n */\nexport function createBundleSystemBuilder<\n\tComponentTypes extends Record<string, any>,\n\tEventTypes extends Record<string, any>,\n\tResourceTypes extends Record<string, any>\n>(\n\tlabel: string,\n\tbundle: Bundle<ComponentTypes, EventTypes, ResourceTypes>\n): SystemBuilderWithBundle<ComponentTypes, EventTypes, ResourceTypes> {\n\treturn new SystemBuilder<ComponentTypes, EventTypes, ResourceTypes>(\n\t\tlabel,\n\t\tnull,\n\t\tbundle\n\t) as SystemBuilderWithBundle<ComponentTypes, EventTypes, ResourceTypes>;\n}\n\n// Type interfaces for specialized SystemBuilders\n\n/**\n * SystemBuilder with a guaranteed non-null reference to an ECSpresso instance\n */\nexport interface SystemBuilderWithEcspresso<\n\tComponentTypes extends Record<string, any>,\n\tEventTypes extends Record<string, any>,\n\tResourceTypes extends Record<string, any>,\n\tQueries extends Record<string, QueryDefinition<ComponentTypes>> = {}\n> extends SystemBuilder<ComponentTypes, EventTypes, ResourceTypes, Queries> {\n\treadonly ecspresso: ECSpresso<ComponentTypes, EventTypes, ResourceTypes>;\n\t\n\t/**\n\t * Complete this system and return ECSpresso for seamless chaining\n\t * Automatically registers the system when called\n\t */\n\tand(): ECSpresso<ComponentTypes, EventTypes, ResourceTypes>;\n}\n\n/**\n * SystemBuilder with a guaranteed non-null reference to a Bundle\n */\nexport interface SystemBuilderWithBundle<\n\tComponentTypes extends Record<string, any>,\n\tEventTypes extends Record<string, any>,\n\tResourceTypes extends Record<string, any>,\n\tQueries extends Record<string, QueryDefinition<ComponentTypes>> = {}\n> extends SystemBuilder<ComponentTypes, EventTypes, ResourceTypes, Queries> {\n\treadonly bundle: Bundle<ComponentTypes, EventTypes, ResourceTypes>;\n\n\t/**\n\t * Complete this system and return the Bundle for chaining\n\t * Enables fluent API: bundle.addSystem(...).and().addSystem(...)\n\t */\n\tand(): Bundle<ComponentTypes, EventTypes, ResourceTypes>;\n}\n",
|
|
6
|
-
"import { createBundleSystemBuilder, SystemBuilderWithBundle } from './system-builder';\nimport type ECSpresso from './ecspresso';\nimport type { AssetDefinition } from './asset-types';\nimport type { ScreenDefinition } from './screen-types';\nimport type { BundlesAreCompatible } from './type-utils';\n\n/**\n * Generates a unique ID for a bundle\n */\nfunction generateBundleId(): string {\n\treturn `bundle_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 9)}`;\n}\n\n/**\n * Bundle class that encapsulates a set of components, resources, events, and systems\n * that can be merged into a ECSpresso instance\n */\nexport default class Bundle<\n\tComponentTypes extends Record<string, any> = {},\n\tEventTypes extends Record<string, any> = {},\n\tResourceTypes extends Record<string, any> = {},\n\tAssetTypes extends Record<string, unknown> = {},\n\tScreenStates extends Record<string, ScreenDefinition<any, any>> = {},\n> {\n\tprivate _systems: SystemBuilderWithBundle<ComponentTypes, EventTypes, ResourceTypes, any>[] = [];\n\tprivate _resources: Map<keyof ResourceTypes, ResourceTypes[keyof ResourceTypes]> = new Map();\n\tprivate _assets: Map<string, AssetDefinition<unknown>> = new Map();\n\tprivate _assetGroups: Map<string, Map<string, () => Promise<unknown>>> = new Map();\n\tprivate _screens: Map<string, ScreenDefinition<any, any>> = new Map();\n\tprivate _id: string;\n\n\tconstructor(id?: string) {\n\t\tthis._id = id || generateBundleId();\n\t}\n\n\t/**\n\t * Get the unique ID of this bundle\n\t */\n\tget id(): string {\n\t\treturn this._id;\n\t}\n\n\t/**\n\t * Set the ID of this bundle\n\t * @internal Used by combineBundles\n\t */\n\tset id(value: string) {\n\t\tthis._id = value;\n\t}\n\n\t/**\n\t * Add a system to this bundle, by label (creating a new builder) or by reusing an existing one\n\t */\n\taddSystem<Q extends Record<string, any>>(builder: SystemBuilderWithBundle<ComponentTypes, EventTypes, ResourceTypes, Q>): SystemBuilderWithBundle<ComponentTypes, EventTypes, ResourceTypes, Q>;\n\taddSystem(label: string): SystemBuilderWithBundle<ComponentTypes, EventTypes, ResourceTypes, {}>;\n\taddSystem(builderOrLabel: string | SystemBuilderWithBundle<ComponentTypes, EventTypes, ResourceTypes, any>) {\n\t\tif (typeof builderOrLabel === 'string') {\n\t\t\tconst system = createBundleSystemBuilder<ComponentTypes, EventTypes, ResourceTypes>(builderOrLabel, this);\n\t\t\tthis._systems.push(system);\n\t\t\treturn system;\n\t\t} else {\n\t\t\tthis._systems.push(builderOrLabel);\n\t\t\treturn builderOrLabel;\n\t\t}\n\t}\n\n\t/**\n\t * Add a resource to this bundle\n\t * @param label The resource key\n\t * @param resource The resource value, a factory function, or a factory with dependencies\n\t */\n\taddResource<K extends keyof ResourceTypes>(\n\t\tlabel: K,\n\t\tresource:\n\t\t\t| ResourceTypes[K]\n\t\t\t| ((ecs: ECSpresso<ComponentTypes, EventTypes, ResourceTypes>) => ResourceTypes[K] | Promise<ResourceTypes[K]>)\n\t\t\t| { dependsOn: readonly string[]; factory: (ecs: ECSpresso<ComponentTypes, EventTypes, ResourceTypes>) => ResourceTypes[K] | Promise<ResourceTypes[K]> }\n\t) {\n\t\t// We need this cast because TypeScript doesn't recognize that a value of type\n\t\t// ResourceTypes[K] | (() => ResourceTypes[K] | Promise<ResourceTypes[K]>) | { dependsOn, factory }\n\t\t// can be properly assigned to Map<keyof ResourceTypes, ResourceTypes[keyof ResourceTypes]>\n\t\tthis._resources.set(label, resource as unknown as ResourceTypes[K]);\n\t\treturn this;\n\t}\n\n\t/**\n\t * Add an asset to this bundle\n\t * @param key The asset key\n\t * @param loader Function that loads and returns the asset\n\t * @param options Optional asset configuration\n\t */\n\taddAsset<K extends string, T>(\n\t\tkey: K,\n\t\tloader: () => Promise<T>,\n\t\toptions?: { eager?: boolean; group?: string }\n\t): Bundle<ComponentTypes, EventTypes, ResourceTypes, AssetTypes & Record<K, T>, ScreenStates> {\n\t\tthis._assets.set(key, {\n\t\t\tloader,\n\t\t\teager: options?.eager ?? true,\n\t\t\tgroup: options?.group,\n\t\t});\n\t\treturn this as unknown as Bundle<ComponentTypes, EventTypes, ResourceTypes, AssetTypes & Record<K, T>, ScreenStates>;\n\t}\n\n\t/**\n\t * Add a group of assets to this bundle\n\t * @param groupName The group name\n\t * @param assets Object mapping asset keys to loader functions\n\t */\n\taddAssetGroup<G extends string, T extends Record<string, () => Promise<unknown>>>(\n\t\tgroupName: G,\n\t\tassets: T\n\t): Bundle<ComponentTypes, EventTypes, ResourceTypes, AssetTypes & { [K in keyof T]: Awaited<ReturnType<T[K]>> }, ScreenStates> {\n\t\tconst groupAssets = new Map<string, () => Promise<unknown>>();\n\t\tfor (const [key, loader] of Object.entries(assets)) {\n\t\t\tgroupAssets.set(key, loader as () => Promise<unknown>);\n\t\t\tthis._assets.set(key, {\n\t\t\t\tloader: loader as () => Promise<unknown>,\n\t\t\t\teager: false,\n\t\t\t\tgroup: groupName,\n\t\t\t});\n\t\t}\n\t\tthis._assetGroups.set(groupName, groupAssets);\n\t\treturn this as unknown as Bundle<ComponentTypes, EventTypes, ResourceTypes, AssetTypes & { [K in keyof T]: Awaited<ReturnType<T[K]>> }, ScreenStates>;\n\t}\n\n\t/**\n\t * Add a screen to this bundle\n\t * @param name The screen name\n\t * @param definition The screen definition\n\t */\n\taddScreen<K extends string, Config extends Record<string, unknown>, State extends Record<string, unknown>>(\n\t\tname: K,\n\t\tdefinition: ScreenDefinition<Config, State>\n\t): Bundle<ComponentTypes, EventTypes, ResourceTypes, AssetTypes, ScreenStates & Record<K, ScreenDefinition<Config, State>>> {\n\t\tthis._screens.set(name, definition);\n\t\treturn this as unknown as Bundle<ComponentTypes, EventTypes, ResourceTypes, AssetTypes, ScreenStates & Record<K, ScreenDefinition<Config, State>>>;\n\t}\n\n\t/**\n\t * Get all asset definitions in this bundle\n\t */\n\tgetAssets(): Map<string, AssetDefinition<unknown>> {\n\t\treturn new Map(this._assets);\n\t}\n\n\t/**\n\t * Get all screen definitions in this bundle\n\t */\n\tgetScreens(): Map<string, ScreenDefinition<any, any>> {\n\t\treturn new Map(this._screens);\n\t}\n\n\t/**\n\t * Internal method to set a resource\n\t * @internal Used by mergeBundles\n\t */\n\t_setResource(key: string, value: unknown): void {\n\t\tthis._resources.set(key as keyof ResourceTypes, value as ResourceTypes[keyof ResourceTypes]);\n\t}\n\n\t/**\n\t * Internal method to set an asset definition\n\t * @internal Used by mergeBundles\n\t */\n\t_setAsset(key: string, definition: AssetDefinition<unknown>): void {\n\t\tthis._assets.set(key, definition);\n\t}\n\n\t/**\n\t * Internal method to set a screen definition\n\t * @internal Used by mergeBundles\n\t */\n\t_setScreen(name: string, definition: ScreenDefinition<any, any>): void {\n\t\tthis._screens.set(name, definition);\n\t}\n\n\t/**\n\t * Get all systems defined in this bundle\n\t * Returns built System objects instead of SystemBuilders\n\t */\n\tgetSystems() {\n\t\treturn this._systems.map(system => system.build());\n\t}\n\n\t/**\n\t * Register all systems in this bundle with an ECSpresso instance\n\t * @internal Used by ECSpresso when adding a bundle\n\t */\n\tregisterSystemsWithEcspresso(ecspresso: ECSpresso<ComponentTypes, EventTypes, ResourceTypes>) {\n\t\tfor (const systemBuilder of this._systems) {\n\t\t\tsystemBuilder.build(ecspresso);\n\t\t}\n\t}\n\n\t/**\n\t * Get all resources defined in this bundle\n\t */\n\tgetResources(): Map<keyof ResourceTypes, ResourceTypes[keyof ResourceTypes]> {\n\t\treturn new Map(this._resources);\n\t}\n\n\t/**\n\t * Get a specific resource by key\n\t * @param key The resource key\n\t * @returns The resource value or undefined if not found\n\t */\n\tgetResource<K extends keyof ResourceTypes>(key: K): ResourceTypes[K] {\n\t\treturn this._resources.get(key) as ResourceTypes[K];\n\t}\n\n\t/**\n\t * Get all system builders in this bundle\n\t */\n\tgetSystemBuilders(): SystemBuilderWithBundle<ComponentTypes, EventTypes, ResourceTypes, any>[] {\n\t\treturn [...this._systems];\n\t}\n\n\t/**\n\t * Check if this bundle has a specific resource\n\t * @param key The resource key to check\n\t * @returns True if the resource exists\n\t */\n\thasResource<K extends keyof ResourceTypes>(key: K): boolean {\n\t\treturn this._resources.has(key);\n\t}\n}\n\n/**\n * Function that merges multiple bundles into a single bundle\n */\nexport function mergeBundles<\n\tC1 extends Record<string, any>,\n\tE1 extends Record<string, any>,\n\tR1 extends Record<string, any>,\n\tA1 extends Record<string, unknown>,\n\tS1 extends Record<string, ScreenDefinition<any, any>>,\n\tC2 extends Record<string, any>,\n\tE2 extends Record<string, any>,\n\tR2 extends Record<string, any>,\n\tA2 extends Record<string, unknown>,\n\tS2 extends Record<string, ScreenDefinition<any, any>>,\n>(\n\tid: string,\n\tbundle1: Bundle<C1, E1, R1, A1, S1>,\n\tbundle2: BundlesAreCompatible<C1, C2, E1, E2, R1, R2, A1, A2, S1, S2> extends true\n\t\t? Bundle<C2, E2, R2, A2, S2>\n\t\t: never\n): Bundle<C1 & C2, E1 & E2, R1 & R2, A1 & A2, S1 & S2>;\n\nexport function mergeBundles<\n\tComponentTypes extends Record<string, any>,\n\tEventTypes extends Record<string, any>,\n\tResourceTypes extends Record<string, any>,\n\tAssetTypes extends Record<string, unknown>,\n\tScreenStates extends Record<string, ScreenDefinition<any, any>>,\n>(\n\tid: string,\n\t...bundles: Array<Bundle<ComponentTypes, EventTypes, ResourceTypes, AssetTypes, ScreenStates>>\n): Bundle<ComponentTypes, EventTypes, ResourceTypes, AssetTypes, ScreenStates>;\n\nexport function mergeBundles(\n\tid: string,\n\t...bundles: Array<Bundle<any, any, any, any, any>>\n): Bundle<any, any, any, any, any> {\n\tif (bundles.length === 0) {\n\t\treturn new Bundle(id);\n\t}\n\n\tconst combined = new Bundle(id);\n\n\tfor (const bundle of bundles) {\n\t\tfor (const system of bundle.getSystemBuilders()) {\n\t\t\t// reuse the full builder so we carry over queries, hooks, and handlers\n\t\t\tcombined.addSystem(system);\n\t\t}\n\n\t\t// Add resources from this bundle\n\t\tfor (const [label, resource] of bundle.getResources().entries()) {\n\t\t\tcombined._setResource(label as string, resource);\n\t\t}\n\n\t\t// Add assets from this bundle\n\t\tfor (const [key, definition] of bundle.getAssets().entries()) {\n\t\t\tcombined._setAsset(key, definition);\n\t\t}\n\n\t\t// Add screens from this bundle\n\t\tfor (const [name, definition] of bundle.getScreens().entries()) {\n\t\t\tcombined._setScreen(name, definition);\n\t\t}\n\t}\n\n\treturn combined;\n}\n",
|
|
7
|
-
"/**\n * Timer Bundle for ECSpresso\n *\n * Provides ECS-native timers following the \"data, not callbacks\" philosophy.\n * Timers are components processed each frame, automatically cleaned up when entities are removed.\n */\n\nimport Bundle from '../../bundle';\nimport type { SystemPhase } from '../../types';\n\n// ==================== Event Types ====================\n\n/**\n * Data structure published when a timer completes.\n * Use this type when defining timer completion events in your EventTypes interface.\n *\n * @example\n * ```typescript\n * interface Events {\n * hideMessage: TimerEventData;\n * spawnWave: TimerEventData;\n * }\n * ```\n */\nexport interface TimerEventData {\n\t/** The entity ID that the timer belongs to */\n\tentityId: number;\n\t/** The timer's configured duration in seconds */\n\tduration: number;\n\t/** The actual elapsed time (may exceed duration slightly) */\n\telapsed: number;\n}\n\n// ==================== Component Types ====================\n\n/**\n * Extracts event names from EventTypes that have TimerEventData as their payload.\n * This ensures only compatible events can be used with timer.onComplete.\n */\nexport type TimerEventName<EventTypes extends Record<string, any>> = {\n\t[K in keyof EventTypes]: EventTypes[K] extends TimerEventData ? K : never\n}[keyof EventTypes];\n\n/**\n * Timer component data structure.\n * Use `justFinished` to detect timer completion in your systems.\n *\n * @template EventTypes The event types from your ECS\n */\nexport interface Timer<EventTypes extends Record<string, any>> {\n\t/** Time accumulated so far (seconds) */\n\telapsed: number;\n\t/** Target duration (seconds) */\n\tduration: number;\n\t/** Whether timer repeats after completion */\n\trepeat: boolean;\n\t/** Whether timer is currently running */\n\tactive: boolean;\n\t/** True for one frame after timer completes */\n\tjustFinished: boolean;\n\t/** Optional event name to publish when timer completes. Must be an event with TimerEventData payload. */\n\tonComplete?: TimerEventName<EventTypes>;\n}\n\n/**\n * Component types provided by the timer bundle.\n * Extend your component types with this interface.\n *\n * @template EventTypes The event types from your ECS\n *\n * @example\n * ```typescript\n * interface GameComponents extends TimerComponentTypes<GameEvents> {\n * velocity: { x: number; y: number };\n * player: true;\n * }\n * ```\n */\nexport interface TimerComponentTypes<EventTypes extends Record<string, any>> {\n\ttimer: Timer<EventTypes>;\n}\n\n// ==================== Bundle Options ====================\n\n/**\n * Configuration options for the timer bundle.\n */\nexport interface TimerBundleOptions {\n\t/** System group name (default: 'timers') */\n\tsystemGroup?: string;\n\t/** Priority for timer update system (default: 0) */\n\tpriority?: number;\n\t/** Execution phase (default: 'preUpdate') */\n\tphase?: SystemPhase;\n}\n\n// ==================== Helper Functions ====================\n\n/**\n * Options for timer creation\n *\n * @template EventTypes The event types from your ECS\n */\nexport interface TimerOptions<EventTypes extends Record<string, any>> {\n\t/** Event name to publish when timer completes. Must be an event with TimerEventData payload. */\n\tonComplete?: TimerEventName<EventTypes>;\n}\n\n/**\n * Create a one-shot timer that fires once after the specified duration.\n *\n * @template EventTypes The event types from your ECS (must be explicitly provided)\n * @param duration Duration in seconds until the timer completes\n * @param options Optional configuration including event name\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * // Timer without event\n * ecs.spawn({\n * ...createTimer<GameEvents>(2),\n * explosion: true,\n * });\n *\n * // Timer that publishes an event on completion\n * ecs.spawn({\n * ...createTimer<GameEvents>(1.5, { onComplete: 'hideMessage' }),\n * });\n * ```\n */\nexport function createTimer<EventTypes extends Record<string, any>>(\n\tduration: number,\n\toptions?: TimerOptions<EventTypes>\n): Pick<TimerComponentTypes<EventTypes>, 'timer'> {\n\treturn {\n\t\ttimer: {\n\t\t\telapsed: 0,\n\t\t\tduration,\n\t\t\trepeat: false,\n\t\t\tactive: true,\n\t\t\tjustFinished: false,\n\t\t\tonComplete: options?.onComplete,\n\t\t},\n\t};\n}\n\n/**\n * Create a repeating timer that fires every `duration` seconds.\n *\n * @template EventTypes The event types from your ECS (must be explicitly provided)\n * @param duration Duration in seconds between each timer completion\n * @param options Optional configuration including event name\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * // Timer without event\n * ecs.spawn({\n * ...createRepeatingTimer<GameEvents>(5),\n * spawner: true,\n * });\n *\n * // Repeating timer that publishes an event each cycle\n * ecs.spawn({\n * ...createRepeatingTimer<GameEvents>(3, { onComplete: 'spawnWave' }),\n * });\n * ```\n */\nexport function createRepeatingTimer<EventTypes extends Record<string, any>>(\n\tduration: number,\n\toptions?: TimerOptions<EventTypes>\n): Pick<TimerComponentTypes<EventTypes>, 'timer'> {\n\treturn {\n\t\ttimer: {\n\t\t\telapsed: 0,\n\t\t\tduration,\n\t\t\trepeat: true,\n\t\t\tactive: true,\n\t\t\tjustFinished: false,\n\t\t\tonComplete: options?.onComplete,\n\t\t},\n\t};\n}\n\n// ==================== Bundle Factory ====================\n\n/**\n * Create a timer bundle for ECSpresso.\n *\n * This bundle provides:\n * - Timer update system that processes all timer components each frame\n * - `justFinished` flag pattern for one-frame completion detection\n * - Automatic cleanup when entities are removed\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso\n * .create<Components, Events, Resources>()\n * .withBundle(createTimerBundle())\n * .build();\n *\n * // Spawn entity with timer\n * ecs.spawn({\n * ...createRepeatingTimer(5),\n * spawner: true,\n * });\n *\n * // React to timer completion in a system\n * ecs.addSystem('spawn-on-timer')\n * .addQuery('spawners', { with: ['timer', 'spawner'] })\n * .setProcess((queries, _dt, ecs) => {\n * for (const { components } of queries.spawners) {\n * if (components.timer.justFinished) {\n * ecs.spawn({ enemy: true });\n * }\n * }\n * });\n * ```\n */\nexport function createTimerBundle<EventTypes extends Record<string, any>>(\n\toptions?: TimerBundleOptions\n): Bundle<TimerComponentTypes<EventTypes>, EventTypes, {}> {\n\tconst {\n\t\tsystemGroup = 'timers',\n\t\tpriority = 0,\n\t\tphase = 'preUpdate',\n\t} = options ?? {};\n\n\tconst bundle = new Bundle<TimerComponentTypes<EventTypes>, EventTypes, {}>('timers');\n\n\tbundle\n\t\t.addSystem('timer-update')\n\t\t.setPriority(priority)\n\t\t.inPhase(phase)\n\t\t.inGroup(systemGroup)\n\t\t.addQuery('timers', {\n\t\t\twith: ['timer'],\n\t\t})\n\t\t.setProcess((queries, deltaTime, ecs) => {\n\t\t\tfor (const entity of queries.timers) {\n\t\t\t\tconst { timer } = entity.components;\n\n\t\t\t\t// Reset justFinished flag from previous frame\n\t\t\t\ttimer.justFinished = false;\n\n\t\t\t\t// Skip inactive timers\n\t\t\t\tif (!timer.active) continue;\n\n\t\t\t\t// Accumulate time\n\t\t\t\ttimer.elapsed += deltaTime;\n\n\t\t\t\t// Check if timer completed\n\t\t\t\tif (timer.elapsed < timer.duration) continue;\n\n\t\t\t\t// Timer completed - handle based on repeat mode\n\t\t\t\tif (timer.repeat) {\n\t\t\t\t\t// Handle multiple cycles in one frame\n\t\t\t\t\twhile (timer.elapsed >= timer.duration) {\n\t\t\t\t\t\ttimer.justFinished = true;\n\t\t\t\t\t\tpublishTimerEvent(ecs, entity.id, timer);\n\t\t\t\t\t\ttimer.elapsed -= timer.duration;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// One-shot timer\n\t\t\t\t\ttimer.justFinished = true;\n\t\t\t\t\tpublishTimerEvent(ecs, entity.id, timer);\n\t\t\t\t\ttimer.active = false;\n\t\t\t\t\t// Auto-remove one-shot timer entities after completion.\n\t\t\t\t\t// If configurability is needed in the future, add an autoRemove option to TimerOptions.\n\t\t\t\t\tecs.commands.removeEntity(entity.id);\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t\t.and();\n\n\t/**\n\t * Publishes timer completion event if onComplete is specified.\n\t * Type assertion needed: TypeScript can't infer that TimerEventName<EventTypes>\n\t * maps to events with TimerEventData payloads, even though that's what the type enforces.\n\t */\n\tfunction publishTimerEvent(\n\t\tecs: { eventBus: { publish: (event: any, data: any) => void } },\n\t\tentityId: number,\n\t\ttimer: Timer<EventTypes>\n\t): void {\n\t\tif (!timer.onComplete) return;\n\t\tconst eventData: TimerEventData = {\n\t\t\tentityId,\n\t\t\tduration: timer.duration,\n\t\t\telapsed: timer.elapsed,\n\t\t};\n\t\tecs.eventBus.publish(timer.onComplete, eventData);\n\t}\n\n\treturn bundle;\n}\n"
|
|
5
|
+
"/**\n * Timer Bundle for ECSpresso\n *\n * Provides ECS-native timers following the \"data, not callbacks\" philosophy.\n * Timers are components processed each frame, automatically cleaned up when entities are removed.\n */\n\nimport { Bundle } from 'ecspresso';\nimport type { SystemPhase } from 'ecspresso';\n\n// ==================== Event Types ====================\n\n/**\n * Data structure published when a timer completes.\n * Use this type when defining timer completion events in your EventTypes interface.\n *\n * @example\n * ```typescript\n * interface Events {\n * hideMessage: TimerEventData;\n * spawnWave: TimerEventData;\n * }\n * ```\n */\nexport interface TimerEventData {\n\t/** The entity ID that the timer belongs to */\n\tentityId: number;\n\t/** The timer's configured duration in seconds */\n\tduration: number;\n\t/** The actual elapsed time (may exceed duration slightly) */\n\telapsed: number;\n}\n\n// ==================== Component Types ====================\n\n/**\n * Extracts event names from EventTypes that have TimerEventData as their payload.\n * This ensures only compatible events can be used with timer.onComplete.\n */\nexport type TimerEventName<EventTypes extends Record<string, any>> = {\n\t[K in keyof EventTypes]: EventTypes[K] extends TimerEventData ? K : never\n}[keyof EventTypes];\n\n/**\n * Timer component data structure.\n * Use `justFinished` to detect timer completion in your systems.\n *\n * @template EventTypes The event types from your ECS\n */\nexport interface Timer<EventTypes extends Record<string, any>> {\n\t/** Time accumulated so far (seconds) */\n\telapsed: number;\n\t/** Target duration (seconds) */\n\tduration: number;\n\t/** Whether timer repeats after completion */\n\trepeat: boolean;\n\t/** Whether timer is currently running */\n\tactive: boolean;\n\t/** True for one frame after timer completes */\n\tjustFinished: boolean;\n\t/** Optional event name to publish when timer completes. Must be an event with TimerEventData payload. */\n\tonComplete?: TimerEventName<EventTypes>;\n}\n\n/**\n * Component types provided by the timer bundle.\n * Extend your component types with this interface.\n *\n * @template EventTypes The event types from your ECS\n *\n * @example\n * ```typescript\n * interface GameComponents extends TimerComponentTypes<GameEvents> {\n * velocity: { x: number; y: number };\n * player: true;\n * }\n * ```\n */\nexport interface TimerComponentTypes<EventTypes extends Record<string, any>> {\n\ttimer: Timer<EventTypes>;\n}\n\n// ==================== Bundle Options ====================\n\n/**\n * Configuration options for the timer bundle.\n */\nexport interface TimerBundleOptions {\n\t/** System group name (default: 'timers') */\n\tsystemGroup?: string;\n\t/** Priority for timer update system (default: 0) */\n\tpriority?: number;\n\t/** Execution phase (default: 'preUpdate') */\n\tphase?: SystemPhase;\n}\n\n// ==================== Helper Functions ====================\n\n/**\n * Options for timer creation\n *\n * @template EventTypes The event types from your ECS\n */\nexport interface TimerOptions<EventTypes extends Record<string, any>> {\n\t/** Event name to publish when timer completes. Must be an event with TimerEventData payload. */\n\tonComplete?: TimerEventName<EventTypes>;\n}\n\n/**\n * Create a one-shot timer that fires once after the specified duration.\n *\n * @template EventTypes The event types from your ECS (must be explicitly provided)\n * @param duration Duration in seconds until the timer completes\n * @param options Optional configuration including event name\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * // Timer without event\n * ecs.spawn({\n * ...createTimer<GameEvents>(2),\n * explosion: true,\n * });\n *\n * // Timer that publishes an event on completion\n * ecs.spawn({\n * ...createTimer<GameEvents>(1.5, { onComplete: 'hideMessage' }),\n * });\n * ```\n */\nexport function createTimer<EventTypes extends Record<string, any>>(\n\tduration: number,\n\toptions?: TimerOptions<EventTypes>\n): Pick<TimerComponentTypes<EventTypes>, 'timer'> {\n\treturn {\n\t\ttimer: {\n\t\t\telapsed: 0,\n\t\t\tduration,\n\t\t\trepeat: false,\n\t\t\tactive: true,\n\t\t\tjustFinished: false,\n\t\t\tonComplete: options?.onComplete,\n\t\t},\n\t};\n}\n\n/**\n * Create a repeating timer that fires every `duration` seconds.\n *\n * @template EventTypes The event types from your ECS (must be explicitly provided)\n * @param duration Duration in seconds between each timer completion\n * @param options Optional configuration including event name\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * // Timer without event\n * ecs.spawn({\n * ...createRepeatingTimer<GameEvents>(5),\n * spawner: true,\n * });\n *\n * // Repeating timer that publishes an event each cycle\n * ecs.spawn({\n * ...createRepeatingTimer<GameEvents>(3, { onComplete: 'spawnWave' }),\n * });\n * ```\n */\nexport function createRepeatingTimer<EventTypes extends Record<string, any>>(\n\tduration: number,\n\toptions?: TimerOptions<EventTypes>\n): Pick<TimerComponentTypes<EventTypes>, 'timer'> {\n\treturn {\n\t\ttimer: {\n\t\t\telapsed: 0,\n\t\t\tduration,\n\t\t\trepeat: true,\n\t\t\tactive: true,\n\t\t\tjustFinished: false,\n\t\t\tonComplete: options?.onComplete,\n\t\t},\n\t};\n}\n\n// ==================== Bundle Factory ====================\n\n/**\n * Create a timer bundle for ECSpresso.\n *\n * This bundle provides:\n * - Timer update system that processes all timer components each frame\n * - `justFinished` flag pattern for one-frame completion detection\n * - Automatic cleanup when entities are removed\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso\n * .create<Components, Events, Resources>()\n * .withBundle(createTimerBundle())\n * .build();\n *\n * // Spawn entity with timer\n * ecs.spawn({\n * ...createRepeatingTimer(5),\n * spawner: true,\n * });\n *\n * // React to timer completion in a system\n * ecs.addSystem('spawn-on-timer')\n * .addQuery('spawners', { with: ['timer', 'spawner'] })\n * .setProcess((queries, _dt, ecs) => {\n * for (const { components } of queries.spawners) {\n * if (components.timer.justFinished) {\n * ecs.spawn({ enemy: true });\n * }\n * }\n * });\n * ```\n */\nexport function createTimerBundle<EventTypes extends Record<string, any>>(\n\toptions?: TimerBundleOptions\n): Bundle<TimerComponentTypes<EventTypes>, EventTypes, {}> {\n\tconst {\n\t\tsystemGroup = 'timers',\n\t\tpriority = 0,\n\t\tphase = 'preUpdate',\n\t} = options ?? {};\n\n\tconst bundle = new Bundle<TimerComponentTypes<EventTypes>, EventTypes, {}>('timers');\n\n\tbundle\n\t\t.addSystem('timer-update')\n\t\t.setPriority(priority)\n\t\t.inPhase(phase)\n\t\t.inGroup(systemGroup)\n\t\t.addQuery('timers', {\n\t\t\twith: ['timer'],\n\t\t})\n\t\t.setProcess((queries, deltaTime, ecs) => {\n\t\t\tfor (const entity of queries.timers) {\n\t\t\t\tconst { timer } = entity.components;\n\n\t\t\t\t// Reset justFinished flag from previous frame\n\t\t\t\ttimer.justFinished = false;\n\n\t\t\t\t// Skip inactive timers\n\t\t\t\tif (!timer.active) continue;\n\n\t\t\t\t// Accumulate time\n\t\t\t\ttimer.elapsed += deltaTime;\n\n\t\t\t\t// Check if timer completed\n\t\t\t\tif (timer.elapsed < timer.duration) continue;\n\n\t\t\t\t// Timer completed - handle based on repeat mode\n\t\t\t\tif (timer.repeat) {\n\t\t\t\t\t// Handle multiple cycles in one frame\n\t\t\t\t\twhile (timer.elapsed >= timer.duration) {\n\t\t\t\t\t\ttimer.justFinished = true;\n\t\t\t\t\t\tpublishTimerEvent(ecs, entity.id, timer);\n\t\t\t\t\t\ttimer.elapsed -= timer.duration;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// One-shot timer\n\t\t\t\t\ttimer.justFinished = true;\n\t\t\t\t\tpublishTimerEvent(ecs, entity.id, timer);\n\t\t\t\t\ttimer.active = false;\n\t\t\t\t\t// Auto-remove one-shot timer entities after completion.\n\t\t\t\t\t// If configurability is needed in the future, add an autoRemove option to TimerOptions.\n\t\t\t\t\tecs.commands.removeEntity(entity.id);\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t\t.and();\n\n\t/**\n\t * Publishes timer completion event if onComplete is specified.\n\t * Type assertion needed: TypeScript can't infer that TimerEventName<EventTypes>\n\t * maps to events with TimerEventData payloads, even though that's what the type enforces.\n\t */\n\tfunction publishTimerEvent(\n\t\tecs: { eventBus: { publish: (event: any, data: any) => void } },\n\t\tentityId: number,\n\t\ttimer: Timer<EventTypes>\n\t): void {\n\t\tif (!timer.onComplete) return;\n\t\tconst eventData: TimerEventData = {\n\t\t\tentityId,\n\t\t\tduration: timer.duration,\n\t\t\telapsed: timer.elapsed,\n\t\t};\n\t\tecs.eventBus.publish(timer.onComplete, eventData);\n\t}\n\n\treturn bundle;\n}\n"
|
|
8
6
|
],
|
|
9
|
-
"mappings": "
|
|
10
|
-
"debugId": "
|
|
7
|
+
"mappings": "2PAOA,iBAAS,kBA2HF,SAAS,CAAmD,CAClE,EACA,EACiD,CACjD,MAAO,CACN,MAAO,CACN,QAAS,EACT,WACA,OAAQ,GACR,OAAQ,GACR,aAAc,GACd,WAAY,GAAS,UACtB,CACD,EAyBM,SAAS,CAA4D,CAC3E,EACA,EACiD,CACjD,MAAO,CACN,MAAO,CACN,QAAS,EACT,WACA,OAAQ,GACR,OAAQ,GACR,aAAc,GACd,WAAY,GAAS,UACtB,CACD,EAsCM,SAAS,CAAyD,CACxE,EAC0D,CAC1D,IACC,cAAc,SACd,WAAW,EACX,QAAQ,aACL,GAAW,CAAC,EAEV,EAAS,IAAI,EAAwD,QAAQ,EAEnF,EACE,UAAU,cAAc,EACxB,YAAY,CAAQ,EACpB,QAAQ,CAAK,EACb,QAAQ,CAAW,EACnB,SAAS,SAAU,CACnB,KAAM,CAAC,OAAO,CACf,CAAC,EACA,WAAW,CAAC,EAAS,EAAW,IAAQ,CACxC,QAAW,KAAU,EAAQ,OAAQ,CACpC,IAAQ,SAAU,EAAO,WAMzB,GAHA,EAAM,aAAe,GAGjB,CAAC,EAAM,OAAQ,SAMnB,GAHA,EAAM,SAAW,EAGb,EAAM,QAAU,EAAM,SAAU,SAGpC,GAAI,EAAM,OAET,MAAO,EAAM,SAAW,EAAM,SAC7B,EAAM,aAAe,GACrB,EAAkB,EAAK,EAAO,GAAI,CAAK,EACvC,EAAM,SAAW,EAAM,SAIxB,OAAM,aAAe,GACrB,EAAkB,EAAK,EAAO,GAAI,CAAK,EACvC,EAAM,OAAS,GAGf,EAAI,SAAS,aAAa,EAAO,EAAE,GAGrC,EACA,IAAI,EAON,SAAS,CAAiB,CACzB,EACA,EACA,EACO,CACP,GAAI,CAAC,EAAM,WAAY,OACvB,IAAM,EAA4B,CACjC,WACA,SAAU,EAAM,SAChB,QAAS,EAAM,OAChB,EACA,EAAI,SAAS,QAAQ,EAAM,WAAY,CAAS,EAGjD,OAAO",
|
|
8
|
+
"debugId": "3CE4B9B931E088F464756E2164756E21",
|
|
11
9
|
"names": []
|
|
12
10
|
}
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @see https://docs.rs/bevy/latest/bevy/transform/components/struct.GlobalTransform.html
|
|
8
8
|
*/
|
|
9
|
-
import Bundle from '
|
|
10
|
-
import type { SystemPhase } from '
|
|
9
|
+
import { Bundle } from 'ecspresso';
|
|
10
|
+
import type { SystemPhase } from 'ecspresso';
|
|
11
11
|
/**
|
|
12
12
|
* Local transform relative to parent (or world if no parent).
|
|
13
13
|
* This is the transform you modify directly.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
var S=((j)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(j,{get:(k,q)=>(typeof require<"u"?require:k)[q]}):j)(function(j){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+j+'" is not supported')});import{Bundle as O}from"ecspresso";var Z={x:0,y:0,rotation:0,scaleX:1,scaleY:1},_={x:0,y:0,rotation:0,scaleX:1,scaleY:1};function $(j,k){return{localTransform:{x:j,y:k,rotation:0,scaleX:1,scaleY:1}}}function A(j,k){return{worldTransform:{x:j,y:k,rotation:0,scaleX:1,scaleY:1}}}function B(j,k,q){let H=q?.scale??q?.scaleX??1,v=q?.scale??q?.scaleY??1,D=q?.rotation??0,z={x:j,y:k,rotation:D,scaleX:H,scaleY:v};return{localTransform:{...z},worldTransform:{...z}}}function E(j){let{systemGroup:k="transform",priority:q=500,phase:H="postUpdate"}=j??{},v=new O("transform");return v.addSystem("transform-propagation").setPriority(q).inPhase(H).inGroup(k).setProcess((D,z,F)=>{P(F)}).and(),v}function P(j){let{changeThreshold:k,entityManager:q}=j;j.forEachInHierarchy((v,D)=>{let z=q.getComponent(v,"localTransform"),F=q.getComponent(v,"worldTransform");if(!z||!F)return;let J=q.getChangeSeq(v,"localTransform")>k,N=D!==null&&q.getChangeSeq(D,"worldTransform")>k;if(!J&&!N)return;if(D===null)K(z,F);else{let M=q.getComponent(D,"worldTransform");if(M)Q(M,z,F);else K(z,F)}j.markChanged(v,"worldTransform")});let H=j.getEntitiesWithQuery(["localTransform","worldTransform"]);for(let v of H)if(j.getParent(v.id)===null&&j.getChildren(v.id).length===0){if(!(q.getChangeSeq(v.id,"localTransform")>k))continue;let{localTransform:F,worldTransform:J}=v.components;K(F,J),j.markChanged(v.id,"worldTransform")}}function K(j,k){k.x=j.x,k.y=j.y,k.rotation=j.rotation,k.scaleX=j.scaleX,k.scaleY=j.scaleY}function Q(j,k,q){let H=k.x*j.scaleX,v=k.y*j.scaleY,D=Math.cos(j.rotation),z=Math.sin(j.rotation),F=H*D-v*z,J=H*z+v*D;q.x=j.x+F,q.y=j.y+J,q.rotation=j.rotation+k.rotation,q.scaleX=j.scaleX*k.scaleX,q.scaleY=j.scaleY*k.scaleY}export{A as createWorldTransform,E as createTransformBundle,B as createTransform,$ as createLocalTransform,_ as DEFAULT_WORLD_TRANSFORM,Z as DEFAULT_LOCAL_TRANSFORM};
|
|
2
|
+
|
|
3
|
+
//# debugId=2B678F92021E438E64756E2164756E21
|
|
4
|
+
//# sourceMappingURL=transform.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/bundles/utils/transform.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * Transform Bundle for ECSpresso\n *\n * Provides hierarchical transform propagation following Bevy's Transform/GlobalTransform pattern.\n * LocalTransform is modified by user code; WorldTransform is computed automatically.\n *\n * @see https://docs.rs/bevy/latest/bevy/transform/components/struct.GlobalTransform.html\n */\n\nimport { Bundle } from 'ecspresso';\nimport type { SystemPhase } from 'ecspresso';\nimport type ECSpresso from 'ecspresso';\n\n// ==================== Component Types ====================\n\n/**\n * Local transform relative to parent (or world if no parent).\n * This is the transform you modify directly.\n */\nexport interface LocalTransform {\n\tx: number;\n\ty: number;\n\trotation: number;\n\tscaleX: number;\n\tscaleY: number;\n}\n\n/**\n * Computed world transform (accumulated from parent chain).\n * Read-only - managed by the transform propagation system.\n */\nexport interface WorldTransform {\n\tx: number;\n\ty: number;\n\trotation: number;\n\tscaleX: number;\n\tscaleY: number;\n}\n\n/**\n * Component types provided by the transform bundle.\n * Extend your component types with this interface.\n *\n * @example\n * ```typescript\n * interface GameComponents extends TransformComponentTypes {\n * sprite: Sprite;\n * velocity: { x: number; y: number };\n * }\n * ```\n */\nexport interface TransformComponentTypes {\n\tlocalTransform: LocalTransform;\n\tworldTransform: WorldTransform;\n}\n\n// ==================== Bundle Options ====================\n\n/**\n * Configuration options for the transform bundle.\n */\nexport interface TransformBundleOptions {\n\t/** System group name (default: 'transform') */\n\tsystemGroup?: string;\n\t/** Priority for transform propagation (default: 500, runs after physics/movement) */\n\tpriority?: number;\n\t/** Execution phase (default: 'postUpdate') */\n\tphase?: SystemPhase;\n}\n\n// ==================== Default Values ====================\n\n/**\n * Default local transform values.\n */\nexport const DEFAULT_LOCAL_TRANSFORM: Readonly<LocalTransform> = {\n\tx: 0,\n\ty: 0,\n\trotation: 0,\n\tscaleX: 1,\n\tscaleY: 1,\n};\n\n/**\n * Default world transform values.\n */\nexport const DEFAULT_WORLD_TRANSFORM: Readonly<WorldTransform> = {\n\tx: 0,\n\ty: 0,\n\trotation: 0,\n\tscaleX: 1,\n\tscaleY: 1,\n};\n\n// ==================== Helper Functions ====================\n\n/**\n * Create a local transform component with position only.\n * Uses default rotation (0) and scale (1, 1).\n *\n * @param x The x coordinate\n * @param y The y coordinate\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createLocalTransform(100, 200),\n * sprite,\n * });\n * ```\n */\nexport function createLocalTransform(x: number, y: number): Pick<TransformComponentTypes, 'localTransform'> {\n\treturn {\n\t\tlocalTransform: {\n\t\t\tx,\n\t\t\ty,\n\t\t\trotation: 0,\n\t\t\tscaleX: 1,\n\t\t\tscaleY: 1,\n\t\t},\n\t};\n}\n\n/**\n * Create a world transform component with position only.\n * Typically used alongside createLocalTransform for initial state.\n *\n * @param x The x coordinate\n * @param y The y coordinate\n * @returns Component object suitable for spreading into spawn()\n */\nexport function createWorldTransform(x: number, y: number): Pick<TransformComponentTypes, 'worldTransform'> {\n\treturn {\n\t\tworldTransform: {\n\t\t\tx,\n\t\t\ty,\n\t\t\trotation: 0,\n\t\t\tscaleX: 1,\n\t\t\tscaleY: 1,\n\t\t},\n\t};\n}\n\n/**\n * Options for creating a full transform.\n */\nexport interface TransformOptions {\n\trotation?: number;\n\tscaleX?: number;\n\tscaleY?: number;\n\t/** Uniform scale (overrides scaleX/scaleY if provided) */\n\tscale?: number;\n}\n\n/**\n * Create both local and world transform components.\n * World transform is initialized to match local transform.\n *\n * @param x The x coordinate\n * @param y The y coordinate\n * @param options Optional rotation and scale\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createTransform(100, 200),\n * sprite,\n * });\n *\n * // With rotation and scale\n * ecs.spawn({\n * ...createTransform(100, 200, { rotation: Math.PI / 4, scale: 2 }),\n * sprite,\n * });\n * ```\n */\nexport function createTransform(\n\tx: number,\n\ty: number,\n\toptions?: TransformOptions\n): TransformComponentTypes {\n\tconst scaleX = options?.scale ?? options?.scaleX ?? 1;\n\tconst scaleY = options?.scale ?? options?.scaleY ?? 1;\n\tconst rotation = options?.rotation ?? 0;\n\n\tconst transform = {\n\t\tx,\n\t\ty,\n\t\trotation,\n\t\tscaleX,\n\t\tscaleY,\n\t};\n\n\treturn {\n\t\tlocalTransform: { ...transform },\n\t\tworldTransform: { ...transform },\n\t};\n}\n\n// ==================== Bundle Factory ====================\n\n/**\n * Create a transform bundle for ECSpresso.\n *\n * This bundle provides:\n * - Transform propagation system that computes world transforms from local transforms\n * - Parent-first traversal ensures parents are processed before children\n * - Supports full transform hierarchy (position, rotation, scale)\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso\n * .create<Components, Events, Resources>()\n * .withBundle(createTransformBundle())\n * .withBundle(createMovementBundle())\n * .build();\n *\n * // Spawn entity with transform\n * ecs.spawn({\n * ...createTransform(100, 200),\n * velocity: { x: 50, y: 0 },\n * });\n * ```\n */\nexport function createTransformBundle(\n\toptions?: TransformBundleOptions\n): Bundle<TransformComponentTypes, {}, {}> {\n\tconst {\n\t\tsystemGroup = 'transform',\n\t\tpriority = 500,\n\t\tphase = 'postUpdate',\n\t} = options ?? {};\n\n\tconst bundle = new Bundle<TransformComponentTypes, {}, {}>('transform');\n\n\tbundle\n\t\t.addSystem('transform-propagation')\n\t\t.setPriority(priority)\n\t\t.inPhase(phase)\n\t\t.inGroup(systemGroup)\n\t\t.setProcess((_queries, _deltaTime, ecs) => {\n\t\t\tpropagateTransforms(ecs as ECSpresso<TransformComponentTypes, {}, {}>);\n\t\t})\n\t\t.and();\n\n\treturn bundle;\n}\n\n/**\n * Propagate transforms through the hierarchy.\n * Parent-first traversal ensures parents are computed before children.\n *\n * Only recomputes entities whose localTransform changed since this system\n * last ran, or whose parent's worldTransform changed (cascade).\n * Uses per-system monotonic sequence threshold for change detection.\n */\nfunction propagateTransforms(ecs: ECSpresso<TransformComponentTypes, {}, {}>): void {\n\tconst threshold = ecs.changeThreshold;\n\tconst em = ecs.entityManager;\n\n\t// Use parent-first traversal for entities in hierarchy\n\tecs.forEachInHierarchy((entityId, parentId) => {\n\t\tconst localTransform = em.getComponent(entityId, 'localTransform');\n\t\tconst worldTransform = em.getComponent(entityId, 'worldTransform');\n\n\t\tif (!localTransform || !worldTransform) return;\n\n\t\tconst localChanged = em.getChangeSeq(entityId, 'localTransform') > threshold;\n\t\tconst parentWorldChanged = parentId !== null\n\t\t\t&& em.getChangeSeq(parentId, 'worldTransform') > threshold;\n\n\t\tif (!localChanged && !parentWorldChanged) return;\n\n\t\tif (parentId === null) {\n\t\t\t// Root entity: world transform equals local transform\n\t\t\tcopyTransform(localTransform, worldTransform);\n\t\t} else {\n\t\t\t// Child entity: combine with parent's world transform\n\t\t\tconst parentWorld = em.getComponent(parentId, 'worldTransform');\n\t\t\tif (parentWorld) {\n\t\t\t\tcombineTransforms(parentWorld, localTransform, worldTransform);\n\t\t\t} else {\n\t\t\t\t// Parent has no world transform, treat as root\n\t\t\t\tcopyTransform(localTransform, worldTransform);\n\t\t\t}\n\t\t}\n\n\t\tecs.markChanged(entityId, 'worldTransform');\n\t});\n\n\t// Process orphaned entities (not in hierarchy but have transforms)\n\tconst orphanedEntities = ecs.getEntitiesWithQuery(['localTransform', 'worldTransform']);\n\tfor (const entity of orphanedEntities) {\n\t\tconst parentId = ecs.getParent(entity.id);\n\t\t// Only process if truly orphaned (no parent and not a root with children)\n\t\tif (parentId === null && ecs.getChildren(entity.id).length === 0) {\n\t\t\tconst localChanged = em.getChangeSeq(entity.id, 'localTransform') > threshold;\n\t\t\tif (!localChanged) continue;\n\n\t\t\tconst { localTransform, worldTransform } = entity.components;\n\t\t\tcopyTransform(localTransform, worldTransform);\n\t\t\tecs.markChanged(entity.id, 'worldTransform');\n\t\t}\n\t}\n}\n\n/**\n * Copy transform values from source to destination.\n */\nfunction copyTransform(src: LocalTransform, dest: WorldTransform): void {\n\tdest.x = src.x;\n\tdest.y = src.y;\n\tdest.rotation = src.rotation;\n\tdest.scaleX = src.scaleX;\n\tdest.scaleY = src.scaleY;\n}\n\n/**\n * Combine parent world transform with child local transform into child world transform.\n */\nfunction combineTransforms(\n\tparent: WorldTransform,\n\tlocal: LocalTransform,\n\tworld: WorldTransform\n): void {\n\t// Apply parent's scale to local position\n\tconst scaledLocalX = local.x * parent.scaleX;\n\tconst scaledLocalY = local.y * parent.scaleY;\n\n\t// Rotate local position by parent's rotation\n\tconst cos = Math.cos(parent.rotation);\n\tconst sin = Math.sin(parent.rotation);\n\tconst rotatedX = scaledLocalX * cos - scaledLocalY * sin;\n\tconst rotatedY = scaledLocalX * sin + scaledLocalY * cos;\n\n\t// Add to parent's position\n\tworld.x = parent.x + rotatedX;\n\tworld.y = parent.y + rotatedY;\n\tworld.rotation = parent.rotation + local.rotation;\n\tworld.scaleX = parent.scaleX * local.scaleX;\n\tworld.scaleY = parent.scaleY * local.scaleY;\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "2B678F92021E438E64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
package/dist/ecspresso.d.ts
CHANGED
|
@@ -262,7 +262,24 @@ export default class ECSpresso<ComponentTypes extends Record<string, any> = {},
|
|
|
262
262
|
/**
|
|
263
263
|
* Get all entities with specific components
|
|
264
264
|
*/
|
|
265
|
-
getEntitiesWithQuery<WithComponents extends keyof ComponentTypes, WithoutComponents extends keyof ComponentTypes = never>(withComponents: ReadonlyArray<WithComponents>, withoutComponents?: ReadonlyArray<WithoutComponents>, changedComponents?: ReadonlyArray<keyof ComponentTypes>): Array<FilteredEntity<ComponentTypes, WithComponents, WithoutComponents>>;
|
|
265
|
+
getEntitiesWithQuery<WithComponents extends keyof ComponentTypes, WithoutComponents extends keyof ComponentTypes = never>(withComponents: ReadonlyArray<WithComponents>, withoutComponents?: ReadonlyArray<WithoutComponents>, changedComponents?: ReadonlyArray<keyof ComponentTypes>, parentHas?: ReadonlyArray<keyof ComponentTypes>): Array<FilteredEntity<ComponentTypes, WithComponents, WithoutComponents>>;
|
|
266
|
+
/**
|
|
267
|
+
* Get the single entity matching a query. Throws if zero or more than one match.
|
|
268
|
+
* @param withComponents Components the entity must have
|
|
269
|
+
* @param withoutComponents Components the entity must not have
|
|
270
|
+
* @returns The single matching entity
|
|
271
|
+
* @throws If zero or more than one entity matches
|
|
272
|
+
*/
|
|
273
|
+
getSingleton<WithComponents extends keyof ComponentTypes, WithoutComponents extends keyof ComponentTypes = never>(withComponents: ReadonlyArray<WithComponents>, withoutComponents?: ReadonlyArray<WithoutComponents>): FilteredEntity<ComponentTypes, WithComponents, WithoutComponents>;
|
|
274
|
+
/**
|
|
275
|
+
* Get the single entity matching a query, or undefined if none match.
|
|
276
|
+
* Throws if more than one entity matches.
|
|
277
|
+
* @param withComponents Components the entity must have
|
|
278
|
+
* @param withoutComponents Components the entity must not have
|
|
279
|
+
* @returns The single matching entity, or undefined if none match
|
|
280
|
+
* @throws If more than one entity matches
|
|
281
|
+
*/
|
|
282
|
+
tryGetSingleton<WithComponents extends keyof ComponentTypes, WithoutComponents extends keyof ComponentTypes = never>(withComponents: ReadonlyArray<WithComponents>, withoutComponents?: ReadonlyArray<WithoutComponents>): FilteredEntity<ComponentTypes, WithComponents, WithoutComponents> | undefined;
|
|
266
283
|
/**
|
|
267
284
|
* Remove an entity (and optionally its descendants)
|
|
268
285
|
* @param entityOrId Entity or entity ID to remove
|
|
@@ -435,7 +452,7 @@ export default class ECSpresso<ComponentTypes extends Record<string, any> = {},
|
|
|
435
452
|
* @param name Unique name for the query
|
|
436
453
|
* @param definition Query definition with with/without arrays and onEnter/onExit callbacks
|
|
437
454
|
*/
|
|
438
|
-
addReactiveQuery<WithComponents extends keyof ComponentTypes, WithoutComponents extends keyof ComponentTypes = never>(name: string, definition: ReactiveQueryDefinition<ComponentTypes, WithComponents, WithoutComponents>): void;
|
|
455
|
+
addReactiveQuery<WithComponents extends keyof ComponentTypes, WithoutComponents extends keyof ComponentTypes = never, OptionalComponents extends keyof ComponentTypes = never>(name: string, definition: ReactiveQueryDefinition<ComponentTypes, WithComponents, WithoutComponents, OptionalComponents>): void;
|
|
439
456
|
/**
|
|
440
457
|
* Remove a reactive query by name.
|
|
441
458
|
* @param name Name of the query to remove
|
package/dist/entity-manager.d.ts
CHANGED
|
@@ -37,7 +37,11 @@ export default class EntityManager<ComponentTypes> {
|
|
|
37
37
|
}>(entityOrId: number | Entity<ComponentTypes>, components: T & Record<Exclude<keyof T, keyof ComponentTypes>, never>): this;
|
|
38
38
|
removeComponent<ComponentName extends keyof ComponentTypes>(entityOrId: number | Entity<ComponentTypes>, componentName: ComponentName): this;
|
|
39
39
|
getComponent<ComponentName extends keyof ComponentTypes>(entityId: number, componentName: ComponentName): ComponentTypes[ComponentName] | null;
|
|
40
|
-
getEntitiesWithQuery<WithComponents extends keyof ComponentTypes = never, WithoutComponents extends keyof ComponentTypes = never>(required?: ReadonlyArray<WithComponents>, excluded?: ReadonlyArray<WithoutComponents>, changed?: ReadonlyArray<keyof ComponentTypes>, changeThreshold?: number): Array<FilteredEntity<ComponentTypes, WithComponents extends never ? never : WithComponents, WithoutComponents extends never ? never : WithoutComponents>>;
|
|
40
|
+
getEntitiesWithQuery<WithComponents extends keyof ComponentTypes = never, WithoutComponents extends keyof ComponentTypes = never>(required?: ReadonlyArray<WithComponents>, excluded?: ReadonlyArray<WithoutComponents>, changed?: ReadonlyArray<keyof ComponentTypes>, changeThreshold?: number, parentHas?: ReadonlyArray<keyof ComponentTypes>): Array<FilteredEntity<ComponentTypes, WithComponents extends never ? never : WithComponents, WithoutComponents extends never ? never : WithoutComponents>>;
|
|
41
|
+
/**
|
|
42
|
+
* Check if an entity's direct parent has all specified components
|
|
43
|
+
*/
|
|
44
|
+
private parentHasComponents;
|
|
41
45
|
removeEntity(entityOrId: number | Entity<ComponentTypes>, options?: RemoveEntityOptions): boolean;
|
|
42
46
|
/**
|
|
43
47
|
* Internal method to remove a single entity without cascade logic
|