incanto 0.72.0 → 0.74.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/bin/incanto-check.mjs +114 -42
- package/bin/incanto-verify.mjs +9 -2
- package/dist/2d.d.ts +29 -4
- package/dist/2d.js +3 -3
- package/dist/3d.d.ts +137 -15
- package/dist/3d.js +6 -6
- package/dist/{agent8-BqZ0_O1u.js → agent8-CHTT4unP.js} +1 -1
- package/dist/{audio-player-ioZ7VKC6.d.ts → audio-player-BOrk4eQx.d.ts} +1 -1
- package/dist/{behavior-Bf9P9oB2.d.ts → behavior-J1cmVah0.d.ts} +8 -0
- package/dist/{create-game-CmFAtLZp.js → create-game-BAiA-FjP.js} +7 -7
- package/dist/{create-game-C9rWqqAC.js → create-game-ssvGvP7W.js} +7 -7
- package/dist/debug.d.ts +1 -1
- package/dist/debug.js +1 -1
- package/dist/editor.js +965 -893
- package/dist/{environment-presets-BeHoaGhy.js → environment-presets-BkqlWewf.js} +277 -60
- package/dist/{gameplay-BlQe-M07.js → gameplay-Bxe1sMVT.js} +611 -39
- package/dist/gameplay.d.ts +141 -7
- package/dist/gameplay.js +2 -2
- package/dist/index.d.ts +6 -4
- package/dist/index.js +6 -6
- package/dist/{loader-CBfQzB6S.d.ts → loader-BAeWMYfF.d.ts} +1 -1
- package/dist/net.d.ts +2 -2
- package/dist/net.js +2 -2
- package/dist/{physics-2d-DrIgTlLk.js → physics-2d-C-SfvjzH.js} +94 -11
- package/dist/{physics-3d-BzOCw1VS.js → physics-3d-rOzeg890.js} +202 -15
- package/dist/{picking-C9McVJrO.js → picking-BMV34Pjl.js} +2 -2
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{register-5IYfooAN.js → register-CjociOtt.js} +19 -4
- package/dist/{register-C8HuRkOf.js → register-DRQyZKGU.js} +45 -9
- package/dist/{replay-datiAE-b.js → replay-B6BbgGM_.js} +42 -6
- package/dist/{replay-CBEChqXq.d.ts → replay-wZK5Ok5c.d.ts} +1 -1
- package/dist/{save-slots-CEuJPUle.js → save-slots-CEUGeLuB.js} +11 -2
- package/dist/{split-screen-Bm5DTElG.js → split-screen-BxZ5rBe7.js} +4 -4
- package/dist/{split-screen-DkqLng6q.d.ts → split-screen-D6sx8z3Z.d.ts} +2 -2
- package/dist/{sprite-animation-CqR2o3SA.js → sprite-animation-Bz3DHD_4.js} +1 -1
- package/dist/{src-DRSsRleD.js → src-CqtvYtSN.js} +1 -1
- package/dist/{test-Djuq3Vhq.js → test-CEl0y2Sw.js} +162 -30
- package/dist/test.d.ts +56 -5
- package/dist/test.js +2 -2
- package/dist/{touch-BnCyPA0G.js → touch-CioTZB-y.js} +1 -1
- package/dist/vite.js +3 -3
- package/editor/assets/{agent8-CMKz2cax.js → agent8-BlzturGi.js} +1 -1
- package/editor/assets/{debug-DLrQm-az.js → debug-BMUi8usj.js} +1 -1
- package/editor/assets/{index-BEKuxYcE.js → index-CrUCQoaB.js} +53 -53
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/schemas/scene.schema.json +634 -10
- package/skills/incanto-3d-character.md +113 -5
- package/skills/incanto-assets.md +9 -3
- package/skills/incanto-behaviors-and-scripts.md +26 -2
- package/skills/incanto-building-2d-games.md +9 -0
- package/skills/incanto-building-3d-games.md +45 -0
- package/skills/incanto-editor.md +6 -1
- package/skills/incanto-environment.md +7 -2
- package/skills/incanto-game-feel.md +16 -11
- package/skills/incanto-gameplay-behaviors.md +179 -16
- package/skills/incanto-node-reference.md +124 -45
- package/skills/incanto-physics-and-input.md +67 -5
- package/skills/incanto-scene-json-authoring.md +9 -3
- package/skills/incanto-verifying-your-game.md +19 -2
- package/templates-app/beacon-isle-3d/PROJECT/Status.md +8 -0
- package/templates-app/beacon-isle-3d/PROJECT/Structure.md +3 -0
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/beacon-isle-3d/src/game.scene.json +136 -0
- package/templates-app/beacon-isle-3d/verify.ts +144 -0
- package/templates-app/molehill-2d/package.json +1 -1
- package/templates-app/platformer-2d/PROJECT/Status.md +7 -0
- package/templates-app/platformer-2d/PROJECT/Structure.md +3 -0
- package/templates-app/platformer-2d/package.json +1 -1
- package/templates-app/platformer-2d/src/game.scene.json +126 -0
- package/templates-app/platformer-2d/verify.ts +54 -0
- package/templates-app/star-survivor/package.json +1 -1
- package/templates-app/tps-3d/PROJECT/Context.md +1 -1
- package/templates-app/tps-3d/PROJECT/Status.md +40 -1
- package/templates-app/tps-3d/PROJECT/Structure.md +19 -0
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/tps-3d/src/behaviors.ts +9 -0
- package/templates-app/tps-3d/src/cover.scene.json +26 -0
- package/templates-app/tps-3d/src/game.scene.json +331 -75
- package/templates-app/tps-3d/src/main.ts +14 -0
- package/templates-app/tps-3d/src/pillar.scene.json +25 -0
- package/templates-app/tps-3d/verify.ts +582 -3
- package/templates-app/village-quest-3d/package.json +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { t as jsonClone } from "./json-CfTjpvW8.js";
|
|
2
|
-
import { B as HudLayer, J as Node, K as requireNumber, P as effectiveOrder, Y as diagnose, d as rebuildOptions, et as Behavior, f as rememberAuthoredPath, h as loadScene, n as SaveSlots, ot as registerBehavior, s as duplicateNode } from "./save-slots-
|
|
2
|
+
import { B as HudLayer, J as Node, K as requireNumber, P as effectiveOrder, Y as diagnose, d as rebuildOptions, et as Behavior, f as rememberAuthoredPath, h as loadScene, n as SaveSlots, ot as registerBehavior, s as duplicateNode } from "./save-slots-CEUGeLuB.js";
|
|
3
3
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
4
4
|
import { t as Rng } from "./rng-CDOMybym.js";
|
|
5
|
-
import { h as didYouMean } from "./touch-
|
|
5
|
+
import { h as didYouMean } from "./touch-CioTZB-y.js";
|
|
6
6
|
import { Color, CubeCamera, DepthTexture, DoubleSide, Euler, FloatType, Frustum, HalfFloatType, LinearMipmapLinearFilter, MathUtils, Matrix4, Mesh, MeshDepthMaterial, Object3D, PerspectiveCamera, Plane, PlaneGeometry, Quaternion, ShaderMaterial, Sphere, Vector2, Vector3, Vector4, WebGLCubeRenderTarget, WebGLRenderTarget } from "three";
|
|
7
7
|
//#region src/gameplay/spatial.ts
|
|
8
8
|
/** Duck-type test: every spatial node (Node2D/Node3D) exposes `position: number[]`. */
|
|
@@ -2433,7 +2433,14 @@ var Node3D = class Node3D extends Node {
|
|
|
2433
2433
|
"overlay"
|
|
2434
2434
|
]
|
|
2435
2435
|
},
|
|
2436
|
-
snapToGround: {
|
|
2436
|
+
snapToGround: {
|
|
2437
|
+
default: null,
|
|
2438
|
+
kinds: [
|
|
2439
|
+
"null",
|
|
2440
|
+
"boolean",
|
|
2441
|
+
"number"
|
|
2442
|
+
]
|
|
2443
|
+
}
|
|
2437
2444
|
};
|
|
2438
2445
|
position = [
|
|
2439
2446
|
0,
|
|
@@ -2698,9 +2705,27 @@ var PhysicsBody3D = class extends Node3D {
|
|
|
2698
2705
|
/** @internal Set by Physics3D when the body is created. */
|
|
2699
2706
|
_physics = null;
|
|
2700
2707
|
};
|
|
2701
|
-
/**
|
|
2708
|
+
/**
|
|
2709
|
+
* Immovable collider (ground, walls, platforms).
|
|
2710
|
+
*
|
|
2711
|
+
* It also carries the SURFACE it is: `friction` and `restitution` used to
|
|
2712
|
+
* live on `RigidBody3D` alone, so a trampoline, an ice patch or a sticky
|
|
2713
|
+
* landing could not be written in scene JSON, and Rapier's AVERAGE rule
|
|
2714
|
+
* halved whatever a ball brought to a plain floor. A static body's authored
|
|
2715
|
+
* value WINS the pair — restitution above 0 combines by Max, friction below
|
|
2716
|
+
* the default by Min and above it by Max — so the pad decides, whatever lands
|
|
2717
|
+
* on it. Left at the defaults, nothing changes for any existing scene.
|
|
2718
|
+
*/
|
|
2702
2719
|
var StaticBody3D = class extends PhysicsBody3D {
|
|
2703
2720
|
static typeName = "StaticBody3D";
|
|
2721
|
+
static props = {
|
|
2722
|
+
friction: { default: .5 },
|
|
2723
|
+
restitution: { default: 0 }
|
|
2724
|
+
};
|
|
2725
|
+
/** 0.5 plain · 0 ice (slides on and on) · 1+ glue. Away from 0.5 it wins the pair. */
|
|
2726
|
+
friction = .5;
|
|
2727
|
+
/** 0 dead · 1 trampoline. Above 0 it wins the pair: a dead ball still bounces. */
|
|
2728
|
+
restitution = 0;
|
|
2704
2729
|
};
|
|
2705
2730
|
/** Sensor volume emitting `triggerEnter(other)` / `triggerExit(other)`. */
|
|
2706
2731
|
var Area3D = class extends PhysicsBody3D {
|
|
@@ -2754,13 +2779,32 @@ var RigidBody3D = class extends PhysicsBody3D {
|
|
|
2754
2779
|
0,
|
|
2755
2780
|
0,
|
|
2756
2781
|
0
|
|
2757
|
-
] }
|
|
2782
|
+
] },
|
|
2783
|
+
linearDamping: {
|
|
2784
|
+
default: 0,
|
|
2785
|
+
range: { min: 0 }
|
|
2786
|
+
},
|
|
2787
|
+
angularDamping: {
|
|
2788
|
+
default: 0,
|
|
2789
|
+
range: { min: 0 }
|
|
2790
|
+
}
|
|
2758
2791
|
};
|
|
2759
2792
|
mass = 1;
|
|
2760
2793
|
gravityScale = 1;
|
|
2761
2794
|
fixedRotation = false;
|
|
2762
2795
|
friction = .5;
|
|
2763
2796
|
restitution = 0;
|
|
2797
|
+
/**
|
|
2798
|
+
* Drag, per second: velocity decays by `e^(-linearDamping·t)`. `0` = none.
|
|
2799
|
+
* A body flying through nothing keeps its speed forever otherwise.
|
|
2800
|
+
*/
|
|
2801
|
+
linearDamping = 0;
|
|
2802
|
+
/**
|
|
2803
|
+
* Rolling resistance, per second, on the spin. Rapier has none by itself:
|
|
2804
|
+
* friction only converts slip into spin, so a rolling ball is a wheel and a
|
|
2805
|
+
* golf ball rolled to the edge of the world. `1`–`3` stops a ball on a green.
|
|
2806
|
+
*/
|
|
2807
|
+
angularDamping = 0;
|
|
2764
2808
|
/** m/s, y-up. Read back every step; write to launch. */
|
|
2765
2809
|
linearVelocity = [
|
|
2766
2810
|
0,
|
|
@@ -2815,7 +2859,8 @@ var CharacterBody3D = class extends PhysicsBody3D {
|
|
|
2815
2859
|
stepHeight: {
|
|
2816
2860
|
default: .35,
|
|
2817
2861
|
range: { min: 0 }
|
|
2818
|
-
}
|
|
2862
|
+
},
|
|
2863
|
+
collideWithCharacters: { default: true }
|
|
2819
2864
|
};
|
|
2820
2865
|
/** m/s, y-up (up = +y). */
|
|
2821
2866
|
velocity = [
|
|
@@ -2837,6 +2882,16 @@ var CharacterBody3D = class extends PhysicsBody3D {
|
|
|
2837
2882
|
/** Max climbable slope angle. */
|
|
2838
2883
|
slopeLimitDeg = 45;
|
|
2839
2884
|
/**
|
|
2885
|
+
* Whether OTHER characters are obstacles to this one. On (default), a
|
|
2886
|
+
* character is a wall to every other character — Godot's answer, and what a
|
|
2887
|
+
* few enemies want. Off, the controller ignores every other kinematic body
|
|
2888
|
+
* and a crowd costs what the floor and the walls cost: eighty chasers around
|
|
2889
|
+
* a player cost 64–286 ms a step with this on (each slide shape-casts
|
|
2890
|
+
* against every neighbour, O(n²) in the crowd) and under a millisecond off.
|
|
2891
|
+
* A survivor's horde overlaps itself and nobody minds.
|
|
2892
|
+
*/
|
|
2893
|
+
collideWithCharacters = true;
|
|
2894
|
+
/**
|
|
2840
2895
|
* How high a step this body walks UP without jumping (0 = none).
|
|
2841
2896
|
*
|
|
2842
2897
|
* Rapier's character controller does not autostep unless asked, and nothing
|
|
@@ -3321,7 +3376,10 @@ var Water3D = class Water3D extends Node3D {
|
|
|
3321
3376
|
mirrorInterval: { default: 33 },
|
|
3322
3377
|
swellSteepness: { default: .35 },
|
|
3323
3378
|
foam: { default: true },
|
|
3324
|
-
reflectivity: {
|
|
3379
|
+
reflectivity: {
|
|
3380
|
+
default: null,
|
|
3381
|
+
kinds: ["null", "number"]
|
|
3382
|
+
},
|
|
3325
3383
|
interaction: { default: true },
|
|
3326
3384
|
splash: { default: true },
|
|
3327
3385
|
sunDirection: { default: [
|
|
@@ -4739,11 +4797,23 @@ function moveBody(node, to, dt) {
|
|
|
4739
4797
|
*/
|
|
4740
4798
|
var Chase = class extends Behavior {
|
|
4741
4799
|
static props = {
|
|
4742
|
-
/** NodePath of the node to chase (resolved from THIS behavior's node). */
|
|
4800
|
+
/** NodePath of the node to chase (resolved from THIS behavior's node). Optional beside `targetGroup`. */
|
|
4743
4801
|
target: {
|
|
4744
4802
|
default: "",
|
|
4745
|
-
nodePath: true
|
|
4746
|
-
|
|
4803
|
+
nodePath: true
|
|
4804
|
+
},
|
|
4805
|
+
/**
|
|
4806
|
+
* The other way to name a target: the NEAREST member of this group, re-picked
|
|
4807
|
+
* every `retargetEvery` seconds. `target: "%Player"` was the whole vocabulary,
|
|
4808
|
+
* written for games with one hero — a guard in an RTS, a co-op, a survival
|
|
4809
|
+
* game with allies could not chase "whichever of the six is closest".
|
|
4810
|
+
* With no member in reach the chaser waits.
|
|
4811
|
+
*/
|
|
4812
|
+
targetGroup: { default: "" },
|
|
4813
|
+
/** Seconds between nearest-of-group picks (`0` = every frame). */
|
|
4814
|
+
retargetEvery: {
|
|
4815
|
+
default: .25,
|
|
4816
|
+
range: { min: 0 }
|
|
4747
4817
|
},
|
|
4748
4818
|
/** Units per second toward the target. */
|
|
4749
4819
|
speed: { default: 60 },
|
|
@@ -4753,20 +4823,46 @@ var Chase = class extends Behavior {
|
|
|
4753
4823
|
loseRange: { default: 0 },
|
|
4754
4824
|
/** Move the parent node instead of this one (AI-on-a-child pattern). */
|
|
4755
4825
|
moveParent: { default: false },
|
|
4826
|
+
/**
|
|
4827
|
+
* Chase along the GROUND — the target's height is not a direction.
|
|
4828
|
+
*
|
|
4829
|
+
* `null` (default) decides from the mover: a `CharacterBody3D` walks (the
|
|
4830
|
+
* KCC's `slopeLimitDeg` and `stepHeight` say what it climbs, `stickToGround`
|
|
4831
|
+
* keeps it on the surface), anything else — a 2D body, a plain node, a
|
|
4832
|
+
* rigid body — chases in every axis. `true`/`false` overrides either way:
|
|
4833
|
+
* a flying enemy on a kinematic capsule says `false`.
|
|
4834
|
+
*
|
|
4835
|
+
* Before this a chaser moved straight at `target.position` in all three
|
|
4836
|
+
* axes, so a player on a ledge was met by enemies pushing diagonally up
|
|
4837
|
+
* whatever face was in the way — a 70° cliff included — and the slope
|
|
4838
|
+
* limit never got a say.
|
|
4839
|
+
*/
|
|
4840
|
+
ground: {
|
|
4841
|
+
default: null,
|
|
4842
|
+
kinds: ["null", "boolean"]
|
|
4843
|
+
},
|
|
4756
4844
|
...FACING_PROPS
|
|
4757
4845
|
};
|
|
4758
4846
|
static signals = ["reachedTarget", "lostTarget"];
|
|
4759
4847
|
target = "";
|
|
4848
|
+
targetGroup = "";
|
|
4849
|
+
retargetEvery = .25;
|
|
4760
4850
|
speed = 60;
|
|
4851
|
+
/** The group member currently chased (`targetGroup`), or null. */
|
|
4852
|
+
picked = null;
|
|
4853
|
+
sincePick = Number.POSITIVE_INFINITY;
|
|
4761
4854
|
stopRange = 0;
|
|
4762
4855
|
loseRange = 0;
|
|
4763
4856
|
moveParent = false;
|
|
4857
|
+
ground = null;
|
|
4764
4858
|
facePath = "";
|
|
4859
|
+
/** Signed fall speed accumulated while a ground chaser is off the floor. */
|
|
4860
|
+
fall = 0;
|
|
4765
4861
|
turnSpeed = 100;
|
|
4766
4862
|
inRange = false;
|
|
4767
4863
|
lost = false;
|
|
4768
4864
|
onReady() {
|
|
4769
|
-
if (this.target === "") throw new IncantoError("PROP_TYPE_MISMATCH", `Chase on '${this.node.getPath()}': "target" (a node path) is required.`, { prop: "target" });
|
|
4865
|
+
if (this.target === "" && this.targetGroup === "") throw new IncantoError("PROP_TYPE_MISMATCH", `Chase on '${this.node.getPath()}': "target" (a node path) or "targetGroup" (a group name) is required.`, { prop: "target" });
|
|
4770
4866
|
if (this.moveParent) {
|
|
4771
4867
|
const parent = this.node.parent;
|
|
4772
4868
|
if (!parent || !hasPosition$1(parent)) throw new IncantoError("PROP_TYPE_MISMATCH", `Chase on '${this.node.getPath()}': "moveParent" needs a spatial parent (a Node2D/Node3D) to move. '${this.node.name}' has none.`, {
|
|
@@ -4775,12 +4871,66 @@ var Chase = class extends Behavior {
|
|
|
4775
4871
|
});
|
|
4776
4872
|
} else requirePosition(this);
|
|
4777
4873
|
}
|
|
4874
|
+
/**
|
|
4875
|
+
* A walker falls off what it walks off. `CharacterBody3D` applies no gravity
|
|
4876
|
+
* of its own ("integrate velocity yourself"), and a chaser that ignores the
|
|
4877
|
+
* target's height would otherwise walk off a ledge and keep its altitude —
|
|
4878
|
+
* measured on tps-3d's 0.6 m Ramp, where the old chase glided down over five
|
|
4879
|
+
* seconds only because the player happened to stand lower. Scene gravity
|
|
4880
|
+
* (default 9.81), accumulated while `isOnFloor()` says no, reset when it says
|
|
4881
|
+
* yes; the KCC's snap-to-ground handles the last 10 cm.
|
|
4882
|
+
*/
|
|
4883
|
+
fallStep(mover, dt) {
|
|
4884
|
+
if (mover.isOnFloor?.() ?? true) {
|
|
4885
|
+
this.fall = 0;
|
|
4886
|
+
return 0;
|
|
4887
|
+
}
|
|
4888
|
+
const g = (this.node.tree?.engine?.scene?.physics?.gravity)?.[1] ?? (mover.position.length === 2 ? 980 : -9.81);
|
|
4889
|
+
this.fall += g * dt;
|
|
4890
|
+
return this.fall * dt;
|
|
4891
|
+
}
|
|
4892
|
+
/** Off the floor with nowhere to chase: still fall. */
|
|
4893
|
+
dropIfAirborne(mover, dt) {
|
|
4894
|
+
const drop = this.fallStep(mover, dt);
|
|
4895
|
+
if (drop !== 0) {
|
|
4896
|
+
const to = [...mover.position];
|
|
4897
|
+
to[1] = (to[1] ?? 0) + drop;
|
|
4898
|
+
moveBody(mover, to, dt);
|
|
4899
|
+
}
|
|
4900
|
+
}
|
|
4901
|
+
/** The nearest live member of `targetGroup` — re-picked every `retargetEvery` seconds, never the mover itself. */
|
|
4902
|
+
nearestOfGroup(mover, dt) {
|
|
4903
|
+
this.sincePick += dt;
|
|
4904
|
+
if (this.picked && this.picked.tree !== null && this.sincePick < this.retargetEvery) return this.picked;
|
|
4905
|
+
this.sincePick = 0;
|
|
4906
|
+
const here = worldPosition(mover);
|
|
4907
|
+
let best = null;
|
|
4908
|
+
let bestD = Number.POSITIVE_INFINITY;
|
|
4909
|
+
for (const n of this.node.tree?.getNodesInGroup(this.targetGroup) ?? []) {
|
|
4910
|
+
if (n === mover || n === this.node || !hasPosition$1(n)) continue;
|
|
4911
|
+
const d = distance$1(here, worldPosition(n));
|
|
4912
|
+
if (d < bestD) {
|
|
4913
|
+
best = n;
|
|
4914
|
+
bestD = d;
|
|
4915
|
+
}
|
|
4916
|
+
}
|
|
4917
|
+
this.picked = best;
|
|
4918
|
+
return best;
|
|
4919
|
+
}
|
|
4920
|
+
/** See `ground`: an explicit answer, else "is the mover a 3D kinematic capsule". */
|
|
4921
|
+
walksTheGround(mover) {
|
|
4922
|
+
if (this.ground !== null) return this.ground;
|
|
4923
|
+
const kcc = mover;
|
|
4924
|
+
return mover.position.length === 3 && typeof kcc.slopeLimitDeg === "number";
|
|
4925
|
+
}
|
|
4778
4926
|
update(dt) {
|
|
4779
4927
|
const mover = this.moveParent ? this.node.parent : this.node;
|
|
4780
4928
|
if (!mover || !hasPosition$1(mover)) return;
|
|
4781
|
-
const target = this.node.getNodeOrNull(this.target);
|
|
4929
|
+
const target = this.targetGroup !== "" ? this.nearestOfGroup(mover, dt) : this.node.getNodeOrNull(this.target);
|
|
4782
4930
|
if (!target || !hasPosition$1(target)) return;
|
|
4783
4931
|
const targetPos = localFromWorld(mover, worldPosition(target));
|
|
4932
|
+
const walks = this.walksTheGround(mover);
|
|
4933
|
+
if (walks) targetPos[1] = mover.position[1] ?? 0;
|
|
4784
4934
|
const d = distance$1(mover.position, targetPos);
|
|
4785
4935
|
if (this.loseRange > 0 && d > this.loseRange) {
|
|
4786
4936
|
if (!this.lost) {
|
|
@@ -4788,6 +4938,7 @@ var Chase = class extends Behavior {
|
|
|
4788
4938
|
this.inRange = false;
|
|
4789
4939
|
this.emit("lostTarget");
|
|
4790
4940
|
}
|
|
4941
|
+
if (walks) this.dropIfAirborne(mover, dt);
|
|
4791
4942
|
return;
|
|
4792
4943
|
}
|
|
4793
4944
|
this.lost = false;
|
|
@@ -4796,12 +4947,14 @@ var Chase = class extends Behavior {
|
|
|
4796
4947
|
this.inRange = true;
|
|
4797
4948
|
this.emit("reachedTarget");
|
|
4798
4949
|
}
|
|
4950
|
+
if (walks) this.dropIfAirborne(mover, dt);
|
|
4799
4951
|
return;
|
|
4800
4952
|
}
|
|
4801
4953
|
this.inRange = false;
|
|
4802
4954
|
const step = Math.min(this.speed * dt, this.stopRange > 0 ? d - this.stopRange : d);
|
|
4803
4955
|
const from = [...mover.position];
|
|
4804
4956
|
const { position } = moveToward(mover.position, targetPos, Math.max(0, step));
|
|
4957
|
+
if (walks) position[1] = (position[1] ?? 0) + this.fallStep(mover, dt);
|
|
4805
4958
|
moveBody(mover, position, dt);
|
|
4806
4959
|
if (this.facePath !== "") {
|
|
4807
4960
|
const skin = this.node.getNodeOrNull(this.facePath);
|
|
@@ -4810,6 +4963,59 @@ var Chase = class extends Behavior {
|
|
|
4810
4963
|
}
|
|
4811
4964
|
};
|
|
4812
4965
|
//#endregion
|
|
4966
|
+
//#region src/gameplay/checkpoint.ts
|
|
4967
|
+
/**
|
|
4968
|
+
* A checkpoint: touch it and a `Respawn` puts you back HERE from now on.
|
|
4969
|
+
*
|
|
4970
|
+
* Sits on an `Area2D`/`Area3D` (listens to `triggerEnter`). `respawn` names the
|
|
4971
|
+
* `Respawn` node whose `to` becomes this checkpoint's world position, less
|
|
4972
|
+
* `dropBelow` so the character lands ON the flag rather than inside it.
|
|
4973
|
+
* `group` gates who can light it; `once` (default) lights it a single time.
|
|
4974
|
+
* Emits `activated(other)`.
|
|
4975
|
+
*
|
|
4976
|
+
* Built after a platformer built from the tarball wrote exactly this in nine
|
|
4977
|
+
* lines — the ninth being the one that found `Respawn.to` was read once at
|
|
4978
|
+
* ready. A checkpoint is the second most common thing a platformer has, and it
|
|
4979
|
+
* should be one node with two props, not a behaviour every game rewrites.
|
|
4980
|
+
*/
|
|
4981
|
+
var Checkpoint = class extends Behavior {
|
|
4982
|
+
static props = {
|
|
4983
|
+
/** Node path of the `Respawn` to redirect. */
|
|
4984
|
+
respawn: {
|
|
4985
|
+
default: "",
|
|
4986
|
+
nodePath: true,
|
|
4987
|
+
required: true
|
|
4988
|
+
},
|
|
4989
|
+
/** Only nodes in this group light it ('' = anything). */
|
|
4990
|
+
group: { default: "player" },
|
|
4991
|
+
/** Light it once; false re-fires `activated` on every entry. */
|
|
4992
|
+
once: { default: true },
|
|
4993
|
+
/** How far below the checkpoint's origin the respawn point sits. */
|
|
4994
|
+
dropBelow: { default: 0 }
|
|
4995
|
+
};
|
|
4996
|
+
static signals = ["activated"];
|
|
4997
|
+
respawn = "";
|
|
4998
|
+
group = "player";
|
|
4999
|
+
once = true;
|
|
5000
|
+
dropBelow = 0;
|
|
5001
|
+
/** True after the first activation. */
|
|
5002
|
+
lit = false;
|
|
5003
|
+
onReady() {
|
|
5004
|
+
this.node.on("triggerEnter", (other) => {
|
|
5005
|
+
const who = other;
|
|
5006
|
+
if (this.group !== "" && !who.isInGroup(this.group)) return;
|
|
5007
|
+
if (this.once && this.lit) return;
|
|
5008
|
+
const catcher = this.node.getNodeOrNull(this.respawn);
|
|
5009
|
+
if (!catcher || !Array.isArray(catcher.to)) return;
|
|
5010
|
+
const to = [...worldPosition(this.node)];
|
|
5011
|
+
if (to.length >= 2) to[1] = (to[1] ?? 0) - this.dropBelow;
|
|
5012
|
+
catcher.to = to;
|
|
5013
|
+
this.lit = true;
|
|
5014
|
+
this.emit("activated", who);
|
|
5015
|
+
});
|
|
5016
|
+
}
|
|
5017
|
+
};
|
|
5018
|
+
//#endregion
|
|
4813
5019
|
//#region src/gameplay/clickable.ts
|
|
4814
5020
|
/**
|
|
4815
5021
|
* Click and hover THIS node — the mouse as gameplay input.
|
|
@@ -5204,6 +5410,17 @@ var Health = class extends Behavior {
|
|
|
5204
5410
|
if (this.invulnerableFor > 0) this.invulnTimer = this.invulnerableFor;
|
|
5205
5411
|
if (this.current <= 0) this.die();
|
|
5206
5412
|
}
|
|
5413
|
+
/**
|
|
5414
|
+
* Be immune for `seconds` from NOW — a dodge roll, a spawn, a cutscene.
|
|
5415
|
+
*
|
|
5416
|
+
* `invulnerableFor` grants i-frames only AFTER a hit; this is the window
|
|
5417
|
+
* before one. It extends a window already open and never shortens it, so a
|
|
5418
|
+
* second dodge mid-window cannot expose you. Dead or not, it is harmless.
|
|
5419
|
+
*/
|
|
5420
|
+
protect(seconds) {
|
|
5421
|
+
if (requireNumber(seconds, this.node, "Health.protect") < 0) throw new IncantoError("PROP_TYPE_MISMATCH", `Health.protect on '${this.node.getPath()}': seconds must be >= 0, got ${seconds}.`);
|
|
5422
|
+
this.invulnTimer = Math.max(this.invulnTimer, seconds);
|
|
5423
|
+
}
|
|
5207
5424
|
/** Restore `n` hit points (clamped to `max`). No-op when dead. Emits `healed`. */
|
|
5208
5425
|
heal(n) {
|
|
5209
5426
|
if (this.dead || requireNumber(n, this.node, "Health.heal") <= 0) return;
|
|
@@ -5351,8 +5568,10 @@ function onTriggerEnter(behavior, fn) {
|
|
|
5351
5568
|
* whose Health-owner node is in that group takes damage. This is how you stop
|
|
5352
5569
|
* enemies killing each other — give the player's weapon `targetGroup:'enemy'`
|
|
5353
5570
|
* and each enemy's contact hitbox `targetGroup:'player'`.
|
|
5354
|
-
* - `oncePerTarget` (default) prevents re-hitting the same node
|
|
5355
|
-
* hazard would otherwise drain a resting body every
|
|
5571
|
+
* - `oncePerTarget` (default) prevents re-hitting the same node WHILE it stays
|
|
5572
|
+
* in contact (a stationary hazard would otherwise drain a resting body every
|
|
5573
|
+
* frame). It is per overlap, not per lifetime: leave and come back, or disarm
|
|
5574
|
+
* the hitbox and arm it again over the same foe, and the hit lands again.
|
|
5356
5575
|
* - `repeatEvery` (seconds, 0 = off) re-damages targets that STAY overlapped —
|
|
5357
5576
|
* lava pools, poison clouds, an enemy standing on you. Contact events fire
|
|
5358
5577
|
* only on entry/exit; this is the "and it keeps hurting" knob.
|
|
@@ -5396,6 +5615,7 @@ var DamageOnContact = class extends Behavior {
|
|
|
5396
5615
|
});
|
|
5397
5616
|
this.node.on("triggerExit", (other) => {
|
|
5398
5617
|
this.inside.delete(other);
|
|
5618
|
+
this.hit.delete(other);
|
|
5399
5619
|
});
|
|
5400
5620
|
}
|
|
5401
5621
|
update(dt) {
|
|
@@ -5517,15 +5737,15 @@ var DayNight = class extends Behavior {
|
|
|
5517
5737
|
}
|
|
5518
5738
|
update(dt) {
|
|
5519
5739
|
if (!this.paused) this.hour = (this.hour + dt / this.daySeconds * 24) % 24;
|
|
5520
|
-
this.applyClock -= dt;
|
|
5521
|
-
if (this.applyClock > 0) return;
|
|
5522
|
-
this.applyClock = .2;
|
|
5523
|
-
this.applyHour();
|
|
5524
5740
|
const next = phaseOf(this.hour);
|
|
5525
5741
|
if (next !== this.phase) {
|
|
5526
5742
|
this.phase = next;
|
|
5527
5743
|
this.emit("dayPhaseChanged", next);
|
|
5528
5744
|
}
|
|
5745
|
+
this.applyClock -= dt;
|
|
5746
|
+
if (this.applyClock > 0) return;
|
|
5747
|
+
this.applyClock = .2;
|
|
5748
|
+
this.applyHour();
|
|
5529
5749
|
}
|
|
5530
5750
|
applyHour() {
|
|
5531
5751
|
const engine = this.node.tree?.engine;
|
|
@@ -6161,13 +6381,15 @@ var GameFlow = class GameFlow extends Behavior {
|
|
|
6161
6381
|
this.engine.timeScale = 1;
|
|
6162
6382
|
}
|
|
6163
6383
|
update() {
|
|
6384
|
+
const pausePressed = this.readAction("pauseAction", "The pause key will do nothing.");
|
|
6385
|
+
const restartPressed = this.readAction("restartAction", "The restart key will do nothing.");
|
|
6164
6386
|
if (this.state === "playing" || this.state === "paused") {
|
|
6165
|
-
if (
|
|
6387
|
+
if (pausePressed) if (this.state === "playing") this.pause();
|
|
6166
6388
|
else this.resume();
|
|
6167
6389
|
return;
|
|
6168
6390
|
}
|
|
6169
6391
|
if (this.state !== "gameover" && this.state !== "won") return;
|
|
6170
|
-
if (
|
|
6392
|
+
if (restartPressed) this.restart();
|
|
6171
6393
|
}
|
|
6172
6394
|
};
|
|
6173
6395
|
/** The `HudLayer` a widget lives on, or null. */
|
|
@@ -6209,24 +6431,64 @@ var Interactable = class extends Behavior {
|
|
|
6209
6431
|
/** Maximum distance an actor may be to interact. */
|
|
6210
6432
|
range: { default: 2 },
|
|
6211
6433
|
/** Only nodes in this group can interact. */
|
|
6212
|
-
actorGroup: { default: "player" }
|
|
6434
|
+
actorGroup: { default: "player" },
|
|
6435
|
+
/** What the actor sees on it — `Open drawer`. Read by whoever draws the "[E] …" prompt. */
|
|
6436
|
+
prompt: { default: "" },
|
|
6437
|
+
/**
|
|
6438
|
+
* Which Interactable answers a press when several are in range. `any`:
|
|
6439
|
+
* every one of them (the old rule). `nearest`: only the closest to the
|
|
6440
|
+
* actor. `facing`: of those inside `coneDeg` of the current camera's
|
|
6441
|
+
* forward, the one nearest the CENTRE of the view — a crosshair — so a
|
|
6442
|
+
* note on the desk and the drawer under it are two different looks, and
|
|
6443
|
+
* in third person the plot the farmer looks at beats the plot under the
|
|
6444
|
+
* farmer's feet (with no camera, the same as `nearest`).
|
|
6445
|
+
*/
|
|
6446
|
+
focus: {
|
|
6447
|
+
default: "any",
|
|
6448
|
+
options: [
|
|
6449
|
+
"any",
|
|
6450
|
+
"nearest",
|
|
6451
|
+
"facing"
|
|
6452
|
+
]
|
|
6453
|
+
},
|
|
6454
|
+
/** Full width of the `facing` cone, degrees — a generous crosshair. */
|
|
6455
|
+
coneDeg: {
|
|
6456
|
+
default: 40,
|
|
6457
|
+
range: {
|
|
6458
|
+
min: 1,
|
|
6459
|
+
max: 360
|
|
6460
|
+
}
|
|
6461
|
+
}
|
|
6213
6462
|
};
|
|
6214
|
-
static signals = [
|
|
6463
|
+
static signals = [
|
|
6464
|
+
"interacted",
|
|
6465
|
+
"focused",
|
|
6466
|
+
"unfocused"
|
|
6467
|
+
];
|
|
6215
6468
|
action = "interact";
|
|
6216
6469
|
range = 2;
|
|
6217
6470
|
actorGroup = "player";
|
|
6471
|
+
prompt = "";
|
|
6472
|
+
focus = "any";
|
|
6473
|
+
coneDeg = 40;
|
|
6474
|
+
/**
|
|
6475
|
+
* The actor this Interactable would answer RIGHT NOW, or null — updated every
|
|
6476
|
+
* frame, with `focused(actor)` / `unfocused(actor)` on the edges. A HUD reads
|
|
6477
|
+
* the focused one's `prompt`.
|
|
6478
|
+
*/
|
|
6479
|
+
focusedBy = null;
|
|
6218
6480
|
/** Actions already reported missing, so a per-frame read says it once. */
|
|
6219
6481
|
reportedActions = /* @__PURE__ */ new Set();
|
|
6220
6482
|
update() {
|
|
6221
6483
|
const engine = this.node.tree?.engine;
|
|
6222
6484
|
if (!engine?.scene) return;
|
|
6223
|
-
|
|
6485
|
+
const pressed = tolerateUnknownAction({
|
|
6224
6486
|
engine,
|
|
6225
6487
|
who: this.node.getPath(),
|
|
6226
6488
|
reported: this.reportedActions,
|
|
6227
6489
|
consequence: "nothing here can be interacted with",
|
|
6228
6490
|
authored: this.action !== "interact"
|
|
6229
|
-
}, this.action, () => engine.input.justPressed(this.action), false)
|
|
6491
|
+
}, this.action, () => engine.input.justPressed(this.action), false);
|
|
6230
6492
|
if (!hasPosition(this.node)) return;
|
|
6231
6493
|
const self = worldPosition(this.node);
|
|
6232
6494
|
let nearest = null;
|
|
@@ -6240,9 +6502,92 @@ var Interactable = class extends Behavior {
|
|
|
6240
6502
|
nearestDist = d;
|
|
6241
6503
|
}
|
|
6242
6504
|
}
|
|
6243
|
-
|
|
6505
|
+
let holder = nearest;
|
|
6506
|
+
if (holder && this.focus !== "any" && !this.claims(holder, self)) holder = null;
|
|
6507
|
+
if (holder !== this.focusedBy) {
|
|
6508
|
+
const was = this.focusedBy;
|
|
6509
|
+
this.focusedBy = holder;
|
|
6510
|
+
if (was) this.emit("unfocused", was);
|
|
6511
|
+
if (holder) this.emit("focused", holder);
|
|
6512
|
+
}
|
|
6513
|
+
if (!pressed || !holder) return;
|
|
6514
|
+
this.emit("interacted", holder);
|
|
6515
|
+
}
|
|
6516
|
+
/** Is THIS the Interactable `actor` means — the nearest (that it faces) of every one in reach? */
|
|
6517
|
+
claims(actor, self) {
|
|
6518
|
+
const eye = this.focus === "facing" ? cameraEye(this.node.getRoot()) : null;
|
|
6519
|
+
const from = eye ? eye.at : worldPosition(actor);
|
|
6520
|
+
const centred = (target) => {
|
|
6521
|
+
if (!eye) return 1;
|
|
6522
|
+
const dx = (target[0] ?? 0) - (from[0] ?? 0);
|
|
6523
|
+
const dy = (target[1] ?? 0) - (from[1] ?? 0);
|
|
6524
|
+
const dz = (target[2] ?? 0) - (from[2] ?? 0);
|
|
6525
|
+
const len = Math.hypot(dx, dy, dz);
|
|
6526
|
+
if (len < 1e-6) return 1;
|
|
6527
|
+
return dx / len * eye.fx + dy / len * eye.fy + dz / len * eye.fz;
|
|
6528
|
+
};
|
|
6529
|
+
const edge = Math.cos(this.coneDeg / 2 * (Math.PI / 180));
|
|
6530
|
+
const mineCentred = centred(self);
|
|
6531
|
+
if (mineCentred < edge) return false;
|
|
6532
|
+
const mine = eye ? -mineCentred : distance(from, self);
|
|
6533
|
+
for (const other of interactablesIn(this.node.getRoot())) {
|
|
6534
|
+
if (other === this || other.focus === "any" || other.actorGroup !== this.actorGroup) continue;
|
|
6535
|
+
if (!hasPosition(other.node)) continue;
|
|
6536
|
+
const there = worldPosition(other.node);
|
|
6537
|
+
if (distance(worldPosition(actor), there) > other.range) continue;
|
|
6538
|
+
const theirsCentred = centred(there);
|
|
6539
|
+
if (eye && theirsCentred < edge) continue;
|
|
6540
|
+
if ((eye ? -theirsCentred : distance(from, there)) < mine) return false;
|
|
6541
|
+
}
|
|
6542
|
+
return true;
|
|
6244
6543
|
}
|
|
6245
6544
|
};
|
|
6545
|
+
/** Every Interactable in the tree — walked on demand, only while an actor is in reach of one. */
|
|
6546
|
+
function interactablesIn(root) {
|
|
6547
|
+
const out = [];
|
|
6548
|
+
const walk = (n) => {
|
|
6549
|
+
if (n.behavior instanceof Interactable) out.push(n.behavior);
|
|
6550
|
+
for (const c of n.children) walk(c);
|
|
6551
|
+
};
|
|
6552
|
+
walk(root);
|
|
6553
|
+
return out;
|
|
6554
|
+
}
|
|
6555
|
+
/**
|
|
6556
|
+
* Where the current camera is and which way it looks, or null when the tree
|
|
6557
|
+
* has no `Camera3D` (a 2D scene — `facing` then means `nearest`). Forward is
|
|
6558
|
+
* the camera's -z through its Euler XYZ rotation: the third basis column
|
|
6559
|
+
* negated, `(-sin ry, sin rx·cos ry, -cos rx·cos ry)`.
|
|
6560
|
+
*/
|
|
6561
|
+
function cameraEye(root) {
|
|
6562
|
+
let first = null;
|
|
6563
|
+
let current = null;
|
|
6564
|
+
const walk = (n) => {
|
|
6565
|
+
if (n.constructor.typeName === "Camera3D") {
|
|
6566
|
+
first ??= n;
|
|
6567
|
+
if (n.current) current ??= n;
|
|
6568
|
+
}
|
|
6569
|
+
for (const c of n.children) walk(c);
|
|
6570
|
+
};
|
|
6571
|
+
walk(root);
|
|
6572
|
+
const cam = current ?? first;
|
|
6573
|
+
if (!cam) return null;
|
|
6574
|
+
const rot = cam.rotation ?? [
|
|
6575
|
+
0,
|
|
6576
|
+
0,
|
|
6577
|
+
0
|
|
6578
|
+
];
|
|
6579
|
+
const rx = (rot[0] ?? 0) * Math.PI / 180;
|
|
6580
|
+
const ry = (rot[1] ?? 0) * Math.PI / 180;
|
|
6581
|
+
const fx = -Math.sin(ry);
|
|
6582
|
+
const fy = Math.sin(rx) * Math.cos(ry);
|
|
6583
|
+
const fz = -Math.cos(rx) * Math.cos(ry);
|
|
6584
|
+
return {
|
|
6585
|
+
at: worldPosition(cam),
|
|
6586
|
+
fx,
|
|
6587
|
+
fy,
|
|
6588
|
+
fz
|
|
6589
|
+
};
|
|
6590
|
+
}
|
|
6246
6591
|
function collectGroup(node, group) {
|
|
6247
6592
|
const out = [];
|
|
6248
6593
|
const walk = (n) => {
|
|
@@ -6703,11 +7048,18 @@ var PathFollow = class extends Behavior {
|
|
|
6703
7048
|
static signals = ["waypointReached", "arrived"];
|
|
6704
7049
|
static props = {
|
|
6705
7050
|
speed: { default: 120 },
|
|
6706
|
-
loop: { default: false }
|
|
7051
|
+
loop: { default: false },
|
|
7052
|
+
/** Move the parent node instead of this one — the AI-on-a-child pattern `Chase`/`Patrol`/`Wander` share. */
|
|
7053
|
+
moveParent: { default: false },
|
|
7054
|
+
/** A node to TURN toward the direction of travel (a skin, a hull) — the same `facePath`/`turnSpeed` the other movers have. */
|
|
7055
|
+
...FACING_PROPS
|
|
6707
7056
|
};
|
|
6708
7057
|
/** Units per second (px in 2D, meters in 3D). */
|
|
6709
7058
|
speed = 120;
|
|
6710
7059
|
loop = false;
|
|
7060
|
+
moveParent = false;
|
|
7061
|
+
facePath = "";
|
|
7062
|
+
turnSpeed = 100;
|
|
6711
7063
|
waypoints = [];
|
|
6712
7064
|
index = 0;
|
|
6713
7065
|
/** True while there is somewhere left to go. */
|
|
@@ -6725,24 +7077,47 @@ var PathFollow = class extends Behavior {
|
|
|
6725
7077
|
}
|
|
6726
7078
|
update(dt) {
|
|
6727
7079
|
if (!this.moving) return;
|
|
6728
|
-
const node =
|
|
7080
|
+
const node = this.mover();
|
|
6729
7081
|
let budget = this.speed * dt;
|
|
7082
|
+
const from = [...node.position];
|
|
7083
|
+
let at = [...node.position];
|
|
6730
7084
|
while (budget > 0 && this.index < this.waypoints.length) {
|
|
6731
7085
|
const target = this.waypoints[this.index];
|
|
6732
|
-
const step = moveToward(
|
|
6733
|
-
const travelled = distanceBetween(
|
|
6734
|
-
|
|
7086
|
+
const step = moveToward(at, target, budget);
|
|
7087
|
+
const travelled = distanceBetween(at, step.position);
|
|
7088
|
+
at = step.position;
|
|
6735
7089
|
budget -= travelled;
|
|
6736
7090
|
if (step.reached) {
|
|
6737
7091
|
this.node.emit("waypointReached", this.index);
|
|
6738
7092
|
this.index += 1;
|
|
6739
7093
|
if (this.index >= this.waypoints.length) if (this.loop && this.waypoints.length > 1) this.index = 0;
|
|
6740
7094
|
else {
|
|
7095
|
+
moveBody(node, at, dt);
|
|
7096
|
+
this.face(node, from, at, dt);
|
|
6741
7097
|
this.node.emit("arrived");
|
|
6742
7098
|
return;
|
|
6743
7099
|
}
|
|
6744
|
-
} else
|
|
7100
|
+
} else break;
|
|
6745
7101
|
}
|
|
7102
|
+
moveBody(node, at, dt);
|
|
7103
|
+
this.face(node, from, at, dt);
|
|
7104
|
+
}
|
|
7105
|
+
/** `facePath` turns toward where this frame went — nothing when it did not move. */
|
|
7106
|
+
face(node, from, to, dt) {
|
|
7107
|
+
if (this.facePath === "") return;
|
|
7108
|
+
const skin = this.node.getNodeOrNull(this.facePath);
|
|
7109
|
+
if (!skin) return;
|
|
7110
|
+
faceTravel(skin, to.map((v, i) => v - (from[i] ?? 0)), dt, this.turnSpeed);
|
|
7111
|
+
}
|
|
7112
|
+
/** The node this walks: the parent under `moveParent`, else this one — both must be spatial. */
|
|
7113
|
+
mover() {
|
|
7114
|
+
if (!this.moveParent) return requirePosition(this);
|
|
7115
|
+
const parent = this.node.parent;
|
|
7116
|
+
if (!parent || !hasPosition$1(parent)) throw new IncantoError("PROP_TYPE_MISMATCH", `PathFollow on '${this.node.getPath()}': "moveParent" needs a spatial parent (a Node2D/Node3D) to move.`, {
|
|
7117
|
+
prop: "moveParent",
|
|
7118
|
+
path: this.node.getPath()
|
|
7119
|
+
});
|
|
7120
|
+
return parent;
|
|
6746
7121
|
}
|
|
6747
7122
|
};
|
|
6748
7123
|
function distanceBetween(a, b) {
|
|
@@ -7120,8 +7495,21 @@ var Projectile = class extends Behavior {
|
|
|
7120
7495
|
/** Units per second along `direction`. */
|
|
7121
7496
|
speed: { default: 300 },
|
|
7122
7497
|
/** A vector ([x,y(,z)]) OR the string 'forward' (null = 'forward', from rotation). */
|
|
7123
|
-
direction: {
|
|
7124
|
-
|
|
7498
|
+
direction: {
|
|
7499
|
+
default: null,
|
|
7500
|
+
kinds: [
|
|
7501
|
+
"null",
|
|
7502
|
+
"array",
|
|
7503
|
+
"string"
|
|
7504
|
+
]
|
|
7505
|
+
},
|
|
7506
|
+
/**
|
|
7507
|
+
* Constant DOWNWARD acceleration in the scene's own convention — +y in 2D
|
|
7508
|
+
* (y-down), −y in 3D (y-up) — so `9.81` pulls a 3D lob down. It used to add
|
|
7509
|
+
* to +y regardless, the 2D convention, and `9.81` in a y-up scene LIFTED the
|
|
7510
|
+
* shot (measured: 19 m up at 1.5 s); `Chase.ground` signs its fall the same
|
|
7511
|
+
* way. 0 = straight line.
|
|
7512
|
+
*/
|
|
7125
7513
|
gravity: { default: 0 }
|
|
7126
7514
|
};
|
|
7127
7515
|
static signals = [];
|
|
@@ -7180,7 +7568,7 @@ var Projectile = class extends Behavior {
|
|
|
7180
7568
|
update(dt) {
|
|
7181
7569
|
if (!this.armed) this.aim();
|
|
7182
7570
|
const node = this.node;
|
|
7183
|
-
if (this.gravity !== 0) this.fall += this.gravity * dt;
|
|
7571
|
+
if (this.gravity !== 0) this.fall += this.gravity * dt * (node.position.length === 3 ? -1 : 1);
|
|
7184
7572
|
const pos = node.position;
|
|
7185
7573
|
const n = Math.max(pos.length, this.velocity.length);
|
|
7186
7574
|
const next = new Array(n);
|
|
@@ -7429,6 +7817,17 @@ var ScoreKeeper = class extends Behavior {
|
|
|
7429
7817
|
scoreToWin = 0;
|
|
7430
7818
|
hasWon = false;
|
|
7431
7819
|
hasLost = false;
|
|
7820
|
+
/**
|
|
7821
|
+
* Whether the run is over — readable, not only heard. A stealth yard's HUD
|
|
7822
|
+
* and a melee arena's both read `score.won` for their banner and got
|
|
7823
|
+
* `undefined`, silently, because only the signals were public.
|
|
7824
|
+
*/
|
|
7825
|
+
get won() {
|
|
7826
|
+
return this.hasWon;
|
|
7827
|
+
}
|
|
7828
|
+
get lost() {
|
|
7829
|
+
return this.hasLost;
|
|
7830
|
+
}
|
|
7432
7831
|
onReady() {
|
|
7433
7832
|
if (this.lives < 0) throw new IncantoError("PROP_TYPE_MISMATCH", `ScoreKeeper on '${this.node.getPath()}': "lives" must be >= 0, got ${this.lives}.`, { prop: "lives" });
|
|
7434
7833
|
if (this.scoreToWin < 0) throw new IncantoError("PROP_TYPE_MISMATCH", `ScoreKeeper on '${this.node.getPath()}': "scoreToWin" must be >= 0, got ${this.scoreToWin}.`, { prop: "scoreToWin" });
|
|
@@ -7563,8 +7962,12 @@ var SpawnSource = class {
|
|
|
7563
7962
|
*/
|
|
7564
7963
|
spawn(template, opts = void 0) {
|
|
7565
7964
|
const clone = duplicateNode(template, rebuildOptions(this.behavior.node));
|
|
7566
|
-
const at = opts?.at;
|
|
7567
|
-
|
|
7965
|
+
const at = opts?.at ?? [];
|
|
7966
|
+
const under = opts?.parent ?? this.behavior.node;
|
|
7967
|
+
if (hasPosition$1(clone) && (at.length > 0 || under !== this.behavior.node)) {
|
|
7968
|
+
const origin = under === this.behavior.node || !hasPosition$1(this.behavior.node) ? [] : sub(worldPosition(this.behavior.node), worldPosition(under));
|
|
7969
|
+
clone.position = add(origin, at.length > 0 ? at : zeros(origin.length || clone.position.length));
|
|
7970
|
+
}
|
|
7568
7971
|
if (typeof clone.visible === "boolean") clone.visible = true;
|
|
7569
7972
|
(opts?.parent ?? this.behavior.node).addChild(clone);
|
|
7570
7973
|
return clone;
|
|
@@ -7593,6 +7996,15 @@ function absolutePrefabPath(node, prefab) {
|
|
|
7593
7996
|
}
|
|
7594
7997
|
return parts.join("/");
|
|
7595
7998
|
}
|
|
7999
|
+
function sub(a, b) {
|
|
8000
|
+
const n = Math.max(a.length, b.length);
|
|
8001
|
+
const out = [];
|
|
8002
|
+
for (let i = 0; i < n; i++) out.push((a[i] ?? 0) - (b[i] ?? 0));
|
|
8003
|
+
return out;
|
|
8004
|
+
}
|
|
8005
|
+
function zeros(n) {
|
|
8006
|
+
return Array.from({ length: n }, () => 0);
|
|
8007
|
+
}
|
|
7596
8008
|
//#endregion
|
|
7597
8009
|
//#region src/gameplay/spawner.ts
|
|
7598
8010
|
/**
|
|
@@ -7626,7 +8038,17 @@ var Spawner = class extends Behavior {
|
|
|
7626
8038
|
/** Begin ticking at ready. */
|
|
7627
8039
|
autoStart: { default: true },
|
|
7628
8040
|
/** Total LIFETIME spawns (0 = infinite). */
|
|
7629
|
-
total: { default: 0 }
|
|
8041
|
+
total: { default: 0 },
|
|
8042
|
+
/**
|
|
8043
|
+
* Where clones live: a node path, or '' for under the spawner itself. Clones
|
|
8044
|
+
* of a spawner that MOVES ride along with it when they are its children —
|
|
8045
|
+
* a gate a director slid to a new edge before each spawn dragged every
|
|
8046
|
+
* enemy it had made — so a container makes the spawner just a place.
|
|
8047
|
+
*/
|
|
8048
|
+
into: {
|
|
8049
|
+
default: "",
|
|
8050
|
+
nodePath: true
|
|
8051
|
+
}
|
|
7630
8052
|
};
|
|
7631
8053
|
static signals = ["spawned", "finished"];
|
|
7632
8054
|
prefab = "";
|
|
@@ -7635,6 +8057,7 @@ var Spawner = class extends Behavior {
|
|
|
7635
8057
|
at = [];
|
|
7636
8058
|
autoStart = true;
|
|
7637
8059
|
total = 0;
|
|
8060
|
+
into = "";
|
|
7638
8061
|
source = new SpawnSource(this);
|
|
7639
8062
|
template;
|
|
7640
8063
|
live = /* @__PURE__ */ new Set();
|
|
@@ -7657,6 +8080,7 @@ var Spawner = class extends Behavior {
|
|
|
7657
8080
|
onReady() {
|
|
7658
8081
|
if (!(this.interval > 0) && this.max <= 0 && this.total <= 0) throw new IncantoError("PROP_TYPE_MISMATCH", `Spawner on '${this.node.getPath()}': "interval" ${this.interval} spawns a clone every FRAME and neither "max" (live cap) nor "total" (lifetime cap) is set, so the scene grows without bound. Give it an interval, or a cap.`, { prop: "interval" });
|
|
7659
8082
|
this.template = this.source.resolveTemplate(this.prefab);
|
|
8083
|
+
if (this.into !== "") this.node.getNode(this.into);
|
|
7660
8084
|
this.running = this.autoStart;
|
|
7661
8085
|
this.timer = this.interval;
|
|
7662
8086
|
}
|
|
@@ -7674,7 +8098,11 @@ var Spawner = class extends Behavior {
|
|
|
7674
8098
|
this.prune();
|
|
7675
8099
|
if (this.done) return null;
|
|
7676
8100
|
if (this.max > 0 && this.live.size >= this.max) return null;
|
|
7677
|
-
const
|
|
8101
|
+
const parent = this.into === "" ? void 0 : this.node.getNode(this.into);
|
|
8102
|
+
const clone = this.source.spawn(this.template, this.at.length > 0 || parent ? {
|
|
8103
|
+
at: this.at,
|
|
8104
|
+
parent
|
|
8105
|
+
} : void 0);
|
|
7678
8106
|
this.live.add(clone);
|
|
7679
8107
|
this.spawnedCount += 1;
|
|
7680
8108
|
this.emit("spawned", clone);
|
|
@@ -7698,6 +8126,140 @@ var Spawner = class extends Behavior {
|
|
|
7698
8126
|
}
|
|
7699
8127
|
};
|
|
7700
8128
|
//#endregion
|
|
8129
|
+
//#region src/gameplay/turret.ts
|
|
8130
|
+
/**
|
|
8131
|
+
* The oldest verb in games: pick the nearest live member of `targetGroup`
|
|
8132
|
+
* within `range` and hit it every `cooldown` — a defense tower, a ship's gun,
|
|
8133
|
+
* a sentry, a trooper who fights what he meets.
|
|
8134
|
+
*
|
|
8135
|
+
* Two games in a row wrote these forty lines before it existed (an RTS's
|
|
8136
|
+
* trooper, a tower defense's tower): the engine had `Chase` for going TO a
|
|
8137
|
+
* target and `Projectile` for the bullet, and nothing that decides to shoot.
|
|
8138
|
+
*
|
|
8139
|
+
* - **Hitscan** by default: the target's `Health` (on it or a child, as
|
|
8140
|
+
* `DamageOnContact` finds it) takes `damage(n)`.
|
|
8141
|
+
* - **Or a clone**: name a `shelf` (a `PrefabShelf`) and a `prefab` on it; each
|
|
8142
|
+
* shot takes one, puts it at the turret plus `muzzle`, points its
|
|
8143
|
+
* `Projectile` at the target and adds it under `into` (the turret's parent
|
|
8144
|
+
* when empty). The clone does the hurting then — give it `DamageOnContact`.
|
|
8145
|
+
* - `facePath` turns a barrel toward the target at `turnSpeed`.
|
|
8146
|
+
*
|
|
8147
|
+
* Signals: `acquired(target)` when something comes in range, `fired(target)`
|
|
8148
|
+
* per shot, `lost()` when nothing live is in range any more.
|
|
8149
|
+
*/
|
|
8150
|
+
var Turret = class extends Behavior {
|
|
8151
|
+
static props = {
|
|
8152
|
+
/** Who it shoots: the nearest live member of this group. */
|
|
8153
|
+
targetGroup: { default: "enemy" },
|
|
8154
|
+
/** How far it reaches, in the scene's units. */
|
|
8155
|
+
range: {
|
|
8156
|
+
default: 6,
|
|
8157
|
+
range: { min: 0 }
|
|
8158
|
+
},
|
|
8159
|
+
/** Hitscan damage per shot (ignored when a `prefab` does the hurting). */
|
|
8160
|
+
damage: { default: 10 },
|
|
8161
|
+
/** Seconds between shots. */
|
|
8162
|
+
cooldown: {
|
|
8163
|
+
default: .6,
|
|
8164
|
+
range: { min: 0 }
|
|
8165
|
+
},
|
|
8166
|
+
/** A `PrefabShelf` to take each shot from (empty = hitscan). */
|
|
8167
|
+
shelf: {
|
|
8168
|
+
default: "",
|
|
8169
|
+
nodePath: true
|
|
8170
|
+
},
|
|
8171
|
+
/** The template on that shelf. */
|
|
8172
|
+
prefab: { default: "" },
|
|
8173
|
+
/** Where clones start, relative to the turret. */
|
|
8174
|
+
muzzle: { default: [] },
|
|
8175
|
+
/** Where clones go; empty = the turret's parent. */
|
|
8176
|
+
into: {
|
|
8177
|
+
default: "",
|
|
8178
|
+
nodePath: true
|
|
8179
|
+
},
|
|
8180
|
+
...FACING_PROPS
|
|
8181
|
+
};
|
|
8182
|
+
static signals = [
|
|
8183
|
+
"acquired",
|
|
8184
|
+
"fired",
|
|
8185
|
+
"lost"
|
|
8186
|
+
];
|
|
8187
|
+
targetGroup = "enemy";
|
|
8188
|
+
range = 6;
|
|
8189
|
+
damage = 10;
|
|
8190
|
+
cooldown = .6;
|
|
8191
|
+
shelf = "";
|
|
8192
|
+
prefab = "";
|
|
8193
|
+
muzzle = [];
|
|
8194
|
+
into = "";
|
|
8195
|
+
facePath = "";
|
|
8196
|
+
turnSpeed = 100;
|
|
8197
|
+
/** What it is shooting at right now, or null. */
|
|
8198
|
+
target = null;
|
|
8199
|
+
/** Shots fired so far. */
|
|
8200
|
+
shots = 0;
|
|
8201
|
+
wait = 0;
|
|
8202
|
+
onReady() {
|
|
8203
|
+
if (this.shelf === "" !== (this.prefab === "")) throw new IncantoError("PROP_TYPE_MISMATCH", `Turret on '${this.node.getPath()}': "shelf" and "prefab" go together — name both for a projectile, neither for hitscan.`, {
|
|
8204
|
+
prop: this.shelf === "" ? "shelf" : "prefab",
|
|
8205
|
+
path: this.node.getPath()
|
|
8206
|
+
});
|
|
8207
|
+
}
|
|
8208
|
+
update(dt) {
|
|
8209
|
+
this.wait = Math.max(0, this.wait - dt);
|
|
8210
|
+
if (!hasPosition$1(this.node)) return;
|
|
8211
|
+
const here = worldPosition(this.node);
|
|
8212
|
+
let best = null;
|
|
8213
|
+
let bestD = this.range;
|
|
8214
|
+
for (const n of this.node.tree?.getNodesInGroup(this.targetGroup) ?? []) {
|
|
8215
|
+
if (n === this.node || !hasPosition$1(n)) continue;
|
|
8216
|
+
if (findHealth(n)?.health.isDead) continue;
|
|
8217
|
+
const d = distance$1(here, worldPosition(n));
|
|
8218
|
+
if (d <= bestD) {
|
|
8219
|
+
best = n;
|
|
8220
|
+
bestD = d;
|
|
8221
|
+
}
|
|
8222
|
+
}
|
|
8223
|
+
if (best !== this.target) {
|
|
8224
|
+
const had = this.target;
|
|
8225
|
+
this.target = best;
|
|
8226
|
+
if (best) this.emit("acquired", best);
|
|
8227
|
+
else if (had) this.emit("lost");
|
|
8228
|
+
}
|
|
8229
|
+
if (!best) return;
|
|
8230
|
+
const dir = worldPosition(best).map((v, i) => v - (here[i] ?? 0));
|
|
8231
|
+
if (this.facePath !== "") {
|
|
8232
|
+
const barrel = this.node.getNodeOrNull(this.facePath);
|
|
8233
|
+
if (barrel) faceTravel(barrel, dir, dt, this.turnSpeed);
|
|
8234
|
+
}
|
|
8235
|
+
if (this.wait > 0) return;
|
|
8236
|
+
this.wait = this.cooldown;
|
|
8237
|
+
this.shots += 1;
|
|
8238
|
+
if (this.shelf !== "") this.launch(best, here, dir);
|
|
8239
|
+
else findHealth(best)?.health.damage(this.damage);
|
|
8240
|
+
this.emit("fired", best);
|
|
8241
|
+
}
|
|
8242
|
+
/** One clone from the shelf, at the muzzle, aimed at the target, into the container. */
|
|
8243
|
+
launch(target, here, dir) {
|
|
8244
|
+
const shelf = this.node.getNodeOrNull(this.shelf)?.behavior;
|
|
8245
|
+
if (!shelf || typeof shelf.make !== "function") throw new IncantoError("NODE_NOT_FOUND", `Turret on '${this.node.getPath()}': "shelf" '${this.shelf}' is not a PrefabShelf.`, {
|
|
8246
|
+
prop: "shelf",
|
|
8247
|
+
path: this.node.getPath()
|
|
8248
|
+
});
|
|
8249
|
+
const clone = shelf.make(this.prefab);
|
|
8250
|
+
if (!hasPosition$1(clone)) return;
|
|
8251
|
+
const container = this.into !== "" ? this.node.getNodeOrNull(this.into) : this.node.parent;
|
|
8252
|
+
if (!container) return;
|
|
8253
|
+
const len = Math.hypot(...dir) || 1;
|
|
8254
|
+
const unit = dir.map((v) => v / len);
|
|
8255
|
+
const containerAt = hasPosition$1(container) ? worldPosition(container) : here.map(() => 0);
|
|
8256
|
+
clone.position = here.map((v, i) => v + (this.muzzle[i] ?? 0) - (containerAt[i] ?? 0));
|
|
8257
|
+
const shot = clone.behavior;
|
|
8258
|
+
if (shot && "direction" in shot) shot.direction = unit;
|
|
8259
|
+
container.addChild(clone);
|
|
8260
|
+
}
|
|
8261
|
+
};
|
|
8262
|
+
//#endregion
|
|
7701
8263
|
//#region src/gameplay/wander.ts
|
|
7702
8264
|
/**
|
|
7703
8265
|
* Aimless roaming inside a circle around the spawn point — idle critters,
|
|
@@ -7793,6 +8355,14 @@ var WaveSpawner = class extends Behavior {
|
|
|
7793
8355
|
parsed = [];
|
|
7794
8356
|
live = /* @__PURE__ */ new Set();
|
|
7795
8357
|
waveIndex = -1;
|
|
8358
|
+
/** The wave now running or being cleared, 1-based — what a HUD prints as `wave 2 / 5`. `0` before the first. */
|
|
8359
|
+
get wave() {
|
|
8360
|
+
return this.waveIndex < 0 ? 0 : Math.min(this.parsed.length, this.waveIndex + 1);
|
|
8361
|
+
}
|
|
8362
|
+
/** How many waves were declared. */
|
|
8363
|
+
get waveCount() {
|
|
8364
|
+
return this.parsed.length;
|
|
8365
|
+
}
|
|
7796
8366
|
phase = "idle";
|
|
7797
8367
|
timer = 0;
|
|
7798
8368
|
spawnedThisWave = 0;
|
|
@@ -8105,6 +8675,8 @@ const GAMEPLAY_BEHAVIORS = {
|
|
|
8105
8675
|
PathFollow,
|
|
8106
8676
|
Health,
|
|
8107
8677
|
Lifetime,
|
|
8678
|
+
Checkpoint,
|
|
8679
|
+
Turret,
|
|
8108
8680
|
ScoreKeeper,
|
|
8109
8681
|
Currency,
|
|
8110
8682
|
Pickup,
|
|
@@ -8138,4 +8710,4 @@ function registerGameplayBehaviors(opts) {
|
|
|
8138
8710
|
for (const [name, ctor] of Object.entries(GAMEPLAY_BEHAVIORS)) registerBehavior(name, ctor, { replace });
|
|
8139
8711
|
}
|
|
8140
8712
|
//#endregion
|
|
8141
|
-
export {
|
|
8713
|
+
export { localFromWorld as $, FaceTarget as A, WATER_CUTOUT_MAX as B, GameFlow as C, FollowCamera as D, tolerateUnknownAction as E, Currency as F, PhysicsBody3D as G, rejectScale as H, Collector as I, Node3D as J, RigidBody3D as K, Checkpoint as L, phaseOf as M, DamageOnContact as N, effectRng as O, Health as P, colliderFootDrop as Q, Chase as R, Interactable as S, restartScene as T, Area3D as U, WaterCutout3D as V, CharacterBody3D as W, createCausticsQuad as X, validateCollider3D as Y, WATER_MAX_RIPPLES as Z, Lifetime as _, Wander as a, hitStop as b, ScoreKeeper as c, PrefabShelf as d, worldPosition as et, Pickup as f, MoveTo as g, Oscillate as h, WaveSpawner as i, DayNight as j, FloatAway as k, SavePoint as l, PathFollow as m, registerGameplayBehaviors as n, Turret as o, Patrol as p, StaticBody3D as q, ZombieAI as r, Spawner as s, GAMEPLAY_BEHAVIORS as t, Projectile as u, CameraShake as v, goToScene as w, screenFlash as x, Cooldown as y, Water3D as z };
|