@idosgames/game-kit 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ import {m,d,h,f,k,e,n,j,o,r}from'../chunk-NBIYNARI.js';import _ from'phaser';import {HERO_ANIMATIONS,heroRig,HERO_PRESETS,mergeLooks,LOOK_VERSION,seededRandom,seedOf,raceLook,createCatalog,heroAttack,GAIT_GROUND_SPEED}from'@idosgames/characters/rig2d';import {vectorArt,CharacterView}from'@idosgames/characters/rig2d/phaser';var Pt=new Set(["Hat","Top","Pants","Boots","Gloves","Weapon","Offhand","Back"]),Rt=null,Et=()=>Rt??=createCatalog();function ot(s,o,r){let p=o?.preset?HERO_PRESETS.find(g=>g.id===o.preset)?.look:void 0,u=mergeLooks(p??HERO_PRESETS[0]?.look,{v:LOOK_VERSION,parts:o?.parts??{},colors:o?.colors??{}}),d=o?.races??[],l=d.length>0?heroRig.races?.get(d[Math.abs(r)%d.length]):null;if(!l)return u;let m=seededRandom(seedOf(`${s}:${r}`)),k=raceLook(l,m,heroRig.skinColor??"Skin"),y=Object.fromEntries(Object.entries(u.parts??{}).filter(([g])=>Pt.has(g)));return mergeLooks(u,k,{v:LOOK_VERSION,parts:y})}var it={idle:HERO_ANIMATIONS.idle,run:HERO_ANIMATIONS.run,walk:HERO_ANIMATIONS.walk,die:HERO_ANIMATIONS.die,sit:HERO_ANIMATIONS.sit,dance:HERO_ANIMATIONS.dance,talk:HERO_ANIMATIONS.talk},Ot=.42,Tt=.57,$t=.55,Ct=2.3,It=heroRig.data.height||188,R=s=>s==="run"||s==="walk",K=class{constructor(o,r,p,u){this.scene=o;this.shadow=o.add.image(0,0,u),this.view=new CharacterView(o,{rig:heroRig,art:r,look:p});let d=this.view.parts;this.attack=heroAttack(d.isWorn("Weapon")?d.part("Weapon"):null),this.view.on("end",l=>{if(!(this.dead||l.animation===HERO_ANIMATIONS.die||!this.acting)){if(this.acting==="attack"&&this.attackRate>0){this.holdUntil=this.scene.time.now+1e3/this.attackRate*.75,this.acting=null;return}this.acting=null,this.playPose(.12,false);}});}scene;view;shadow;attack;poseKey=null;pose=null;actN;acting=null;actSpeed=1;dead=false;lastAttackAt=-1e9;attackRate=0;holdUntil=-1;playPose(o,r){let p=this.pose??"idle";if(p==="air"){this.view.play(HERO_ANIMATIONS.jump,{loop:false,from:this.poseKey==="air-up"?Ot:Tt,mix:r?0:.1}),this.view.animator.setSpeed(0);return}if(p==="die"){this.view.play(it.die,{loop:false,mix:.06});return}let u=it[p],d=r?Math.random()*Math.max(.01,this.view.rig.data.animations.get(u)?.duration??1):0;this.view.play(u,{mix:o,from:d});}playAct(o,r){if(o==="hit"){this.view.flash(16734810,140);let u=this.scene.time.now-this.lastAttackAt<1200;if(R(this.pose)||this.pose==="air"||this.acting==="attack"||u)return;this.acting=this.view.play(HERO_ANIMATIONS.hit,{loop:false,mix:.05})?"hit":null,this.actSpeed=1;return}if(o==="attack"){if(R(this.pose)||this.pose==="air")return;let u=this.view.animator;if(this.acting==="attack"&&u.time()<u.duration()*.6)return;let d=this.view.rig.data.animations.get(this.attack)?.duration??.8;this.attackRate=r.actRate??0,this.lastAttackAt=this.scene.time.now,this.holdUntil=-1,this.actSpeed=Math.max(1,Math.min(3,d*this.attackRate*1.05)),this.acting=this.view.play(this.attack,{loop:false,mix:this.attackRate>0?.04:.06,speed:this.actSpeed})?"attack":null;return}if(R(this.pose)||this.pose==="air")return;if(this.acting&&this.acting!=="attack"&&this.acting!=="hit"){let u=this.view.animator;if(u.time()<u.duration()*.7)return}let p=o==="wave"?HERO_ANIMATIONS.wave:o==="surprised"?HERO_ANIMATIONS.surprised:HERO_ANIMATIONS.jump;this.actSpeed=1,this.acting=this.view.play(p,{loop:false,mix:.08})?o:null;}gaitSpeed(o,r){if(o.speed===void 0)return 1;let p=this.view.animation()??HERO_ANIMATIONS.run,u=(GAIT_GROUND_SPEED[p]??GAIT_GROUND_SPEED[HERO_ANIMATIONS.run]??96)*r;return Math.max($t,Math.min(Ct,Math.abs(o.speed)/Math.max(.001,u)))}apply(o,r){let p=r.h/It*(o.scale??1),u=o.y+(r.feet??r.h/2);this.view.setScale(p).setPosition(o.x,u),this.view.setFacing(r.facing===-1?1:-1);let d=r.depth??0;this.view.setDepth(d);let l=r.alpha??1;this.view.setAlpha(l);let m=r.pose??"idle",k=r.h*.55;this.shadow.setPosition(o.x,u).setDisplaySize(k,k*.22).setDepth(d-.05).setAlpha(m==="air"?0:l*.9);let y=m==="air"?r.rising?"air-up":"air-down":m;if(y!==this.poseKey){let P=this.poseKey===null;this.poseKey=y,this.pose=m,this.dead=m==="die",(this.dead||R(m)||m==="air")&&(this.acting=null,this.holdUntil=-1),!this.acting&&this.holdUntil<0&&this.playPose(m==="air"?.1:.14,P);}this.actN===void 0?this.actN=r.actN??0:r.actN!==void 0&&r.actN!==this.actN&&(this.actN=r.actN,r.act&&!this.dead&&this.playAct(r.act,r)),this.holdUntil>=0&&!this.acting&&this.scene.time.now>=this.holdUntil&&(this.holdUntil=-1,this.playPose(.18,false));let g=this.view.animator;r.paused?g.setSpeed(0):this.acting?g.setSpeed(this.actSpeed):m==="air"?g.setSpeed(0):R(m)?g.setSpeed(this.gaitSpeed(r,p)):g.setSpeed(1);}inspect(){let o=this.view.animator;return {pose:this.poseKey,animation:o.current(),time:Math.round(o.time()*100)/100,speed:Math.round((this.view.animator.track?.speed??0)*100)/100,acting:this.acting}}destroy(){this.view.destroy(),this.shadow.destroy();}};function ct(s,o){let r=vectorArt(s.textures,Et(),2),p="kit-actor-shadow";if(!s.textures.exists(p)){let d=s.make.graphics({x:0,y:0},false);for(let l=0;l<6;l++)d.fillStyle(0,.06+l*.03),d.fillEllipse(64,16,128-l*18,32-l*4);d.generateTexture(p,128,32),d.destroy();}let u=new Map;return {make(d){let l=`${d.cast}#${d.variant??0}`,m=u.get(l);return m||(m=ot(d.cast,o[d.cast],d.variant??0),u.set(l,m)),new K(s,r,m,p)}}}var Dt=350,_t=24,Vt=40,W=s=>Number.parseInt(s.slice(1,7),16),lt=s=>{switch(s.shape){case "rect":return `r|${s.w}|${s.h}|${s.color}|${s.radius??0}`;case "circle":return `c|${s.r}|${s.color}`;case "text":return `t|${s.text}|${s.size}|${s.color}|${s.bold?1:0}`;case "sprite":return `s|${s.art}|${Math.round(s.w)}|${Math.round(s.h)}|${s.color??""}|${s.flipX?1:0}`;case "tile":return `l|${s.art}|${Math.round(s.w)}|${Math.round(s.h)}|${Math.round(s.unit??0)}|${s.color??""}`;case "actor":return `a|${s.cast}|${s.variant??0}|${Math.round(s.h)}`}},ht=s=>s<=64?Math.ceil(s/8)*8:s<=256?Math.ceil(s/16)*16:Math.ceil(s/32)*32,dt={ArrowLeft:"left",KeyA:"left",ArrowRight:"right",KeyD:"right",ArrowUp:"up",KeyW:"up",ArrowDown:"down",KeyS:"down",Space:"action",Enter:"action",Escape:"back"};function jt(s){let{parent:o$1,palette:r$1}=s,p=s.look??m,u=s.designSize??720,d$1=Math.min(typeof window>"u"?1:window.devicePixelRatio||1,s.maxDensity??2),l=s.world,m$1=d(l),k$1=new Map,y=1,g=u,P=u,b=false,X=()=>{let a=o$1.getBoundingClientRect();return {w:Math.max(1,a.width),h:Math.max(1,a.height)}},E=a=>Math.max(1,Math.ceil(a*d$1)),V=(a,t)=>{let e=y;if(y=Math.min(a,t)/u,b&&Math.abs(e-y)>.05)for(let n of k$1.values())n.actor||(n.sig="");if(g=a/y,P=t/y,l.vars.view_w=Math.round(g),l.vars.view_h=Math.round(P),b){let n=$.cameras.main;n.setZoom(y),n.centerOn(g/2,P/2);}l.emit(k,{w:Math.round(g),h:Math.round(P)});},j$1=a=>{a.obj?.destroy(),a.actor?.destroy();},U=()=>{for(let a of k$1.values())j$1(a);k$1.clear();},O=new Set,z=false,F=()=>Math.min(3,Math.max(.5,y*d$1)),H=(a,t,e,n,c,i=0)=>{let h=o[t]?.exact?`:${Math.round(n.w)}x${Math.round(n.h)}`:"",f=`kit:${p.style}:${t}:${e??""}:${c.w}x${c.h}${h}`;if(a.textures.exists(f))return f;if(O.has(f))return null;let A=r(t,{style:p.style,palette:r$1,...e?{color:e}:{},box:n,px:c,padBottom:i});if(!A)return null;O.add(f);let S=new Image;return S.onload=()=>{O.delete(f),b&&(a.textures.exists(f)||a.textures.addImage(f,S),z=true);},S.onerror=()=>O.delete(f),S.src=`data:image/svg+xml;charset=utf-8,${encodeURIComponent(A)}`,null},ut=(a,t)=>{let e={sig:lt(t),obj:null,actor:null,sx:1,sy:1,k:1};if(t.shape==="actor")return e.actor=Y?.make(t)??null,e;if(t.shape==="sprite"){if(!o[t.art])return e;let c=F(),i={w:ht(t.w*c),h:ht(t.h*c)},h=H(a,t.art,t.color,{w:t.w,h:t.h},i);if(!h)return e.sig="",e;let f=a.add.image(0,0,h);return f.setFlipX(!!t.flipX),e.obj=f,e.sx=t.w/i.w,e.sy=t.h/i.h,e}if(t.shape==="tile"){let c=o[t.art];if(!c)return e;let i=F(),h=c.exact?1:c.w/c.h,f=t.unit??t.h*h,A={w:Math.max(4,Math.round(f*i)),h:Math.max(4,Math.round(f/h*i))},S=H(a,t.art,t.color,{w:f,h:f/h},A,c.band?2:0);return S?(e.obj=a.add.tileSprite(0,0,Math.max(1,Math.round(t.w*i)),Math.max(1,Math.round(t.h*i)),S),e.sx=e.sy=1/i,e.k=i,e):(e.sig="",e)}let n=W(h(t.color,r$1));if(t.shape==="circle")e.obj=a.add.circle(0,0,t.r,n);else if(t.shape==="text")e.obj=a.add.text(0,0,t.text,{fontFamily:"Rubik Variable, Rubik, system-ui, sans-serif",fontSize:`${t.size}px`,fontStyle:t.bold?"bold":"normal",color:h(t.color,r$1),resolution:Math.max(1,y*d$1),stroke:"#00000099",strokeThickness:Math.max(2,Math.round(t.size*.12))}).setOrigin(.5);else {let c=a.add.graphics();c.fillStyle(n,1),t.radius&&t.radius>0?c.fillRoundedRect(-t.w/2,-t.h/2,t.w,t.h,Math.min(t.radius,t.w/2,t.h/2)):c.fillRect(-t.w/2,-t.h/2,t.w,t.h),e.obj=c;}return e},pt=(a,t,e)=>{if(a.actor){e.shape==="actor"&&a.actor.apply(t,e);return}let n=a.obj;if(!n)return;let c=t.scale??1;if(n.setPosition(t.x,t.y),n.setRotation(t.rotation??0),n.setScale(a.sx*c,a.sy*c),n.setDepth(e.depth??0),n.setAlpha(e.alpha??1),e.shape==="tile"){let i=n;i.tilePositionX=(e.scrollX??0)*a.k,i.tilePositionY=(e.scrollY??0)*a.k;}},T=a=>{z=false;let t=new Set;for(let e$1 of l.query(e,f)){t.add(e$1);let n=l.need(e$1,e),c=l.need(e$1,f),i=lt(c),h=k$1.get(e$1);(!h||h.sig!==i)&&(h&&j$1(h),h=ut(a,c),k$1.set(e$1,h)),pt(h,n,c);}for(let[e,n]of k$1)t.has(e)||(j$1(n),k$1.delete(e));},Y=null,G=null,mt="Rubik Variable, Rubik, system-ui, sans-serif",ft=(a,t)=>{if(t.kind==="shake"){a.cameras.main.shake(160,.004*(t.power??1));return}if(t.kind==="flash"){let i=W(h(t.color,r$1));a.cameras.main.flash(140,i>>16&255,i>>8&255,i&255);return}if(t.kind==="pop"){let i=t.size??34,h$1=a.add.text(t.x,t.y,t.text,{fontFamily:mt,fontSize:`${i}px`,fontStyle:"bold",color:h(t.color??"coin",r$1),stroke:"#000000",strokeThickness:Math.max(3,Math.round(i*.14)),resolution:Math.max(1,y*d$1)}).setOrigin(.5).setDepth(70);a.tweens.add({targets:h$1,y:t.y-i*1.8,alpha:0,duration:750,ease:"Cubic.easeOut",onComplete:()=>h$1.destroy()});return}let e=W(h(t.color,r$1));if(t.kind==="ring"){let i=t.size??60,h=a.add.graphics({x:t.x,y:t.y}).setDepth(65);h.lineStyle(Math.max(3,i*.08),e,1),h.strokeCircle(0,0,i/2),h.setScale(.3),a.tweens.add({targets:h,scale:1.4,alpha:0,duration:380,ease:"Cubic.easeOut",onComplete:()=>h.destroy()});return}let n=t.n??10,c=t.size??40;for(let i=0;i<n;i++){let h=i/n*Math.PI*2+Math.random()*.6,f=c*(.6+Math.random()*.8),A=a.add.circle(t.x,t.y,Math.max(2,c*(.06+Math.random()*.06)),e).setDepth(66);a.tweens.add({targets:A,x:t.x+Math.cos(h)*f,y:t.y+Math.sin(h)*f,alpha:0,scale:.3,duration:380+Math.random()*220,ease:"Cubic.easeOut",onComplete:()=>A.destroy()});}},B=()=>{G?.(),G=l.on(n,a=>{b&&ft($,a);});};class wt extends _.Scene{down=null;constructor(){super({key:"kit"});}create(){b=true,Y=ct(this,p.cast),B(),this.cameras.main.setBackgroundColor(h("background",r$1)),V(this.scale.width,this.scale.height);let t=e=>l.emit(j,e);this.input.on("pointerdown",e=>{this.down={x:e.worldX,y:e.worldY,at:e.downTime},t({type:"press",x:e.worldX,y:e.worldY});}),this.input.on("pointermove",e=>{this.down&&t({type:"move",x:e.worldX,y:e.worldY});}),this.input.on("pointerup",e=>{let n=this.down;if(this.down=null,t({type:"release",x:e.worldX,y:e.worldY}),!n)return;let c=e.worldX-n.x,i=e.worldY-n.y,h=Math.hypot(c,i);h<=_t&&e.upTime-n.at<=Dt?t({type:"tap",x:e.worldX,y:e.worldY}):h>=Vt&&t({type:"swipe",dir:Math.abs(c)>Math.abs(i)?c<0?"left":"right":i<0?"up":"down",x:n.x,y:n.y});}),this.input.keyboard?.on("keydown",e=>{if(e.repeat)return;let n=dt[e.code];n&&t({type:"key",key:n});}),this.input.keyboard?.on("keyup",e=>{let n=dt[e.code];n&&t({type:"key-up",key:n});}),l.flushEvents(),T(this);}update(t,e){m$1.advance(e/1e3)>0?(T(this),s.onStep?.(l)):z&&T(this);}}let $=new wt;o$1.style.overflow="hidden";let Z=X(),x=new _.Game({type:_.AUTO,parent:o$1,width:E(Z.w),height:E(Z.h),backgroundColor:h("background",r$1),scene:$,scale:{mode:_.Scale.NONE,zoom:1/d$1},render:{autoMobileTextures:false},banner:false});globalThis.PHASER_GAME=x;let yt=()=>{let a=o$1.getBoundingClientRect();if(a.width<1||a.height<1)return;let t=X(),e=E(t.w),n=E(t.h);Math.abs(e-x.scale.width)<1&&Math.abs(n-x.scale.height)<1||(x.scale.resize(e,n),V(e,n));},q=typeof ResizeObserver>"u"?null:new ResizeObserver(yt);return q?.observe(o$1),{setRunning(a){a?x.resume():x.pause();},setWorld(a){l=a,m$1=d(l),U(),b&&B(),V(x.scale.width,x.scale.height),l.flushEvents(),b&&T($);},inspectActors(){let a=[];for(let[t,e]of k$1){let n=l.get(t,f);e.actor&&n?.shape==="actor"&&a.push({entity:t,cast:n.cast,...e.actor.inspect()});}return a},capture(){return new Promise(a=>{try{x.renderer.snapshot(t=>{if(!(t instanceof HTMLImageElement))return a(null);let e=document.createElement("canvas");e.width=t.naturalWidth,e.height=t.naturalHeight;let n=e.getContext("2d");if(!n)return a(null);n.drawImage(t,0,0),e.toBlob(c=>a(c),"image/png");});}catch{a(null);}})},destroy(){q?.disconnect(),G?.(),U(),b=false,x.destroy(true);let a=globalThis;a.PHASER_GAME===x&&delete a.PHASER_GAME;}}}export{ot as castLook,jt as createPhaserView};
@@ -0,0 +1,309 @@
1
+ /** A seeded random source. All methods advance the same state. */
2
+ interface Rng {
3
+ /** A float in [0, 1). */
4
+ next(): number;
5
+ /** An integer in [min, max] (both inclusive). */
6
+ int(min: number, max: number): number;
7
+ /** A float in [min, max). */
8
+ range(min: number, max: number): number;
9
+ /** true with the given probability (0..1). */
10
+ chance(probability: number): boolean;
11
+ /** One element of a non-empty list. */
12
+ pick<T>(items: readonly T[]): T;
13
+ /** One element by weight; weights ≤ 0 never win. Throws when no weight is positive. */
14
+ weighted<T>(items: readonly T[], weight: (item: T) => number): T;
15
+ /** A shuffled copy (Fisher–Yates). */
16
+ shuffle<T>(items: readonly T[]): T[];
17
+ /**
18
+ * An independent stream derived from this generator's SEED (not its current state) and a name.
19
+ * The same name always gives the same stream, however many numbers were drawn here before.
20
+ */
21
+ stream(name: string): Rng;
22
+ /** The seed this generator was created from. */
23
+ readonly seed: string;
24
+ }
25
+ /** A generator from any string or number seed. */
26
+ declare function createRng(seed: string | number): Rng;
27
+
28
+ type Entity = number;
29
+ /** A typed component token: `const Position = defineComponent<{ x: number; y: number }>("position")`. */
30
+ interface ComponentType<T> {
31
+ readonly name: string;
32
+ /** Phantom: carries the data type, never set at runtime. */
33
+ readonly __data?: T;
34
+ }
35
+ declare function defineComponent<T>(name: string): ComponentType<T>;
36
+ /** A value a rule or formula can read from `vars`. */
37
+ type WorldVar = number | string | boolean;
38
+ /** Logic that runs every step. Lower `order` runs first; equal orders keep the order they were added in. */
39
+ interface System {
40
+ readonly id: string;
41
+ readonly order?: number;
42
+ run(world: World, dt: number): void;
43
+ }
44
+ type EventHandler = (payload: Readonly<Record<string, unknown>>, world: World) => void;
45
+ interface WorldOptions {
46
+ readonly seed?: string | number;
47
+ /** Starting values of `vars` (score, lives, level, …). */
48
+ readonly vars?: Readonly<Record<string, WorldVar>>;
49
+ /**
50
+ * How many rounds of events emitted BY event handlers one step delivers before giving up.
51
+ * A guard against two rules that trigger each other forever.
52
+ */
53
+ readonly maxEventRounds?: number;
54
+ }
55
+ declare class World {
56
+ /** The world's random source. Systems should draw from their own `rng.stream(name)`. */
57
+ readonly rng: Rng;
58
+ /** Shared named values: score, lives, wave, coins of the run. Rules and formulas read them as `vars.*`. */
59
+ readonly vars: Record<string, WorldVar>;
60
+ /** Steps run so far. */
61
+ tick: number;
62
+ /** Seconds simulated so far (the sum of dt). */
63
+ time: number;
64
+ private nextId;
65
+ private readonly alive;
66
+ private readonly stores;
67
+ private readonly doomed;
68
+ private systems;
69
+ private systemSeq;
70
+ private readonly handlers;
71
+ private queue;
72
+ private readonly maxEventRounds;
73
+ private stepping;
74
+ constructor(options?: WorldOptions);
75
+ create(): Entity;
76
+ /** Removes the entity at the end of the current step (now, when called outside a step). */
77
+ destroy(e: Entity): void;
78
+ isAlive(e: Entity): boolean;
79
+ get entityCount(): number;
80
+ /** Sets (adds or replaces) a component. Returns the entity for chaining. */
81
+ set<T>(e: Entity, type: ComponentType<T>, value: T): Entity;
82
+ get<T>(e: Entity, type: ComponentType<T>): T | undefined;
83
+ /** Like get, but throws when the component is missing. */
84
+ need<T>(e: Entity, type: ComponentType<T>): T;
85
+ has(e: Entity, type: ComponentType<unknown>): boolean;
86
+ remove(e: Entity, type: ComponentType<unknown>): void;
87
+ /** Living entities that have ALL the given components, in id order. A snapshot: safe to mutate while iterating. */
88
+ query(...types: ComponentType<unknown>[]): Entity[];
89
+ addSystem(system: System): void;
90
+ removeSystem(id: string): void;
91
+ get systemIds(): string[];
92
+ /** Listen to a topic. Returns an unsubscribe function. */
93
+ on(topic: string, handler: EventHandler): () => void;
94
+ /**
95
+ * Queue an event. Delivered after the systems of the current step ran (or by `flushEvents()`
96
+ * outside a step), in emission order. Payload values should be plain data: rules read them.
97
+ */
98
+ emit(topic: string, payload?: Readonly<Record<string, unknown>>): void;
99
+ /** Deliver queued events now. Events emitted by handlers are delivered in further rounds. */
100
+ flushEvents(): void;
101
+ /** How many times event delivery hit the round limit (a sign of rules triggering each other). */
102
+ overflowed: number;
103
+ /** One step of `dt` seconds: systems in order, then events, then removal of destroyed entities. */
104
+ step(dt: number): void;
105
+ private purge;
106
+ }
107
+ /**
108
+ * Fixed-step driver: a renderer calls `advance(elapsedSeconds)` every frame, the world steps in
109
+ * equal slices (same result at 30 or 144 FPS). Returns the number of steps taken.
110
+ */
111
+ declare function createFixedStepper(world: World, options?: {
112
+ step?: number;
113
+ maxSteps?: number;
114
+ }): {
115
+ step: number;
116
+ advance(elapsed: number): number;
117
+ /** 0..1: how far between the last step and the next (for render interpolation). */
118
+ readonly alpha: number;
119
+ };
120
+
121
+ interface TransformData {
122
+ x: number;
123
+ y: number;
124
+ /** Radians. */
125
+ rotation?: number;
126
+ scale?: number;
127
+ }
128
+ type VisualData = {
129
+ shape: "rect";
130
+ w: number;
131
+ h: number;
132
+ /** Palette role or #hex. */
133
+ color: string;
134
+ /** Corner radius, logical pixels. */
135
+ radius?: number;
136
+ alpha?: number;
137
+ depth?: number;
138
+ } | {
139
+ shape: "circle";
140
+ r: number;
141
+ color: string;
142
+ alpha?: number;
143
+ depth?: number;
144
+ }
145
+ /** A picture of the kit's art (src/art: drawn in the pack's style, coloured by the palette). */
146
+ | {
147
+ shape: "sprite";
148
+ art: string;
149
+ w: number;
150
+ h: number;
151
+ /** The art's main colour (a palette role or #hex), for art that has one (a gem, a crystal, a cell). */
152
+ color?: string;
153
+ /** Mirrored left ↔ right. */
154
+ flipX?: boolean;
155
+ alpha?: number;
156
+ depth?: number;
157
+ }
158
+ /** A repeating picture over a box (the ground, a layer of the backdrop): scroll moves the pattern. */
159
+ | {
160
+ shape: "tile";
161
+ art: string;
162
+ w: number;
163
+ h: number;
164
+ /** Logical width of one repeat (default: the art's own aspect at the box height). */
165
+ unit?: number;
166
+ /** Pattern offset, logical px (a parallax layer adds what the world scrolled). */
167
+ scrollX?: number;
168
+ scrollY?: number;
169
+ color?: string;
170
+ alpha?: number;
171
+ depth?: number;
172
+ }
173
+ /**
174
+ * A living character on the 2D hero rig (@idosgames/characters): who it is comes from the cast of the
175
+ * asset pack ("hero", "foe", "foe2", "boss", "helper"), the variant picks its race and face. The box is
176
+ * h tall and the Transform is its centre (the feet stand at y + h / 2).
177
+ */
178
+ | {
179
+ shape: "actor";
180
+ cast: string;
181
+ variant?: number;
182
+ h: number;
183
+ /** Where the feet stand below the Transform (default h / 2: the Transform is the box's centre). */
184
+ feet?: number;
185
+ /** What it does all the time (loops; die and air are held). */
186
+ pose?: ActorPose;
187
+ /** A one-shot over the pose, played each time `actN` changes. */
188
+ act?: ActorAct;
189
+ actN?: number;
190
+ /** 1 faces right (default), -1 left. */
191
+ facing?: 1 | -1;
192
+ /** Logical px a second it moves over its ground: a run or a walk is played at that pace. */
193
+ speed?: number;
194
+ /** In the air (pose "air"): going up; otherwise coming down. */
195
+ rising?: boolean;
196
+ /** Attacks a second it makes (a fast fire plays the swing faster, so it ends before the next). */
197
+ actRate?: number;
198
+ /** Frozen where it is (the field is paused: an upgrade to pick). */
199
+ paused?: boolean;
200
+ alpha?: number;
201
+ depth?: number;
202
+ } | {
203
+ shape: "text";
204
+ text: string;
205
+ size: number;
206
+ color: string;
207
+ /** Font weight. */
208
+ bold?: boolean;
209
+ alpha?: number;
210
+ depth?: number;
211
+ };
212
+ /** "air": in a jump (held, rising or falling by `rising`); "die" is held at its end. */
213
+ type ActorPose = "idle" | "run" | "walk" | "air" | "die" | "sit" | "dance" | "talk";
214
+ type ActorAct = "attack" | "hit" | "jump" | "wave" | "cheer" | "surprised";
215
+ declare const Transform: ComponentType<TransformData>;
216
+ declare const Visual: ComponentType<VisualData>;
217
+ /** Palette roles every adapter knows, with the colours used when the spec gives none. */
218
+ declare const DEFAULT_PALETTE: Readonly<Record<string, string>>;
219
+ /** A palette role or a #hex → #rrggbb (alpha dropped; Visual.alpha is the alpha). Unknown roles → text colour. */
220
+ declare function resolveColor(color: string, palette: Readonly<Record<string, string>>): string;
221
+ /** The spec's palette over the defaults, invalid colours dropped. */
222
+ declare function mergePalette(palette: Readonly<Record<string, string>> | undefined): Record<string, string>;
223
+ /** The input events adapters emit on topic "input". Blocks read these, never the engine's input. */
224
+ type InputEvent = {
225
+ type: "press" | "release" | "move";
226
+ x: number;
227
+ y: number;
228
+ }
229
+ /** A short press without much movement. */
230
+ | {
231
+ type: "tap";
232
+ x: number;
233
+ y: number;
234
+ } | {
235
+ type: "swipe";
236
+ dir: "left" | "right" | "up" | "down";
237
+ x: number;
238
+ y: number;
239
+ } | {
240
+ type: "key";
241
+ key: "left" | "right" | "up" | "down" | "action" | "back";
242
+ }
243
+ /** The key is let go (a held arrow moves a hero until then). */
244
+ | {
245
+ type: "key-up";
246
+ key: "left" | "right" | "up" | "down" | "action" | "back";
247
+ };
248
+ declare const INPUT_TOPIC = "input";
249
+ declare const VIEW_RESIZED_TOPIC = "view-resized";
250
+ /** Who an actor is: a look of the 2D hero rig as plain data (the adapter dresses the character). */
251
+ interface CastDef {
252
+ /** A ready look of the rig (@idosgames/characters HERO_PRESETS: "knight", "soldier", "camper"...). */
253
+ readonly preset?: string;
254
+ /** Parts over the preset: type -> part id ("Weapon": "weapon/battle_axe"), null takes it off. */
255
+ readonly parts?: Readonly<Record<string, string | null>>;
256
+ readonly colors?: Readonly<Record<string, string>>;
257
+ /** Races of the rig ("human", "orc"...); the actor's variant picks one, with its face, skin and hair. */
258
+ readonly races?: readonly string[];
259
+ }
260
+ /** How a running game looks besides its colours: the art style (src/art) and who plays the roles. */
261
+ interface ArtLook {
262
+ /** Art style id: the pack's ("fantasy", "sci-fi", "cozy", "post-apocalypse") or "default". */
263
+ readonly style: string;
264
+ readonly cast: Readonly<Record<string, CastDef>>;
265
+ }
266
+ /** The cast without a pack (a small fantasy crowd). A pack's cast goes over it role by role. */
267
+ declare const DEFAULT_CAST: Readonly<Record<string, CastDef>>;
268
+ declare const DEFAULT_LOOK: ArtLook;
269
+ /** Visual effects a block asks for on topic "fx". The logic never waits for them (a test sees none). */
270
+ type FxEvent =
271
+ /** Sparks flying out of a point. */
272
+ {
273
+ kind: "burst";
274
+ x: number;
275
+ y: number;
276
+ color: string;
277
+ n?: number;
278
+ size?: number;
279
+ }
280
+ /** A number or a word rising and fading ("+10"). */
281
+ | {
282
+ kind: "pop";
283
+ x: number;
284
+ y: number;
285
+ text: string;
286
+ color?: string;
287
+ size?: number;
288
+ }
289
+ /** A ring growing out of a point (a hit, a merge). */
290
+ | {
291
+ kind: "ring";
292
+ x: number;
293
+ y: number;
294
+ color: string;
295
+ size?: number;
296
+ }
297
+ /** The camera shakes. */
298
+ | {
299
+ kind: "shake";
300
+ power?: number;
301
+ }
302
+ /** The screen flashes a colour. */
303
+ | {
304
+ kind: "flash";
305
+ color: string;
306
+ };
307
+ declare const FX_TOPIC = "fx";
308
+
309
+ export { type ArtLook as A, type CastDef as C, DEFAULT_CAST as D, type Entity as E, type FxEvent as F, INPUT_TOPIC as I, type Rng as R, type System as S, type TransformData as T, type VisualData as V, World as W, type ActorAct as a, type ActorPose as b, type ComponentType as c, DEFAULT_LOOK as d, DEFAULT_PALETTE as e, type EventHandler as f, FX_TOPIC as g, type InputEvent as h, Transform as i, VIEW_RESIZED_TOPIC as j, Visual as k, type WorldOptions as l, type WorldVar as m, createFixedStepper as n, createRng as o, defineComponent as p, mergePalette as q, resolveColor as r };
@@ -0,0 +1,309 @@
1
+ /** A seeded random source. All methods advance the same state. */
2
+ interface Rng {
3
+ /** A float in [0, 1). */
4
+ next(): number;
5
+ /** An integer in [min, max] (both inclusive). */
6
+ int(min: number, max: number): number;
7
+ /** A float in [min, max). */
8
+ range(min: number, max: number): number;
9
+ /** true with the given probability (0..1). */
10
+ chance(probability: number): boolean;
11
+ /** One element of a non-empty list. */
12
+ pick<T>(items: readonly T[]): T;
13
+ /** One element by weight; weights ≤ 0 never win. Throws when no weight is positive. */
14
+ weighted<T>(items: readonly T[], weight: (item: T) => number): T;
15
+ /** A shuffled copy (Fisher–Yates). */
16
+ shuffle<T>(items: readonly T[]): T[];
17
+ /**
18
+ * An independent stream derived from this generator's SEED (not its current state) and a name.
19
+ * The same name always gives the same stream, however many numbers were drawn here before.
20
+ */
21
+ stream(name: string): Rng;
22
+ /** The seed this generator was created from. */
23
+ readonly seed: string;
24
+ }
25
+ /** A generator from any string or number seed. */
26
+ declare function createRng(seed: string | number): Rng;
27
+
28
+ type Entity = number;
29
+ /** A typed component token: `const Position = defineComponent<{ x: number; y: number }>("position")`. */
30
+ interface ComponentType<T> {
31
+ readonly name: string;
32
+ /** Phantom: carries the data type, never set at runtime. */
33
+ readonly __data?: T;
34
+ }
35
+ declare function defineComponent<T>(name: string): ComponentType<T>;
36
+ /** A value a rule or formula can read from `vars`. */
37
+ type WorldVar = number | string | boolean;
38
+ /** Logic that runs every step. Lower `order` runs first; equal orders keep the order they were added in. */
39
+ interface System {
40
+ readonly id: string;
41
+ readonly order?: number;
42
+ run(world: World, dt: number): void;
43
+ }
44
+ type EventHandler = (payload: Readonly<Record<string, unknown>>, world: World) => void;
45
+ interface WorldOptions {
46
+ readonly seed?: string | number;
47
+ /** Starting values of `vars` (score, lives, level, …). */
48
+ readonly vars?: Readonly<Record<string, WorldVar>>;
49
+ /**
50
+ * How many rounds of events emitted BY event handlers one step delivers before giving up.
51
+ * A guard against two rules that trigger each other forever.
52
+ */
53
+ readonly maxEventRounds?: number;
54
+ }
55
+ declare class World {
56
+ /** The world's random source. Systems should draw from their own `rng.stream(name)`. */
57
+ readonly rng: Rng;
58
+ /** Shared named values: score, lives, wave, coins of the run. Rules and formulas read them as `vars.*`. */
59
+ readonly vars: Record<string, WorldVar>;
60
+ /** Steps run so far. */
61
+ tick: number;
62
+ /** Seconds simulated so far (the sum of dt). */
63
+ time: number;
64
+ private nextId;
65
+ private readonly alive;
66
+ private readonly stores;
67
+ private readonly doomed;
68
+ private systems;
69
+ private systemSeq;
70
+ private readonly handlers;
71
+ private queue;
72
+ private readonly maxEventRounds;
73
+ private stepping;
74
+ constructor(options?: WorldOptions);
75
+ create(): Entity;
76
+ /** Removes the entity at the end of the current step (now, when called outside a step). */
77
+ destroy(e: Entity): void;
78
+ isAlive(e: Entity): boolean;
79
+ get entityCount(): number;
80
+ /** Sets (adds or replaces) a component. Returns the entity for chaining. */
81
+ set<T>(e: Entity, type: ComponentType<T>, value: T): Entity;
82
+ get<T>(e: Entity, type: ComponentType<T>): T | undefined;
83
+ /** Like get, but throws when the component is missing. */
84
+ need<T>(e: Entity, type: ComponentType<T>): T;
85
+ has(e: Entity, type: ComponentType<unknown>): boolean;
86
+ remove(e: Entity, type: ComponentType<unknown>): void;
87
+ /** Living entities that have ALL the given components, in id order. A snapshot: safe to mutate while iterating. */
88
+ query(...types: ComponentType<unknown>[]): Entity[];
89
+ addSystem(system: System): void;
90
+ removeSystem(id: string): void;
91
+ get systemIds(): string[];
92
+ /** Listen to a topic. Returns an unsubscribe function. */
93
+ on(topic: string, handler: EventHandler): () => void;
94
+ /**
95
+ * Queue an event. Delivered after the systems of the current step ran (or by `flushEvents()`
96
+ * outside a step), in emission order. Payload values should be plain data: rules read them.
97
+ */
98
+ emit(topic: string, payload?: Readonly<Record<string, unknown>>): void;
99
+ /** Deliver queued events now. Events emitted by handlers are delivered in further rounds. */
100
+ flushEvents(): void;
101
+ /** How many times event delivery hit the round limit (a sign of rules triggering each other). */
102
+ overflowed: number;
103
+ /** One step of `dt` seconds: systems in order, then events, then removal of destroyed entities. */
104
+ step(dt: number): void;
105
+ private purge;
106
+ }
107
+ /**
108
+ * Fixed-step driver: a renderer calls `advance(elapsedSeconds)` every frame, the world steps in
109
+ * equal slices (same result at 30 or 144 FPS). Returns the number of steps taken.
110
+ */
111
+ declare function createFixedStepper(world: World, options?: {
112
+ step?: number;
113
+ maxSteps?: number;
114
+ }): {
115
+ step: number;
116
+ advance(elapsed: number): number;
117
+ /** 0..1: how far between the last step and the next (for render interpolation). */
118
+ readonly alpha: number;
119
+ };
120
+
121
+ interface TransformData {
122
+ x: number;
123
+ y: number;
124
+ /** Radians. */
125
+ rotation?: number;
126
+ scale?: number;
127
+ }
128
+ type VisualData = {
129
+ shape: "rect";
130
+ w: number;
131
+ h: number;
132
+ /** Palette role or #hex. */
133
+ color: string;
134
+ /** Corner radius, logical pixels. */
135
+ radius?: number;
136
+ alpha?: number;
137
+ depth?: number;
138
+ } | {
139
+ shape: "circle";
140
+ r: number;
141
+ color: string;
142
+ alpha?: number;
143
+ depth?: number;
144
+ }
145
+ /** A picture of the kit's art (src/art: drawn in the pack's style, coloured by the palette). */
146
+ | {
147
+ shape: "sprite";
148
+ art: string;
149
+ w: number;
150
+ h: number;
151
+ /** The art's main colour (a palette role or #hex), for art that has one (a gem, a crystal, a cell). */
152
+ color?: string;
153
+ /** Mirrored left ↔ right. */
154
+ flipX?: boolean;
155
+ alpha?: number;
156
+ depth?: number;
157
+ }
158
+ /** A repeating picture over a box (the ground, a layer of the backdrop): scroll moves the pattern. */
159
+ | {
160
+ shape: "tile";
161
+ art: string;
162
+ w: number;
163
+ h: number;
164
+ /** Logical width of one repeat (default: the art's own aspect at the box height). */
165
+ unit?: number;
166
+ /** Pattern offset, logical px (a parallax layer adds what the world scrolled). */
167
+ scrollX?: number;
168
+ scrollY?: number;
169
+ color?: string;
170
+ alpha?: number;
171
+ depth?: number;
172
+ }
173
+ /**
174
+ * A living character on the 2D hero rig (@idosgames/characters): who it is comes from the cast of the
175
+ * asset pack ("hero", "foe", "foe2", "boss", "helper"), the variant picks its race and face. The box is
176
+ * h tall and the Transform is its centre (the feet stand at y + h / 2).
177
+ */
178
+ | {
179
+ shape: "actor";
180
+ cast: string;
181
+ variant?: number;
182
+ h: number;
183
+ /** Where the feet stand below the Transform (default h / 2: the Transform is the box's centre). */
184
+ feet?: number;
185
+ /** What it does all the time (loops; die and air are held). */
186
+ pose?: ActorPose;
187
+ /** A one-shot over the pose, played each time `actN` changes. */
188
+ act?: ActorAct;
189
+ actN?: number;
190
+ /** 1 faces right (default), -1 left. */
191
+ facing?: 1 | -1;
192
+ /** Logical px a second it moves over its ground: a run or a walk is played at that pace. */
193
+ speed?: number;
194
+ /** In the air (pose "air"): going up; otherwise coming down. */
195
+ rising?: boolean;
196
+ /** Attacks a second it makes (a fast fire plays the swing faster, so it ends before the next). */
197
+ actRate?: number;
198
+ /** Frozen where it is (the field is paused: an upgrade to pick). */
199
+ paused?: boolean;
200
+ alpha?: number;
201
+ depth?: number;
202
+ } | {
203
+ shape: "text";
204
+ text: string;
205
+ size: number;
206
+ color: string;
207
+ /** Font weight. */
208
+ bold?: boolean;
209
+ alpha?: number;
210
+ depth?: number;
211
+ };
212
+ /** "air": in a jump (held, rising or falling by `rising`); "die" is held at its end. */
213
+ type ActorPose = "idle" | "run" | "walk" | "air" | "die" | "sit" | "dance" | "talk";
214
+ type ActorAct = "attack" | "hit" | "jump" | "wave" | "cheer" | "surprised";
215
+ declare const Transform: ComponentType<TransformData>;
216
+ declare const Visual: ComponentType<VisualData>;
217
+ /** Palette roles every adapter knows, with the colours used when the spec gives none. */
218
+ declare const DEFAULT_PALETTE: Readonly<Record<string, string>>;
219
+ /** A palette role or a #hex → #rrggbb (alpha dropped; Visual.alpha is the alpha). Unknown roles → text colour. */
220
+ declare function resolveColor(color: string, palette: Readonly<Record<string, string>>): string;
221
+ /** The spec's palette over the defaults, invalid colours dropped. */
222
+ declare function mergePalette(palette: Readonly<Record<string, string>> | undefined): Record<string, string>;
223
+ /** The input events adapters emit on topic "input". Blocks read these, never the engine's input. */
224
+ type InputEvent = {
225
+ type: "press" | "release" | "move";
226
+ x: number;
227
+ y: number;
228
+ }
229
+ /** A short press without much movement. */
230
+ | {
231
+ type: "tap";
232
+ x: number;
233
+ y: number;
234
+ } | {
235
+ type: "swipe";
236
+ dir: "left" | "right" | "up" | "down";
237
+ x: number;
238
+ y: number;
239
+ } | {
240
+ type: "key";
241
+ key: "left" | "right" | "up" | "down" | "action" | "back";
242
+ }
243
+ /** The key is let go (a held arrow moves a hero until then). */
244
+ | {
245
+ type: "key-up";
246
+ key: "left" | "right" | "up" | "down" | "action" | "back";
247
+ };
248
+ declare const INPUT_TOPIC = "input";
249
+ declare const VIEW_RESIZED_TOPIC = "view-resized";
250
+ /** Who an actor is: a look of the 2D hero rig as plain data (the adapter dresses the character). */
251
+ interface CastDef {
252
+ /** A ready look of the rig (@idosgames/characters HERO_PRESETS: "knight", "soldier", "camper"...). */
253
+ readonly preset?: string;
254
+ /** Parts over the preset: type -> part id ("Weapon": "weapon/battle_axe"), null takes it off. */
255
+ readonly parts?: Readonly<Record<string, string | null>>;
256
+ readonly colors?: Readonly<Record<string, string>>;
257
+ /** Races of the rig ("human", "orc"...); the actor's variant picks one, with its face, skin and hair. */
258
+ readonly races?: readonly string[];
259
+ }
260
+ /** How a running game looks besides its colours: the art style (src/art) and who plays the roles. */
261
+ interface ArtLook {
262
+ /** Art style id: the pack's ("fantasy", "sci-fi", "cozy", "post-apocalypse") or "default". */
263
+ readonly style: string;
264
+ readonly cast: Readonly<Record<string, CastDef>>;
265
+ }
266
+ /** The cast without a pack (a small fantasy crowd). A pack's cast goes over it role by role. */
267
+ declare const DEFAULT_CAST: Readonly<Record<string, CastDef>>;
268
+ declare const DEFAULT_LOOK: ArtLook;
269
+ /** Visual effects a block asks for on topic "fx". The logic never waits for them (a test sees none). */
270
+ type FxEvent =
271
+ /** Sparks flying out of a point. */
272
+ {
273
+ kind: "burst";
274
+ x: number;
275
+ y: number;
276
+ color: string;
277
+ n?: number;
278
+ size?: number;
279
+ }
280
+ /** A number or a word rising and fading ("+10"). */
281
+ | {
282
+ kind: "pop";
283
+ x: number;
284
+ y: number;
285
+ text: string;
286
+ color?: string;
287
+ size?: number;
288
+ }
289
+ /** A ring growing out of a point (a hit, a merge). */
290
+ | {
291
+ kind: "ring";
292
+ x: number;
293
+ y: number;
294
+ color: string;
295
+ size?: number;
296
+ }
297
+ /** The camera shakes. */
298
+ | {
299
+ kind: "shake";
300
+ power?: number;
301
+ }
302
+ /** The screen flashes a colour. */
303
+ | {
304
+ kind: "flash";
305
+ color: string;
306
+ };
307
+ declare const FX_TOPIC = "fx";
308
+
309
+ export { type ArtLook as A, type CastDef as C, DEFAULT_CAST as D, type Entity as E, type FxEvent as F, INPUT_TOPIC as I, type Rng as R, type System as S, type TransformData as T, type VisualData as V, World as W, type ActorAct as a, type ActorPose as b, type ComponentType as c, DEFAULT_LOOK as d, DEFAULT_PALETTE as e, type EventHandler as f, FX_TOPIC as g, type InputEvent as h, Transform as i, VIEW_RESIZED_TOPIC as j, Visual as k, type WorldOptions as l, type WorldVar as m, createFixedStepper as n, createRng as o, defineComponent as p, mergePalette as q, resolveColor as r };