@simpludify/core 0.1.5 → 0.1.6

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/dist/index.js CHANGED
@@ -1583,7 +1583,7 @@ var RandomManager = class {
1583
1583
  };
1584
1584
  // packages/core/src/foundation/version.ts
1585
1585
  /** Source workspace version; checked against package.json by the build validator. */
1586
- var ENGINE_VERSION = "0.1.5";
1586
+ var ENGINE_VERSION = "0.1.6";
1587
1587
  // packages/core/src/scene/actor.ts
1588
1588
  var inputPhase$1 = ComponentPhase.Input;
1589
1589
  var updatePhase$1 = ComponentPhase.Update;
@@ -2814,7 +2814,7 @@ function boxCircle(box, circle, out, tolerance) {
2814
2814
  out.normalY = nx * box.sin + ny * box.cos;
2815
2815
  return true;
2816
2816
  }
2817
- function axis(a, b, x, y, out, tolerance) {
2817
+ function axis$1(a, b, x, y, out, tolerance) {
2818
2818
  const distance = (b.x - a.x) * x + (b.y - a.y) * y;
2819
2819
  const depth = a.hx * Math.abs(a.cos * x + a.sin * y) + a.hy * Math.abs(-a.sin * x + a.cos * y) + (b.hx * Math.abs(b.cos * x + b.sin * y) + b.hy * Math.abs(-b.sin * x + b.cos * y)) - Math.abs(distance);
2820
2820
  if (depth < -tolerance) return false;
@@ -2845,7 +2845,7 @@ function contactManifold(a, b, out, tolerance = 0) {
2845
2845
  }
2846
2846
  if (b.collider.shape === CollisionShape.Circle) return boxCircle(ag, bg, out, tolerance);
2847
2847
  out.depth = Infinity;
2848
- return axis(ag, bg, ag.cos, ag.sin, out, tolerance) && axis(ag, bg, -ag.sin, ag.cos, out, tolerance) && axis(ag, bg, bg.cos, bg.sin, out, tolerance) && axis(ag, bg, -bg.sin, bg.cos, out, tolerance);
2848
+ return axis$1(ag, bg, ag.cos, ag.sin, out, tolerance) && axis$1(ag, bg, -ag.sin, ag.cos, out, tolerance) && axis$1(ag, bg, bg.cos, bg.sin, out, tolerance) && axis$1(ag, bg, -bg.sin, bg.cos, out, tolerance);
2849
2849
  }
2850
2850
  // packages/core/src/game/collision-response.ts
2851
2851
  /** Translate along the minimum separation axis, preserving tangential movement intent. */
@@ -4080,6 +4080,135 @@ var TileGeometry = class TileGeometry {
4080
4080
  for (let dy = -radius; dy <= radius; dy++) for (let dx = Math.max(-radius, -dy - radius); dx <= Math.min(radius, -dy + radius); dx++) yield tile(x + dx, y + dy);
4081
4081
  }
4082
4082
  };
4083
+ // packages/core/src/interaction/gamepad-bindings.ts
4084
+ /** Standard mapping physical positions, not product-specific printed labels. */
4085
+ var GamepadButton = Object.freeze({
4086
+ South: 0,
4087
+ East: 1,
4088
+ West: 2,
4089
+ North: 3,
4090
+ LeftShoulder: 4,
4091
+ RightShoulder: 5,
4092
+ LeftTrigger: 6,
4093
+ RightTrigger: 7,
4094
+ Select: 8,
4095
+ Start: 9,
4096
+ LeftStick: 10,
4097
+ RightStick: 11,
4098
+ DPadUp: 12,
4099
+ DPadDown: 13,
4100
+ DPadLeft: 14,
4101
+ DPadRight: 15,
4102
+ Home: 16
4103
+ });
4104
+ var GamepadAxis = Object.freeze({
4105
+ LeftX: 0,
4106
+ LeftY: 1,
4107
+ RightX: 2,
4108
+ RightY: 3
4109
+ });
4110
+ var axis = (index) => Object.freeze({ axis: index });
4111
+ /** Ready-to-use data; game Actions retain application-defined names. */
4112
+ var GamepadBindings = Object.freeze({
4113
+ LeftStick: Object.freeze({
4114
+ x: axis(0),
4115
+ y: axis(1)
4116
+ }),
4117
+ RightStick: Object.freeze({
4118
+ x: axis(2),
4119
+ y: axis(3)
4120
+ }),
4121
+ DPad: Object.freeze({
4122
+ x: Object.freeze({
4123
+ negative: Object.freeze([14]),
4124
+ positive: Object.freeze([15])
4125
+ }),
4126
+ y: Object.freeze({
4127
+ negative: Object.freeze([12]),
4128
+ positive: Object.freeze([13])
4129
+ })
4130
+ }),
4131
+ Move: Object.freeze({
4132
+ x: Object.freeze({
4133
+ axis: 0,
4134
+ negative: Object.freeze([14]),
4135
+ positive: Object.freeze([15])
4136
+ }),
4137
+ y: Object.freeze({
4138
+ axis: 1,
4139
+ negative: Object.freeze([12]),
4140
+ positive: Object.freeze([13])
4141
+ })
4142
+ })
4143
+ });
4144
+ var fail = () => {
4145
+ throw new EngineError("INVALID_GAMEPAD_BINDING", "Invalid gamepad binding");
4146
+ };
4147
+ function gamepadIndex(value) {
4148
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) return fail();
4149
+ return value;
4150
+ }
4151
+ function gamepadDeadZone(value) {
4152
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value >= 1) return fail();
4153
+ return value;
4154
+ }
4155
+ function record(value, keys) {
4156
+ if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((k) => !keys.includes(k))) return fail();
4157
+ return Object.fromEntries(Object.entries(value));
4158
+ }
4159
+ function axisBinding(value) {
4160
+ const v = record(value, [
4161
+ "axis",
4162
+ "invert",
4163
+ "negative",
4164
+ "positive",
4165
+ "deadZone"
4166
+ ]);
4167
+ const indices = (value) => {
4168
+ if (value === void 0) return Object.freeze([]);
4169
+ if (!Array.isArray(value)) return fail();
4170
+ return Object.freeze([...new Set(value.map(gamepadIndex))]);
4171
+ };
4172
+ if (v.invert !== void 0 && typeof v.invert !== "boolean") return fail();
4173
+ return Object.freeze({
4174
+ ...v.axis === void 0 ? {} : { axis: gamepadIndex(v.axis) },
4175
+ ...v.invert === void 0 ? {} : { invert: v.invert },
4176
+ negative: indices(v.negative),
4177
+ positive: indices(v.positive),
4178
+ ...v.deadZone === void 0 ? {} : { deadZone: gamepadDeadZone(v.deadZone) }
4179
+ });
4180
+ }
4181
+ function checkedGamepadBindings(actions, value) {
4182
+ const v = record(value, Object.keys(actions));
4183
+ return Object.freeze(Object.entries(v).map(([name, binding]) => {
4184
+ if (!Object.hasOwn(actions, name)) return fail();
4185
+ const action = actions[name];
4186
+ if (!action) return fail();
4187
+ let result;
4188
+ if (action.kind === "button") {
4189
+ const b = record(binding, ["buttons"]);
4190
+ if (b.buttons !== void 0 && !Array.isArray(b.buttons)) return fail();
4191
+ result = Object.freeze({ buttons: Object.freeze((b.buttons ?? []).map((item) => {
4192
+ if (typeof item === "number") return gamepadIndex(item);
4193
+ const b = record(item, ["button", "threshold"]), threshold = b.threshold ?? .5;
4194
+ if (typeof threshold !== "number" || !Number.isFinite(threshold) || threshold <= 0 || threshold > 1) return fail();
4195
+ return Object.freeze({
4196
+ button: gamepadIndex(b.button),
4197
+ threshold
4198
+ });
4199
+ })) });
4200
+ } else if (action.kind === "axis1d") result = axisBinding(binding);
4201
+ else {
4202
+ const keys = action.kind === "axis2d" ? ["x", "y"] : [
4203
+ "x",
4204
+ "y",
4205
+ "z"
4206
+ ], b = record(binding, keys);
4207
+ result = Object.freeze(Object.fromEntries(keys.map((k) => [k, axisBinding(b[k] ?? {})])));
4208
+ }
4209
+ return Object.freeze([action, result]);
4210
+ }));
4211
+ }
4083
4212
  // packages/core/src/interaction/key-code.ts
4084
4213
  /** Physical UI Events code values plus F13–F24; Unidentified is not bindable. */
4085
4214
  var KeyCode = {
@@ -4275,6 +4404,27 @@ function parseKeyCode(value) {
4275
4404
  if (!isKeyCode(value)) throw new EngineError("INVALID_KEY_CODE", "Unknown physical keyboard code", { value });
4276
4405
  return value;
4277
4406
  }
4407
+ // packages/core/src/interaction/physical-input.ts
4408
+ var InputDevice = Object.freeze({
4409
+ Keyboard: "keyboard",
4410
+ Mouse: "mouse",
4411
+ Gamepad: "gamepad"
4412
+ });
4413
+ /** Standard PointerEvent.button indices; these are not the buttons bit mask. */
4414
+ var MouseButton = Object.freeze({
4415
+ Left: 0,
4416
+ Middle: 1,
4417
+ Right: 2,
4418
+ Back: 3,
4419
+ Forward: 4
4420
+ });
4421
+ function isMouseButton(value) {
4422
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 4;
4423
+ }
4424
+ function parseMouseButton(value) {
4425
+ if (!isMouseButton(value)) throw new EngineError("INVALID_MOUSE_BUTTON", "Expected a standard mouse button index (0–4)");
4426
+ return value;
4427
+ }
4278
4428
  // packages/core/src/interaction/action-bindings.ts
4279
4429
  function checkedBinding(action, value) {
4280
4430
  const record = (v, allowed) => {
@@ -4288,43 +4438,123 @@ function checkedBinding(action, value) {
4288
4438
  if (!Array.isArray(v)) throw new EngineError("INVALID_BINDING", "Key list must be an array");
4289
4439
  return Object.freeze([...new Set(v.map(parseKeyCode))]);
4290
4440
  };
4291
- const axis = (v) => {
4292
- const r = record(v ?? {}, ["negative", "positive"]);
4441
+ const mice = (v) => {
4442
+ if (!Array.isArray(v)) throw new EngineError("INVALID_BINDING", "Mouse button list must be an array");
4443
+ return Object.freeze([...new Set(v.map(parseMouseButton))]);
4444
+ };
4445
+ const axis = (v, root = false) => {
4446
+ const r = record(v ?? {}, [
4447
+ "negative",
4448
+ "positive",
4449
+ "mouseNegative",
4450
+ "mousePositive",
4451
+ ...root ? ["gamepad"] : []
4452
+ ]);
4293
4453
  return Object.freeze({
4294
4454
  negative: codes(r.negative),
4295
- positive: codes(r.positive)
4455
+ positive: codes(r.positive),
4456
+ ...r.mouseNegative === void 0 ? {} : { mouseNegative: mice(r.mouseNegative) },
4457
+ ...r.mousePositive === void 0 ? {} : { mousePositive: mice(r.mousePositive) }
4296
4458
  });
4297
4459
  };
4298
- if (action.kind === "button") return Object.freeze({ keys: codes(record(value, ["keys"]).keys) });
4299
- if (action.kind === "axis1d") return axis(value);
4300
- const r = record(value, action.kind === "axis2d" ? ["x", "y"] : [
4460
+ const r = record(value, action.kind === "button" ? [
4461
+ "keys",
4462
+ "mouse",
4463
+ "gamepad"
4464
+ ] : action.kind === "axis1d" ? [
4465
+ "negative",
4466
+ "positive",
4467
+ "mouseNegative",
4468
+ "mousePositive",
4469
+ "gamepad"
4470
+ ] : action.kind === "axis2d" ? [
4301
4471
  "x",
4302
4472
  "y",
4303
- "z"
4473
+ "gamepad"
4474
+ ] : [
4475
+ "x",
4476
+ "y",
4477
+ "z",
4478
+ "gamepad"
4304
4479
  ]);
4305
- return Object.freeze(action.kind === "axis2d" ? {
4480
+ let gamepad;
4481
+ if (r.gamepad !== void 0) {
4482
+ const checked = checkedGamepadBindings({ [action.name]: action }, { [action.name]: r.gamepad })[0];
4483
+ if (!checked) throw new EngineError("INVALID_BINDING", "Missing gamepad binding");
4484
+ gamepad = checked[1];
4485
+ }
4486
+ const base = action.kind === "button" ? {
4487
+ keys: codes(r.keys),
4488
+ ...r.mouse === void 0 ? {} : { mouse: mice(r.mouse) }
4489
+ } : action.kind === "axis1d" ? axis(value, true) : action.kind === "axis2d" ? {
4306
4490
  x: axis(r.x),
4307
4491
  y: axis(r.y)
4308
4492
  } : {
4309
4493
  x: axis(r.x),
4310
4494
  y: axis(r.y),
4311
4495
  z: axis(r.z)
4496
+ };
4497
+ return Object.freeze({
4498
+ ...base,
4499
+ ...gamepad === void 0 ? {} : { gamepad }
4312
4500
  });
4313
4501
  }
4314
4502
  function keyLists(binding, path = "") {
4315
4503
  const lists = [];
4316
- for (const [key, value] of Object.entries(binding)) if (Array.isArray(value)) lists.push({
4504
+ for (const [key, value] of Object.entries(binding)) if (key === "keys" || key === "negative" || key === "positive") lists.push({
4317
4505
  path: path + key,
4318
4506
  keys: value
4319
4507
  });
4320
- else lists.push(...keyLists(value, `${path}${key}.`));
4508
+ else if (key === "x" || key === "y" || key === "z") lists.push(...keyLists(value, `${path}${key}.`));
4509
+ return lists;
4510
+ }
4511
+ function mouseLists(binding, path = "") {
4512
+ const lists = [];
4513
+ for (const [key, value] of Object.entries(binding)) if (key === "mouse" || key === "mouseNegative" || key === "mousePositive") lists.push({
4514
+ path: path + key,
4515
+ buttons: value
4516
+ });
4517
+ else if (key === "x" || key === "y" || key === "z") lists.push(...mouseLists(value, `${path}${key}.`));
4321
4518
  return lists;
4322
4519
  }
4520
+ function gamepadControls(binding, path = "gamepad") {
4521
+ const result = [];
4522
+ for (const [name, value] of Object.entries(binding)) if (name === "axis" && typeof value === "number") result.push({
4523
+ kind: "axis",
4524
+ index: value,
4525
+ path: `${path}.axis`
4526
+ });
4527
+ else if (name === "buttons" || name === "negative" || name === "positive") for (const item of value) result.push({
4528
+ kind: "button",
4529
+ index: typeof item === "number" ? item : item.button,
4530
+ path: `${path}.${name}`
4531
+ });
4532
+ else if (name === "x" || name === "y" || name === "z") result.push(...gamepadControls(value, `${path}.${name}`));
4533
+ return result;
4534
+ }
4535
+ function editGamepad(current, patch, remove) {
4536
+ if (Array.isArray(patch)) {
4537
+ const old = Array.isArray(current) ? current : [];
4538
+ const control = (v) => v && typeof v === "object" && "button" in v ? v.button : v;
4539
+ return remove ? old.filter((v) => !patch.some((p) => control(p) === control(v))) : [...new Map([...old, ...patch].map((v) => [JSON.stringify(v), v])).values()];
4540
+ }
4541
+ if (patch && typeof patch === "object") {
4542
+ const result = current && typeof current === "object" ? { ...current } : {};
4543
+ for (const [key, value] of Object.entries(patch)) {
4544
+ const edited = editGamepad(result[key], value, remove);
4545
+ if (edited === void 0) delete result[key];
4546
+ else result[key] = edited;
4547
+ }
4548
+ return result;
4549
+ }
4550
+ return remove ? current === patch ? void 0 : current : patch;
4551
+ }
4323
4552
  function editBinding(current, patch, remove) {
4324
4553
  const result = { ...current };
4325
4554
  for (const [name, value] of Object.entries(patch)) {
4326
4555
  const old = current[name];
4327
- if (Array.isArray(value)) result[name] = remove ? old.filter((k) => !value.includes(k)) : [.../* @__PURE__ */ new Set([...old, ...value])];
4556
+ if (name === "gamepad") result[name] = editGamepad(old, value, remove);
4557
+ else if (Array.isArray(value)) result[name] = remove ? (old ?? []).filter((k) => !value.includes(k)) : [.../* @__PURE__ */ new Set([...old ?? [], ...value])];
4328
4558
  else result[name] = editBinding(old, value, remove);
4329
4559
  }
4330
4560
  return result;
@@ -4522,6 +4752,9 @@ var ActionController = class {
4522
4752
  bus = new EventBus();
4523
4753
  keyboardAllowed = true;
4524
4754
  blockedKeys = /* @__PURE__ */ new Set();
4755
+ mouseAllowed = true;
4756
+ blockedMouse = /* @__PURE__ */ new Set();
4757
+ bindingGeneration = 0;
4525
4758
  pending = [];
4526
4759
  transitions = [];
4527
4760
  constructor(actions, options) {
@@ -4529,6 +4762,8 @@ var ActionController = class {
4529
4762
  this.options = options;
4530
4763
  if (options.keyboard !== void 0 && typeof options.keyboard !== "boolean") throw new EngineError("INVALID_VALUE", "Keyboard enabled must be boolean");
4531
4764
  this.keyboardAllowed = options.keyboard ?? true;
4765
+ if (options.mouse !== void 0 && typeof options.mouse !== "boolean") throw new EngineError("INVALID_VALUE", "Mouse enabled must be boolean");
4766
+ this.mouseAllowed = options.mouse ?? true;
4532
4767
  for (const action of Object.values(actions)) {
4533
4768
  const binding = checkedBinding(action, options.bindings && Object.hasOwn(options.bindings, action.name) ? options.bindings[action.name] ?? {} : {});
4534
4769
  const value = neutral(action);
@@ -4575,6 +4810,24 @@ var ActionController = class {
4575
4810
  get sourceCount() {
4576
4811
  return this.sources.size;
4577
4812
  }
4813
+ get mouseEnabled() {
4814
+ return this.mouseAllowed;
4815
+ }
4816
+ set mouseEnabled(value) {
4817
+ if (this.disposed) throw new EngineError("OWNER_DISPOSED", "Action controller has been disposed");
4818
+ if (typeof value !== "boolean") throw new EngineError("INVALID_VALUE", "Mouse enabled must be boolean");
4819
+ if (value === this.mouseAllowed) return;
4820
+ this.mouseAllowed = value;
4821
+ this.blockedMouse.clear();
4822
+ if (value) {
4823
+ for (const entry of this.entries.values()) for (const list of mouseLists(entry.binding)) for (const button of list.buttons) if (this.options.input.mouseDown(button)) this.blockedMouse.add(button);
4824
+ }
4825
+ for (const action of this.entries.keys()) this.recompute(action);
4826
+ }
4827
+ /** @internal Used by existing host adapters to follow common binding settings. */
4828
+ get bindingRevision() {
4829
+ return this.bindingGeneration;
4830
+ }
4578
4831
  onDispose(callback) {
4579
4832
  return this.lifetime.onDispose(callback);
4580
4833
  }
@@ -4653,13 +4906,13 @@ var ActionController = class {
4653
4906
  }
4654
4907
  return source;
4655
4908
  }
4656
- /** @internal */ write(source, action, value) {
4909
+ /** @internal */ write(source, action, value, edges = true) {
4657
4910
  this.options.input.assertWrite(isNeutral(value));
4658
4911
  this.entry(action);
4659
4912
  const values = this.sources.get(source);
4660
4913
  if (!values) throw new EngineError("OWNER_DISPOSED", "Action source has been disposed");
4661
4914
  values.set(action, value);
4662
- this.recompute(action);
4915
+ this.recompute(action, edges);
4663
4916
  }
4664
4917
  /** @internal */ clearSource(source, remove) {
4665
4918
  const values = this.sources.get(source);
@@ -4670,11 +4923,11 @@ var ActionController = class {
4670
4923
  if (!this.disposed) for (const action of actions) this.recompute(action);
4671
4924
  }
4672
4925
  keyboard(action) {
4673
- if (!this.keyboardAllowed) return neutral(action);
4674
4926
  const binding = this.entry(action).binding;
4675
- const held = (keys) => keys?.some((k) => !this.blockedKeys.has(k) && this.options.input.keyDown(k)) ?? false;
4676
- const axis = (b) => Number(held(b?.positive)) - Number(held(b?.negative));
4677
- if (action.kind === "button") return held(binding.keys);
4927
+ const held = (keys) => this.keyboardAllowed && (keys?.some((k) => !this.blockedKeys.has(k) && this.options.input.keyDown(k)) ?? false);
4928
+ const mouse = (buttons) => this.mouseAllowed && (buttons?.some((b) => !this.blockedMouse.has(b) && this.options.input.mouseDown(b)) ?? false);
4929
+ const axis = (b) => Number(held(b?.positive) || mouse(b?.mousePositive)) - Number(held(b?.negative) || mouse(b?.mouseNegative));
4930
+ if (action.kind === "button") return held(binding.keys) || mouse(binding.mouse);
4678
4931
  if (action.kind === "axis1d") return axis(binding);
4679
4932
  const b = binding;
4680
4933
  return action.kind === "axis2d" ? {
@@ -4727,11 +4980,15 @@ var ActionController = class {
4727
4980
  }
4728
4981
  /** @internal */ keyChanged() {
4729
4982
  for (const key of this.blockedKeys) if (!this.options.input.keyDown(key)) this.blockedKeys.delete(key);
4983
+ for (const button of this.blockedMouse) if (!this.options.input.mouseDown(button)) this.blockedMouse.delete(button);
4730
4984
  if (this.active || !this.disposed && this.options.scene.inputOwner === this.options.input && (this.options.scene.state === SceneState.Preparing || this.options.scene.state === SceneState.Ready)) for (const action of this.entries.keys()) this.recompute(action);
4731
4985
  }
4732
4986
  /** @internal */ accepts(code) {
4733
4987
  return this.active && this.keyboardAllowed && [...this.entries.values()].some((e) => keyLists(e.binding).some((list) => list.keys.includes(code)));
4734
4988
  }
4989
+ /** @internal */ acceptsMouse(button) {
4990
+ return this.active && this.mouseAllowed && [...this.entries.values()].some((e) => mouseLists(e.binding).some((list) => list.buttons.includes(button)));
4991
+ }
4735
4992
  /** @internal */ capture(override) {
4736
4993
  if (!this.active) return;
4737
4994
  const tokens = override ? new Map([...this.entries.keys()].map((a) => [a.name, a])) : void 0;
@@ -4773,16 +5030,25 @@ var ActionController = class {
4773
5030
  /** Clears held contributions and unconsumed edges; no synthetic button release. */
4774
5031
  reset() {
4775
5032
  this.blockedKeys.clear();
5033
+ this.blockedMouse.clear();
4776
5034
  for (const values of this.sources.values()) values.clear();
4777
5035
  this.pending = [];
4778
5036
  for (const [action, entry] of this.entries) entry.value = neutral(action);
4779
5037
  }
5038
+ /** @internal Flush device transitions without destroying explicit program values. */
5039
+ resetPhysical() {
5040
+ this.blockedKeys.clear();
5041
+ this.blockedMouse.clear();
5042
+ this.pending = [];
5043
+ for (const action of this.entries.keys()) this.recompute(action, false);
5044
+ }
4780
5045
  getBinding(action) {
4781
5046
  return this.entry(action).binding;
4782
5047
  }
4783
5048
  rebind(action, binding) {
4784
5049
  const checked = checkedBinding(action, binding), entry = this.entry(action);
4785
5050
  entry.binding = checked;
5051
+ this.bindingGeneration++;
4786
5052
  this.pending = this.pending.filter((event) => event.action !== action);
4787
5053
  this.recompute(action, false);
4788
5054
  }
@@ -4795,7 +5061,13 @@ var ActionController = class {
4795
5061
  resetBindings(action) {
4796
5062
  for (const token of action ? [action] : this.entries.keys()) this.rebind(token, this.entry(token).defaults);
4797
5063
  }
4798
- conflicts() {
5064
+ conflicts(device = "keyboard") {
5065
+ if (![
5066
+ "keyboard",
5067
+ "mouse",
5068
+ "gamepad",
5069
+ "all"
5070
+ ].includes(device)) throw new EngineError("INVALID_BINDING", "Unknown conflict device");
4799
5071
  const keys = /* @__PURE__ */ new Map();
4800
5072
  for (const [action, entry] of this.entries) for (const list of keyLists(entry.binding)) for (const key of list.keys) {
4801
5073
  const group = keys.get(key) ?? [];
@@ -4805,10 +5077,54 @@ var ActionController = class {
4805
5077
  });
4806
5078
  keys.set(key, group);
4807
5079
  }
4808
- return [...keys].filter(([, bindings]) => bindings.length > 1).map(([key, bindings]) => ({
5080
+ const keyboard = [...keys].filter(([, bindings]) => bindings.length > 1).map(([key, bindings]) => ({
4809
5081
  key,
4810
5082
  bindings
4811
5083
  }));
5084
+ if (device === "keyboard") return keyboard;
5085
+ const mice = /* @__PURE__ */ new Map();
5086
+ const pads = /* @__PURE__ */ new Map();
5087
+ for (const [action, entry] of this.entries) {
5088
+ for (const list of mouseLists(entry.binding)) for (const button of list.buttons) {
5089
+ const uses = mice.get(button) ?? [];
5090
+ uses.push({
5091
+ action,
5092
+ path: list.path
5093
+ });
5094
+ mice.set(button, uses);
5095
+ }
5096
+ const pad = entry.binding.gamepad;
5097
+ if (pad) for (const input of gamepadControls(pad)) {
5098
+ const key = `${input.kind}:${input.index}`;
5099
+ const group = pads.get(key) ?? {
5100
+ kind: input.kind,
5101
+ index: input.index,
5102
+ bindings: []
5103
+ };
5104
+ group.bindings.push({
5105
+ action,
5106
+ path: input.path
5107
+ });
5108
+ pads.set(key, group);
5109
+ }
5110
+ }
5111
+ const mouse = [...mice].filter(([, uses]) => uses.length > 1).map(([button, bindings]) => ({
5112
+ device: "mouse",
5113
+ button,
5114
+ bindings
5115
+ }));
5116
+ const gamepad = [...pads.values()].filter((group) => group.bindings.length > 1).map((group) => ({
5117
+ device: "gamepad",
5118
+ ...group
5119
+ }));
5120
+ return device === "mouse" ? mouse : device === "gamepad" ? gamepad : [
5121
+ ...keyboard.map((group) => ({
5122
+ device: "keyboard",
5123
+ ...group
5124
+ })),
5125
+ ...mouse,
5126
+ ...gamepad
5127
+ ];
4812
5128
  }
4813
5129
  exportBindings() {
4814
5130
  return {
@@ -4847,6 +5163,10 @@ var ActionSource = class extends OwnedInputSource {
4847
5163
  const checked = checkedActionValue(action, value);
4848
5164
  if (this.writable(isNeutral(checked))) this.controller.write(this, action, checked);
4849
5165
  }
5166
+ /** @internal Reset one device binding without synthesizing logical edges. */
5167
+ resetBinding(action) {
5168
+ if (this.writable(true)) this.controller.write(this, action, neutral(action), false);
5169
+ }
4850
5170
  release(remove) {
4851
5171
  this.controller.clearSource(this, remove);
4852
5172
  }
@@ -4869,20 +5189,38 @@ var KeyboardSource = class extends OwnedInputSource {
4869
5189
  this.input.releaseKeyboard(this, remove);
4870
5190
  }
4871
5191
  };
5192
+ /** Physical mouse contribution; game state is read through ActionController. */
5193
+ var MouseSource = class extends OwnedInputSource {
5194
+ input;
5195
+ /** @internal */ constructor(input) {
5196
+ super();
5197
+ this.input = input;
5198
+ }
5199
+ set(button, pressed) {
5200
+ parseMouseButton(button);
5201
+ if (typeof pressed !== "boolean") throw new EngineError("INVALID_VALUE", "Mouse state must be boolean");
5202
+ if (this.writable(!pressed)) this.input.writeMouse(this, button, pressed);
5203
+ }
5204
+ release(remove) {
5205
+ this.input.releaseMouse(this, remove);
5206
+ }
5207
+ };
4872
5208
  /** Engine-owned routing and update boundary. No raw game-state/alternate movement API. */
4873
5209
  var InputSystem = class {
4874
5210
  actions = /* @__PURE__ */ new Set();
4875
5211
  adapters = /* @__PURE__ */ new Set();
4876
5212
  keyboards = /* @__PURE__ */ new Map();
5213
+ mice = /* @__PURE__ */ new Map();
5214
+ captureCancel;
4877
5215
  dead = false;
4878
5216
  playback = false;
4879
5217
  frames;
4880
5218
  /** @internal Device hosts may reject live gestures during Replay playback. */
4881
5219
  get liveInput() {
4882
- return !this.dead && !this.playback;
5220
+ return !this.dead && !this.playback && !this.captureCancel;
4883
5221
  }
4884
5222
  get sourceCount() {
4885
- let count = this.keyboards.size;
5223
+ let count = this.keyboards.size + this.mice.size;
4886
5224
  for (const actions of this.actions) count += actions.sourceCount;
4887
5225
  return count;
4888
5226
  }
@@ -4902,10 +5240,19 @@ var InputSystem = class {
4902
5240
  };
4903
5241
  }
4904
5242
  acceptsKey(code) {
4905
- if (this.playback) return false;
5243
+ if (!this.liveInput) return false;
4906
5244
  for (const actions of this.actions) if (actions.accepts(code)) return true;
4907
5245
  return false;
4908
5246
  }
5247
+ acceptsMouse(button) {
5248
+ if (!this.liveInput) return false;
5249
+ for (const actions of this.actions) if (actions.acceptsMouse(button)) return true;
5250
+ return false;
5251
+ }
5252
+ /** @internal */ mouseDown(button) {
5253
+ for (const buttons of this.mice.values()) if (buttons.has(button)) return true;
5254
+ return false;
5255
+ }
4909
5256
  notify() {
4910
5257
  for (const actions of this.actions) actions.keyChanged();
4911
5258
  }
@@ -4949,10 +5296,42 @@ var InputSystem = class {
4949
5296
  }
4950
5297
  return source;
4951
5298
  }
5299
+ createMouseSource(owner) {
5300
+ if (this.dead || owner?.disposed) throw new EngineError("OWNER_DISPOSED", "Input owner has been disposed");
5301
+ const source = new MouseSource(this);
5302
+ this.mice.set(source, /* @__PURE__ */ new Set());
5303
+ try {
5304
+ if (owner) source.own(owner);
5305
+ } catch (error) {
5306
+ source.dispose();
5307
+ throw error;
5308
+ }
5309
+ return source;
5310
+ }
5311
+ /** @internal */ writeMouse(source, button, pressed) {
5312
+ this.assertWrite(!pressed);
5313
+ const buttons = this.mice.get(source);
5314
+ if (!buttons || this.dead) throw new EngineError("OWNER_DISPOSED", "Input has been disposed");
5315
+ if (this.captureCancel && pressed) return;
5316
+ if (buttons.has(button) === pressed) return;
5317
+ const before = this.mouseDown(button);
5318
+ if (pressed) buttons.add(button);
5319
+ else buttons.delete(button);
5320
+ if (before !== this.mouseDown(button)) this.notify();
5321
+ }
5322
+ /** @internal */ releaseMouse(source, remove) {
5323
+ const buttons = this.mice.get(source);
5324
+ if (!buttons) return;
5325
+ const changed = buttons.size > 0;
5326
+ buttons.clear();
5327
+ if (remove) this.mice.delete(source);
5328
+ if (changed && !this.dead) this.notify();
5329
+ }
4952
5330
  /** @internal */ writeKey(source, code, pressed) {
4953
5331
  this.assertWrite(!pressed);
4954
5332
  const keys = this.keyboards.get(source);
4955
5333
  if (!keys || this.dead) throw new EngineError("OWNER_DISPOSED", "Input has been disposed");
5334
+ if (this.captureCancel && pressed) return;
4956
5335
  if (keys.has(code) === pressed) return;
4957
5336
  const before = this.keyDown(code);
4958
5337
  if (pressed) keys.add(code);
@@ -4991,11 +5370,41 @@ var InputSystem = class {
4991
5370
  this.playback = false;
4992
5371
  this.clear();
4993
5372
  }
4994
- clear() {
5373
+ /** @internal Host capture gate. Program ActionSource values remain explicit. */
5374
+ suspendLiveInput(cancel) {
5375
+ if (!this.liveInput) throw new EngineError("INPUT_CAPTURE_UNAVAILABLE", "Live input is unavailable or another capture is active");
5376
+ this.captureCancel = cancel;
5377
+ try {
5378
+ this.resetPhysical();
5379
+ } catch (error) {
5380
+ this.captureCancel = void 0;
5381
+ throw error;
5382
+ }
5383
+ return () => {
5384
+ if (this.captureCancel !== cancel) return;
5385
+ this.captureCancel = void 0;
5386
+ this.resetPhysical();
5387
+ };
5388
+ }
5389
+ resetPhysical() {
4995
5390
  for (const keys of this.keyboards.values()) keys.clear();
4996
- cleanupAll([...[...this.actions].map((actions) => () => actions.reset()), ...[...this.adapters].map((adapter) => () => {
5391
+ for (const buttons of this.mice.values()) buttons.clear();
5392
+ cleanupAll([...[...this.adapters].map((adapter) => () => {
4997
5393
  if (this.adapters.has(adapter)) adapter.reset();
4998
- })], "Input reset failed");
5394
+ }), ...[...this.actions].map((actions) => () => actions.resetPhysical())], "Physical input reset failed");
5395
+ }
5396
+ clear() {
5397
+ const cancel = this.captureCancel;
5398
+ this.captureCancel = void 0;
5399
+ for (const keys of this.keyboards.values()) keys.clear();
5400
+ for (const buttons of this.mice.values()) buttons.clear();
5401
+ cleanupAll([
5402
+ () => cancel?.(),
5403
+ ...[...this.actions].map((actions) => () => actions.reset()),
5404
+ ...[...this.adapters].map((adapter) => () => {
5405
+ if (this.adapters.has(adapter)) adapter.reset();
5406
+ })
5407
+ ], "Input reset failed");
4999
5408
  }
5000
5409
  /** Device diagnostics only; game state is ActionController.snapshot(). */
5001
5410
  snapshot() {
@@ -5006,18 +5415,122 @@ var InputSystem = class {
5006
5415
  dispose() {
5007
5416
  if (this.dead) return;
5008
5417
  this.dead = true;
5418
+ const cancel = this.captureCancel;
5419
+ this.captureCancel = void 0;
5009
5420
  try {
5010
5421
  cleanupAll([
5422
+ () => cancel?.(),
5011
5423
  ...[...this.adapters].map((a) => () => a.dispose()),
5012
5424
  ...[...this.actions].map((a) => () => a.dispose()),
5013
- ...[...this.keyboards.keys()].map((s) => () => s.dispose())
5425
+ ...[...this.keyboards.keys()].map((s) => () => s.dispose()),
5426
+ ...[...this.mice.keys()].map((s) => () => s.dispose())
5014
5427
  ], "Input cleanup failed");
5015
5428
  } finally {
5016
5429
  this.adapters.clear();
5017
5430
  this.actions.clear();
5018
5431
  this.keyboards.clear();
5432
+ this.mice.clear();
5433
+ }
5434
+ }
5435
+ };
5436
+ // packages/core/src/interaction/input-capture.ts
5437
+ var InputCaptureState = Object.freeze({
5438
+ Arming: "arming",
5439
+ Listening: "listening",
5440
+ Completed: "completed",
5441
+ Cancelled: "cancelled"
5442
+ });
5443
+ /** A portable single-input session. Hosts supply neutral arming and physical events. */
5444
+ var InputCapture = class {
5445
+ life = new Lifetime();
5446
+ status = InputCaptureState.Arming;
5447
+ devices;
5448
+ gamepadIndex;
5449
+ result;
5450
+ settle = () => {};
5451
+ constructor(options = {}) {
5452
+ if (!options || typeof options !== "object" || Array.isArray(options) || Object.keys(options).some((key) => ![
5453
+ "devices",
5454
+ "gamepadIndex",
5455
+ "owner"
5456
+ ].includes(key))) throw new EngineError("INVALID_INPUT_CAPTURE", "Invalid input capture options");
5457
+ const devices = options.devices ?? Object.values(InputDevice);
5458
+ if (!Array.isArray(devices) || !devices.length || devices.some((device) => !Object.values(InputDevice).includes(device))) throw new EngineError("INVALID_INPUT_CAPTURE", "Select at least one known capture device");
5459
+ this.devices = Object.freeze([...new Set(devices)]);
5460
+ this.gamepadIndex = options.gamepadIndex === void 0 ? void 0 : gamepadIndex(options.gamepadIndex);
5461
+ if (options.owner?.disposed) throw new EngineError("OWNER_DISPOSED", "Capture owner has been disposed");
5462
+ this.result = new Promise((resolve) => {
5463
+ this.settle = resolve;
5464
+ });
5465
+ if (options.owner) this.life.onDispose(options.owner.onDispose(() => this.cancel()));
5466
+ }
5467
+ get state() {
5468
+ return this.status;
5469
+ }
5470
+ get disposed() {
5471
+ return this.life.disposed;
5472
+ }
5473
+ onDispose(callback) {
5474
+ return this.life.onDispose(callback);
5475
+ }
5476
+ /** @internal Hosts call only after all eligible preheld buttons/keys are neutral. */
5477
+ arm() {
5478
+ if (this.status === InputCaptureState.Arming) this.status = InputCaptureState.Listening;
5479
+ }
5480
+ /** @internal Offer one fresh physical down, never a repeat or opening gesture. */
5481
+ accept(input) {
5482
+ if (this.status !== InputCaptureState.Listening) return false;
5483
+ if (!input || typeof input !== "object" || !Object.values(InputDevice).includes(input.device)) throw new EngineError("INVALID_INPUT_CAPTURE", "Unknown physical input device");
5484
+ if (!this.devices.includes(input.device)) return false;
5485
+ let descriptor, binding;
5486
+ if (input.device === InputDevice.Keyboard) {
5487
+ const code = parseKeyCode(input.code);
5488
+ descriptor = {
5489
+ device: InputDevice.Keyboard,
5490
+ code
5491
+ };
5492
+ binding = Object.freeze({ keys: Object.freeze([code]) });
5493
+ } else if (input.device === InputDevice.Mouse) {
5494
+ const button = parseMouseButton(input.button);
5495
+ descriptor = {
5496
+ device: InputDevice.Mouse,
5497
+ button
5498
+ };
5499
+ binding = Object.freeze({ mouse: Object.freeze([button]) });
5500
+ } else {
5501
+ const index = gamepadIndex(input.index), button = gamepadIndex(input.button);
5502
+ if (this.gamepadIndex !== void 0 && this.gamepadIndex !== index) return false;
5503
+ if (typeof input.id !== "string" || typeof input.mapping !== "string") throw new EngineError("INVALID_INPUT_CAPTURE", "Gamepad capture requires device facts");
5504
+ descriptor = {
5505
+ device: InputDevice.Gamepad,
5506
+ index,
5507
+ button,
5508
+ id: input.id,
5509
+ mapping: input.mapping
5510
+ };
5511
+ binding = Object.freeze({ gamepad: Object.freeze({ buttons: Object.freeze([button]) }) });
5512
+ }
5513
+ this.finish(InputCaptureState.Completed, Object.freeze({
5514
+ ...descriptor,
5515
+ binding
5516
+ }));
5517
+ return true;
5518
+ }
5519
+ finish(state, input) {
5520
+ if (this.disposed) return;
5521
+ this.status = state;
5522
+ try {
5523
+ this.life.dispose();
5524
+ } finally {
5525
+ this.settle(input);
5019
5526
  }
5020
5527
  }
5528
+ cancel() {
5529
+ this.finish(InputCaptureState.Cancelled, void 0);
5530
+ }
5531
+ dispose() {
5532
+ this.cancel();
5533
+ }
5021
5534
  };
5022
5535
  // packages/core/src/ui/canvas-layout.ts
5023
5536
  var finite = (v) => {
@@ -5535,6 +6048,11 @@ var PointerRouting = class {
5535
6048
  owns(id) {
5536
6049
  return this.gestures.has(id) || this.suppressed.has(id);
5537
6050
  }
6051
+ /** @internal Mouse bindings observe world ownership without claiming a second gesture. */
6052
+ worldOwns(id) {
6053
+ const gesture = this.gestures.get(id);
6054
+ return Boolean(gesture && !gesture.node && !gesture.target && !gesture.scroll && !gesture.candidates.length);
6055
+ }
5538
6056
  move(data) {
5539
6057
  this.validate(data);
5540
6058
  if (this.suppressed.has(data.pointerId)) return;
@@ -7082,7 +7600,7 @@ var ReplayPlayer = class {
7082
7600
  this.engine = engine;
7083
7601
  this.data = parseReplayData(data);
7084
7602
  this.bound = bindings(engine, options.controllers);
7085
- if (this.data.engineVersion !== "0.1.5" || engine.random.seed !== this.data.random.seed) throw new EngineError("REPLAY_INCOMPATIBLE", "Engine version or initial seed does not match Replay");
7603
+ if (this.data.engineVersion !== "0.1.6" || engine.random.seed !== this.data.random.seed) throw new EngineError("REPLAY_INCOMPATIBLE", "Engine version or initial seed does not match Replay");
7086
7604
  if (!compatible(this.bound.map(([id, c]) => ({
7087
7605
  id,
7088
7606
  actions: c.schema()
@@ -8980,6 +9498,6 @@ var CanvasUI = class {
8980
9498
  ], "Canvas UI cleanup failed");
8981
9499
  }
8982
9500
  };
8983
- export { Action, ActionController, ActionMovement, ActionSource, Actor, ActorState, Aspect, BodyType, Camera, CameraFollow, Cancellation, CanvasButton, CanvasCheckbox, CanvasCheckboxNode, CanvasColumn, CanvasHoldButton, CanvasInputNode, CanvasLabel, CanvasNode, CanvasNumberInput, CanvasNumberInputNode, CanvasPanel, CanvasRow, CanvasScroll, CanvasSelect, CanvasSelectNode, CanvasSlider, CanvasSliderNode, CanvasTextEditorNode, CanvasTextInput, CanvasTextInputNode, CanvasUI, Character, CharacterActor, Collider, CollisionBody, CollisionResponse, CollisionShape, CollisionWorld, Component, ComponentPhase, ContactEnter, ContactExit, ContactStay, DEFAULT_RANDOM_STREAM, ENGINE_VERSION, Easing, Engine, EngineError, EventBus, FixedStepRunner, GeometryTilemap, GeometryTilemapLayer, Grid, HexOrientation, InputSystem, KeyCode, KeyboardBindings, KeyboardSource, Lifetime, Motion, PointerCancel, PointerClick, PointerDown, PointerEnter, PointerLeave, PointerMove, PointerRouting, PointerUp, RANDOM_ALGORITHM, RandomManager, RandomStream, Renderable, ReplayPlayer, ReplayRecorder, Scene, ScenePointer, SceneState, Shape, SpriteAnimation, SpriteAnimationState, SpriteFit, SpriteSheet, TileContactEnter, TileContactExit, TileContactStay, TileGeometry, Tilemap, TilemapLayer, Transform, Tween, TweenBinding, TweenPlayback, TweenScheduler, VirtualButton, VirtualControl, VirtualInput, VirtualRegions, VirtualStick, VirtualStickMode, actorTween, canvasControlText, canvasHoverHighlight, canvasNumericRange, canvasNumericValue, canvasOpacity, canvasScrollbars, canvasSliderGeometry, canvasTween, cleanupAll, containsScreenPoint, containsUI, createViewTransform, defaultCanvasStyle, defineActions, defineComponent, defineEvent, deriveStreamSeed, imageSize, intersectUI, isKeyCode, layoutCanvas, nativeSession, parseKeyCode, parseReplayData, positive, screenPoint, screenToPhysical, screenToWorld, scrollbarContains, sliderTween, spriteFrame, spriteFrameBounds, tweenFinite, worldPoint, worldToScreen, wrapCanvasText };
9501
+ export { Action, ActionController, ActionMovement, ActionSource, Actor, ActorState, Aspect, BodyType, Camera, CameraFollow, Cancellation, CanvasButton, CanvasCheckbox, CanvasCheckboxNode, CanvasColumn, CanvasHoldButton, CanvasInputNode, CanvasLabel, CanvasNode, CanvasNumberInput, CanvasNumberInputNode, CanvasPanel, CanvasRow, CanvasScroll, CanvasSelect, CanvasSelectNode, CanvasSlider, CanvasSliderNode, CanvasTextEditorNode, CanvasTextInput, CanvasTextInputNode, CanvasUI, Character, CharacterActor, Collider, CollisionBody, CollisionResponse, CollisionShape, CollisionWorld, Component, ComponentPhase, ContactEnter, ContactExit, ContactStay, DEFAULT_RANDOM_STREAM, ENGINE_VERSION, Easing, Engine, EngineError, EventBus, FixedStepRunner, GamepadAxis, GamepadBindings, GamepadButton, GeometryTilemap, GeometryTilemapLayer, Grid, HexOrientation, InputCapture, InputCaptureState, InputDevice, InputSystem, KeyCode, KeyboardBindings, KeyboardSource, Lifetime, Motion, MouseButton, MouseSource, PointerCancel, PointerClick, PointerDown, PointerEnter, PointerLeave, PointerMove, PointerRouting, PointerUp, RANDOM_ALGORITHM, RandomManager, RandomStream, Renderable, ReplayPlayer, ReplayRecorder, Scene, ScenePointer, SceneState, Shape, SpriteAnimation, SpriteAnimationState, SpriteFit, SpriteSheet, TileContactEnter, TileContactExit, TileContactStay, TileGeometry, Tilemap, TilemapLayer, Transform, Tween, TweenBinding, TweenPlayback, TweenScheduler, VirtualButton, VirtualControl, VirtualInput, VirtualRegions, VirtualStick, VirtualStickMode, actorTween, canvasControlText, canvasHoverHighlight, canvasNumericRange, canvasNumericValue, canvasOpacity, canvasScrollbars, canvasSliderGeometry, canvasTween, checkedGamepadBindings, cleanupAll, containsScreenPoint, containsUI, createViewTransform, defaultCanvasStyle, defineActions, defineComponent, defineEvent, deriveStreamSeed, gamepadDeadZone, gamepadIndex, imageSize, intersectUI, isKeyCode, isMouseButton, layoutCanvas, nativeSession, parseKeyCode, parseMouseButton, parseReplayData, positive, screenPoint, screenToPhysical, screenToWorld, scrollbarContains, sliderTween, spriteFrame, spriteFrameBounds, tweenFinite, worldPoint, worldToScreen, wrapCanvasText };
8984
9502
 
8985
9503
  //# sourceMappingURL=index.js.map