incanto 0.71.0 → 0.73.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-editor.mjs +15 -9
- package/bin/incanto-feel.mjs +11 -1
- package/bin/incanto-play.mjs +11 -1
- package/bin/incanto-playtest.mjs +11 -1
- 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-Cfmd3ar_.js → agent8-CHTT4unP.js} +1 -1
- package/dist/{audio-player-L8yccdMP.d.ts → audio-player-BOrk4eQx.d.ts} +1 -1
- package/dist/{behavior-Dcz0fr1S.d.ts → behavior-J1cmVah0.d.ts} +9 -1
- package/dist/{create-game-LCHBLfA4.js → create-game-CAu-ucHg.js} +8 -7
- package/dist/{create-game-CH4kt78v.js → create-game-CbTLFjis.js} +37 -13
- package/dist/debug.d.ts +1 -1
- package/dist/debug.js +1 -1
- package/dist/editor.js +1036 -930
- package/dist/{environment-presets-99BzP_L-.js → environment-presets-BsFJM3v0.js} +277 -60
- package/dist/{gameplay-CNULJvwh.js → gameplay-CRte0OVf.js} +589 -35
- package/dist/gameplay.d.ts +136 -4
- package/dist/gameplay.js +2 -2
- package/dist/index.d.ts +6 -4
- package/dist/index.js +6 -6
- package/dist/{loader-8-IHvbKD.d.ts → loader-BAeWMYfF.d.ts} +10 -3
- package/dist/net.d.ts +2 -2
- package/dist/net.js +2 -2
- package/dist/{physics-2d-FhrXUc6B.js → physics-2d-CovokK7s.js} +94 -11
- package/dist/{physics-3d-CFEGxBXZ.js → physics-3d-BiK6Emwn.js} +202 -15
- package/dist/{picking-DVo7fI13.js → picking-Dtd8JInh.js} +2 -2
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{register-BYQCBySi.js → register-CY9QCCZs.js} +45 -9
- package/dist/{register-DuVdzxbq.js → register-CjociOtt.js} +19 -4
- package/dist/{replay-DEvp3kyV.js → replay-B6BbgGM_.js} +55 -12
- package/dist/{replay-F7IZHdFR.d.ts → replay-wZK5Ok5c.d.ts} +2 -2
- package/dist/{save-slots-Bvuh2p_r.js → save-slots-CEUGeLuB.js} +57 -3
- package/dist/{split-screen-eULetcg-.js → split-screen-BxZ5rBe7.js} +4 -4
- package/dist/{split-screen-CIYf1zSf.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-_jk0qLsS.js → src-D-dnVrFY.js} +1 -1
- package/dist/{test-CmN54kWv.js → test-DZmCFz0Q.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.d.ts +8 -1
- package/dist/vite.js +22 -5
- package/editor/assets/{agent8-yLIEYHbd.js → agent8-GuZ5u5bi.js} +1 -1
- package/editor/assets/{debug-u31w_yhq.js → debug-BbxionO2.js} +1 -1
- package/editor/assets/{index-DK9xMGpW.js → index-CNlTzQbm.js} +92 -92
- 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 +15 -0
- package/skills/incanto-building-3d-games.md +45 -0
- package/skills/incanto-editor.md +10 -1
- package/skills/incanto-environment.md +7 -2
- package/skills/incanto-game-feel.md +16 -11
- package/skills/incanto-gameplay-behaviors.md +174 -15
- package/skills/incanto-node-reference.md +122 -45
- package/skills/incanto-physics-and-input.md +67 -5
- package/skills/incanto-scene-json-authoring.md +37 -1
- 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 +512 -184
- 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 +319 -93
- package/templates-app/platformer-2d/verify.ts +54 -0
- package/templates-app/star-survivor/package.json +1 -1
- package/templates-app/star-survivor/src/game.scene.json +18 -9
- 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 +379 -80
- 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
- package/templates-app/village-quest-3d/src/grove.scene.json +98 -46
- package/templates-app/village-quest-3d/src/village.scene.json +449 -223
|
@@ -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) {
|
|
@@ -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,63 @@ 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`: the closest inside `coneDeg` of the current camera's
|
|
6441
|
+
* forward — the one the player is LOOKING at, pitch included, so a note
|
|
6442
|
+
* on the desk and the drawer under it are two different looks (with no
|
|
6443
|
+
* camera, the same as `nearest`).
|
|
6444
|
+
*/
|
|
6445
|
+
focus: {
|
|
6446
|
+
default: "any",
|
|
6447
|
+
options: [
|
|
6448
|
+
"any",
|
|
6449
|
+
"nearest",
|
|
6450
|
+
"facing"
|
|
6451
|
+
]
|
|
6452
|
+
},
|
|
6453
|
+
/** Full width of the `facing` cone, degrees — a generous crosshair. */
|
|
6454
|
+
coneDeg: {
|
|
6455
|
+
default: 40,
|
|
6456
|
+
range: {
|
|
6457
|
+
min: 1,
|
|
6458
|
+
max: 360
|
|
6459
|
+
}
|
|
6460
|
+
}
|
|
6213
6461
|
};
|
|
6214
|
-
static signals = [
|
|
6462
|
+
static signals = [
|
|
6463
|
+
"interacted",
|
|
6464
|
+
"focused",
|
|
6465
|
+
"unfocused"
|
|
6466
|
+
];
|
|
6215
6467
|
action = "interact";
|
|
6216
6468
|
range = 2;
|
|
6217
6469
|
actorGroup = "player";
|
|
6470
|
+
prompt = "";
|
|
6471
|
+
focus = "any";
|
|
6472
|
+
coneDeg = 40;
|
|
6473
|
+
/**
|
|
6474
|
+
* The actor this Interactable would answer RIGHT NOW, or null — updated every
|
|
6475
|
+
* frame, with `focused(actor)` / `unfocused(actor)` on the edges. A HUD reads
|
|
6476
|
+
* the focused one's `prompt`.
|
|
6477
|
+
*/
|
|
6478
|
+
focusedBy = null;
|
|
6218
6479
|
/** Actions already reported missing, so a per-frame read says it once. */
|
|
6219
6480
|
reportedActions = /* @__PURE__ */ new Set();
|
|
6220
6481
|
update() {
|
|
6221
6482
|
const engine = this.node.tree?.engine;
|
|
6222
6483
|
if (!engine?.scene) return;
|
|
6223
|
-
|
|
6484
|
+
const pressed = tolerateUnknownAction({
|
|
6224
6485
|
engine,
|
|
6225
6486
|
who: this.node.getPath(),
|
|
6226
6487
|
reported: this.reportedActions,
|
|
6227
6488
|
consequence: "nothing here can be interacted with",
|
|
6228
6489
|
authored: this.action !== "interact"
|
|
6229
|
-
}, this.action, () => engine.input.justPressed(this.action), false)
|
|
6490
|
+
}, this.action, () => engine.input.justPressed(this.action), false);
|
|
6230
6491
|
if (!hasPosition(this.node)) return;
|
|
6231
6492
|
const self = worldPosition(this.node);
|
|
6232
6493
|
let nearest = null;
|
|
@@ -6240,9 +6501,89 @@ var Interactable = class extends Behavior {
|
|
|
6240
6501
|
nearestDist = d;
|
|
6241
6502
|
}
|
|
6242
6503
|
}
|
|
6243
|
-
|
|
6504
|
+
let holder = nearest;
|
|
6505
|
+
if (holder && this.focus !== "any" && !this.claims(holder, self)) holder = null;
|
|
6506
|
+
if (holder !== this.focusedBy) {
|
|
6507
|
+
const was = this.focusedBy;
|
|
6508
|
+
this.focusedBy = holder;
|
|
6509
|
+
if (was) this.emit("unfocused", was);
|
|
6510
|
+
if (holder) this.emit("focused", holder);
|
|
6511
|
+
}
|
|
6512
|
+
if (!pressed || !holder) return;
|
|
6513
|
+
this.emit("interacted", holder);
|
|
6514
|
+
}
|
|
6515
|
+
/** Is THIS the Interactable `actor` means — the nearest (that it faces) of every one in reach? */
|
|
6516
|
+
claims(actor, self) {
|
|
6517
|
+
const eye = this.focus === "facing" ? cameraEye(this.node.getRoot()) : null;
|
|
6518
|
+
const from = eye ? eye.at : worldPosition(actor);
|
|
6519
|
+
const inCone = (target) => {
|
|
6520
|
+
if (!eye) return true;
|
|
6521
|
+
const dx = (target[0] ?? 0) - (from[0] ?? 0);
|
|
6522
|
+
const dy = (target[1] ?? 0) - (from[1] ?? 0);
|
|
6523
|
+
const dz = (target[2] ?? 0) - (from[2] ?? 0);
|
|
6524
|
+
const len = Math.hypot(dx, dy, dz);
|
|
6525
|
+
if (len < 1e-6) return true;
|
|
6526
|
+
return dx / len * eye.fx + dy / len * eye.fy + dz / len * eye.fz >= Math.cos(this.coneDeg / 2 * (Math.PI / 180));
|
|
6527
|
+
};
|
|
6528
|
+
if (!inCone(self)) return false;
|
|
6529
|
+
const mine = distance(from, self);
|
|
6530
|
+
for (const other of interactablesIn(this.node.getRoot())) {
|
|
6531
|
+
if (other === this || other.focus === "any" || other.actorGroup !== this.actorGroup) continue;
|
|
6532
|
+
if (!hasPosition(other.node)) continue;
|
|
6533
|
+
const there = worldPosition(other.node);
|
|
6534
|
+
if (distance(worldPosition(actor), there) > other.range) continue;
|
|
6535
|
+
if (!inCone(there)) continue;
|
|
6536
|
+
if (distance(from, there) < mine) return false;
|
|
6537
|
+
}
|
|
6538
|
+
return true;
|
|
6244
6539
|
}
|
|
6245
6540
|
};
|
|
6541
|
+
/** Every Interactable in the tree — walked on demand, only while an actor is in reach of one. */
|
|
6542
|
+
function interactablesIn(root) {
|
|
6543
|
+
const out = [];
|
|
6544
|
+
const walk = (n) => {
|
|
6545
|
+
if (n.behavior instanceof Interactable) out.push(n.behavior);
|
|
6546
|
+
for (const c of n.children) walk(c);
|
|
6547
|
+
};
|
|
6548
|
+
walk(root);
|
|
6549
|
+
return out;
|
|
6550
|
+
}
|
|
6551
|
+
/**
|
|
6552
|
+
* Where the current camera is and which way it looks, or null when the tree
|
|
6553
|
+
* has no `Camera3D` (a 2D scene — `facing` then means `nearest`). Forward is
|
|
6554
|
+
* the camera's -z through its Euler XYZ rotation: the third basis column
|
|
6555
|
+
* negated, `(-sin ry, sin rx·cos ry, -cos rx·cos ry)`.
|
|
6556
|
+
*/
|
|
6557
|
+
function cameraEye(root) {
|
|
6558
|
+
let first = null;
|
|
6559
|
+
let current = null;
|
|
6560
|
+
const walk = (n) => {
|
|
6561
|
+
if (n.constructor.typeName === "Camera3D") {
|
|
6562
|
+
first ??= n;
|
|
6563
|
+
if (n.current) current ??= n;
|
|
6564
|
+
}
|
|
6565
|
+
for (const c of n.children) walk(c);
|
|
6566
|
+
};
|
|
6567
|
+
walk(root);
|
|
6568
|
+
const cam = current ?? first;
|
|
6569
|
+
if (!cam) return null;
|
|
6570
|
+
const rot = cam.rotation ?? [
|
|
6571
|
+
0,
|
|
6572
|
+
0,
|
|
6573
|
+
0
|
|
6574
|
+
];
|
|
6575
|
+
const rx = (rot[0] ?? 0) * Math.PI / 180;
|
|
6576
|
+
const ry = (rot[1] ?? 0) * Math.PI / 180;
|
|
6577
|
+
const fx = -Math.sin(ry);
|
|
6578
|
+
const fy = Math.sin(rx) * Math.cos(ry);
|
|
6579
|
+
const fz = -Math.cos(rx) * Math.cos(ry);
|
|
6580
|
+
return {
|
|
6581
|
+
at: worldPosition(cam),
|
|
6582
|
+
fx,
|
|
6583
|
+
fy,
|
|
6584
|
+
fz
|
|
6585
|
+
};
|
|
6586
|
+
}
|
|
6246
6587
|
function collectGroup(node, group) {
|
|
6247
6588
|
const out = [];
|
|
6248
6589
|
const walk = (n) => {
|
|
@@ -6703,11 +7044,14 @@ var PathFollow = class extends Behavior {
|
|
|
6703
7044
|
static signals = ["waypointReached", "arrived"];
|
|
6704
7045
|
static props = {
|
|
6705
7046
|
speed: { default: 120 },
|
|
6706
|
-
loop: { default: false }
|
|
7047
|
+
loop: { default: false },
|
|
7048
|
+
/** Move the parent node instead of this one — the AI-on-a-child pattern `Chase`/`Patrol`/`Wander` share. */
|
|
7049
|
+
moveParent: { default: false }
|
|
6707
7050
|
};
|
|
6708
7051
|
/** Units per second (px in 2D, meters in 3D). */
|
|
6709
7052
|
speed = 120;
|
|
6710
7053
|
loop = false;
|
|
7054
|
+
moveParent = false;
|
|
6711
7055
|
waypoints = [];
|
|
6712
7056
|
index = 0;
|
|
6713
7057
|
/** True while there is somewhere left to go. */
|
|
@@ -6725,24 +7069,37 @@ var PathFollow = class extends Behavior {
|
|
|
6725
7069
|
}
|
|
6726
7070
|
update(dt) {
|
|
6727
7071
|
if (!this.moving) return;
|
|
6728
|
-
const node =
|
|
7072
|
+
const node = this.mover();
|
|
6729
7073
|
let budget = this.speed * dt;
|
|
7074
|
+
let at = [...node.position];
|
|
6730
7075
|
while (budget > 0 && this.index < this.waypoints.length) {
|
|
6731
7076
|
const target = this.waypoints[this.index];
|
|
6732
|
-
const step = moveToward(
|
|
6733
|
-
const travelled = distanceBetween(
|
|
6734
|
-
|
|
7077
|
+
const step = moveToward(at, target, budget);
|
|
7078
|
+
const travelled = distanceBetween(at, step.position);
|
|
7079
|
+
at = step.position;
|
|
6735
7080
|
budget -= travelled;
|
|
6736
7081
|
if (step.reached) {
|
|
6737
7082
|
this.node.emit("waypointReached", this.index);
|
|
6738
7083
|
this.index += 1;
|
|
6739
7084
|
if (this.index >= this.waypoints.length) if (this.loop && this.waypoints.length > 1) this.index = 0;
|
|
6740
7085
|
else {
|
|
7086
|
+
moveBody(node, at, dt);
|
|
6741
7087
|
this.node.emit("arrived");
|
|
6742
7088
|
return;
|
|
6743
7089
|
}
|
|
6744
|
-
} else
|
|
7090
|
+
} else break;
|
|
6745
7091
|
}
|
|
7092
|
+
moveBody(node, at, dt);
|
|
7093
|
+
}
|
|
7094
|
+
/** The node this walks: the parent under `moveParent`, else this one — both must be spatial. */
|
|
7095
|
+
mover() {
|
|
7096
|
+
if (!this.moveParent) return requirePosition(this);
|
|
7097
|
+
const parent = this.node.parent;
|
|
7098
|
+
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.`, {
|
|
7099
|
+
prop: "moveParent",
|
|
7100
|
+
path: this.node.getPath()
|
|
7101
|
+
});
|
|
7102
|
+
return parent;
|
|
6746
7103
|
}
|
|
6747
7104
|
};
|
|
6748
7105
|
function distanceBetween(a, b) {
|
|
@@ -7120,8 +7477,21 @@ var Projectile = class extends Behavior {
|
|
|
7120
7477
|
/** Units per second along `direction`. */
|
|
7121
7478
|
speed: { default: 300 },
|
|
7122
7479
|
/** A vector ([x,y(,z)]) OR the string 'forward' (null = 'forward', from rotation). */
|
|
7123
|
-
direction: {
|
|
7124
|
-
|
|
7480
|
+
direction: {
|
|
7481
|
+
default: null,
|
|
7482
|
+
kinds: [
|
|
7483
|
+
"null",
|
|
7484
|
+
"array",
|
|
7485
|
+
"string"
|
|
7486
|
+
]
|
|
7487
|
+
},
|
|
7488
|
+
/**
|
|
7489
|
+
* Constant DOWNWARD acceleration in the scene's own convention — +y in 2D
|
|
7490
|
+
* (y-down), −y in 3D (y-up) — so `9.81` pulls a 3D lob down. It used to add
|
|
7491
|
+
* to +y regardless, the 2D convention, and `9.81` in a y-up scene LIFTED the
|
|
7492
|
+
* shot (measured: 19 m up at 1.5 s); `Chase.ground` signs its fall the same
|
|
7493
|
+
* way. 0 = straight line.
|
|
7494
|
+
*/
|
|
7125
7495
|
gravity: { default: 0 }
|
|
7126
7496
|
};
|
|
7127
7497
|
static signals = [];
|
|
@@ -7180,7 +7550,7 @@ var Projectile = class extends Behavior {
|
|
|
7180
7550
|
update(dt) {
|
|
7181
7551
|
if (!this.armed) this.aim();
|
|
7182
7552
|
const node = this.node;
|
|
7183
|
-
if (this.gravity !== 0) this.fall += this.gravity * dt;
|
|
7553
|
+
if (this.gravity !== 0) this.fall += this.gravity * dt * (node.position.length === 3 ? -1 : 1);
|
|
7184
7554
|
const pos = node.position;
|
|
7185
7555
|
const n = Math.max(pos.length, this.velocity.length);
|
|
7186
7556
|
const next = new Array(n);
|
|
@@ -7429,6 +7799,17 @@ var ScoreKeeper = class extends Behavior {
|
|
|
7429
7799
|
scoreToWin = 0;
|
|
7430
7800
|
hasWon = false;
|
|
7431
7801
|
hasLost = false;
|
|
7802
|
+
/**
|
|
7803
|
+
* Whether the run is over — readable, not only heard. A stealth yard's HUD
|
|
7804
|
+
* and a melee arena's both read `score.won` for their banner and got
|
|
7805
|
+
* `undefined`, silently, because only the signals were public.
|
|
7806
|
+
*/
|
|
7807
|
+
get won() {
|
|
7808
|
+
return this.hasWon;
|
|
7809
|
+
}
|
|
7810
|
+
get lost() {
|
|
7811
|
+
return this.hasLost;
|
|
7812
|
+
}
|
|
7432
7813
|
onReady() {
|
|
7433
7814
|
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
7815
|
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 +7944,12 @@ var SpawnSource = class {
|
|
|
7563
7944
|
*/
|
|
7564
7945
|
spawn(template, opts = void 0) {
|
|
7565
7946
|
const clone = duplicateNode(template, rebuildOptions(this.behavior.node));
|
|
7566
|
-
const at = opts?.at;
|
|
7567
|
-
|
|
7947
|
+
const at = opts?.at ?? [];
|
|
7948
|
+
const under = opts?.parent ?? this.behavior.node;
|
|
7949
|
+
if (hasPosition$1(clone) && (at.length > 0 || under !== this.behavior.node)) {
|
|
7950
|
+
const origin = under === this.behavior.node || !hasPosition$1(this.behavior.node) ? [] : sub(worldPosition(this.behavior.node), worldPosition(under));
|
|
7951
|
+
clone.position = add(origin, at.length > 0 ? at : zeros(origin.length || clone.position.length));
|
|
7952
|
+
}
|
|
7568
7953
|
if (typeof clone.visible === "boolean") clone.visible = true;
|
|
7569
7954
|
(opts?.parent ?? this.behavior.node).addChild(clone);
|
|
7570
7955
|
return clone;
|
|
@@ -7593,6 +7978,15 @@ function absolutePrefabPath(node, prefab) {
|
|
|
7593
7978
|
}
|
|
7594
7979
|
return parts.join("/");
|
|
7595
7980
|
}
|
|
7981
|
+
function sub(a, b) {
|
|
7982
|
+
const n = Math.max(a.length, b.length);
|
|
7983
|
+
const out = [];
|
|
7984
|
+
for (let i = 0; i < n; i++) out.push((a[i] ?? 0) - (b[i] ?? 0));
|
|
7985
|
+
return out;
|
|
7986
|
+
}
|
|
7987
|
+
function zeros(n) {
|
|
7988
|
+
return Array.from({ length: n }, () => 0);
|
|
7989
|
+
}
|
|
7596
7990
|
//#endregion
|
|
7597
7991
|
//#region src/gameplay/spawner.ts
|
|
7598
7992
|
/**
|
|
@@ -7626,7 +8020,17 @@ var Spawner = class extends Behavior {
|
|
|
7626
8020
|
/** Begin ticking at ready. */
|
|
7627
8021
|
autoStart: { default: true },
|
|
7628
8022
|
/** Total LIFETIME spawns (0 = infinite). */
|
|
7629
|
-
total: { default: 0 }
|
|
8023
|
+
total: { default: 0 },
|
|
8024
|
+
/**
|
|
8025
|
+
* Where clones live: a node path, or '' for under the spawner itself. Clones
|
|
8026
|
+
* of a spawner that MOVES ride along with it when they are its children —
|
|
8027
|
+
* a gate a director slid to a new edge before each spawn dragged every
|
|
8028
|
+
* enemy it had made — so a container makes the spawner just a place.
|
|
8029
|
+
*/
|
|
8030
|
+
into: {
|
|
8031
|
+
default: "",
|
|
8032
|
+
nodePath: true
|
|
8033
|
+
}
|
|
7630
8034
|
};
|
|
7631
8035
|
static signals = ["spawned", "finished"];
|
|
7632
8036
|
prefab = "";
|
|
@@ -7635,6 +8039,7 @@ var Spawner = class extends Behavior {
|
|
|
7635
8039
|
at = [];
|
|
7636
8040
|
autoStart = true;
|
|
7637
8041
|
total = 0;
|
|
8042
|
+
into = "";
|
|
7638
8043
|
source = new SpawnSource(this);
|
|
7639
8044
|
template;
|
|
7640
8045
|
live = /* @__PURE__ */ new Set();
|
|
@@ -7657,6 +8062,7 @@ var Spawner = class extends Behavior {
|
|
|
7657
8062
|
onReady() {
|
|
7658
8063
|
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
8064
|
this.template = this.source.resolveTemplate(this.prefab);
|
|
8065
|
+
if (this.into !== "") this.node.getNode(this.into);
|
|
7660
8066
|
this.running = this.autoStart;
|
|
7661
8067
|
this.timer = this.interval;
|
|
7662
8068
|
}
|
|
@@ -7674,7 +8080,11 @@ var Spawner = class extends Behavior {
|
|
|
7674
8080
|
this.prune();
|
|
7675
8081
|
if (this.done) return null;
|
|
7676
8082
|
if (this.max > 0 && this.live.size >= this.max) return null;
|
|
7677
|
-
const
|
|
8083
|
+
const parent = this.into === "" ? void 0 : this.node.getNode(this.into);
|
|
8084
|
+
const clone = this.source.spawn(this.template, this.at.length > 0 || parent ? {
|
|
8085
|
+
at: this.at,
|
|
8086
|
+
parent
|
|
8087
|
+
} : void 0);
|
|
7678
8088
|
this.live.add(clone);
|
|
7679
8089
|
this.spawnedCount += 1;
|
|
7680
8090
|
this.emit("spawned", clone);
|
|
@@ -7698,6 +8108,140 @@ var Spawner = class extends Behavior {
|
|
|
7698
8108
|
}
|
|
7699
8109
|
};
|
|
7700
8110
|
//#endregion
|
|
8111
|
+
//#region src/gameplay/turret.ts
|
|
8112
|
+
/**
|
|
8113
|
+
* The oldest verb in games: pick the nearest live member of `targetGroup`
|
|
8114
|
+
* within `range` and hit it every `cooldown` — a defense tower, a ship's gun,
|
|
8115
|
+
* a sentry, a trooper who fights what he meets.
|
|
8116
|
+
*
|
|
8117
|
+
* Two games in a row wrote these forty lines before it existed (an RTS's
|
|
8118
|
+
* trooper, a tower defense's tower): the engine had `Chase` for going TO a
|
|
8119
|
+
* target and `Projectile` for the bullet, and nothing that decides to shoot.
|
|
8120
|
+
*
|
|
8121
|
+
* - **Hitscan** by default: the target's `Health` (on it or a child, as
|
|
8122
|
+
* `DamageOnContact` finds it) takes `damage(n)`.
|
|
8123
|
+
* - **Or a clone**: name a `shelf` (a `PrefabShelf`) and a `prefab` on it; each
|
|
8124
|
+
* shot takes one, puts it at the turret plus `muzzle`, points its
|
|
8125
|
+
* `Projectile` at the target and adds it under `into` (the turret's parent
|
|
8126
|
+
* when empty). The clone does the hurting then — give it `DamageOnContact`.
|
|
8127
|
+
* - `facePath` turns a barrel toward the target at `turnSpeed`.
|
|
8128
|
+
*
|
|
8129
|
+
* Signals: `acquired(target)` when something comes in range, `fired(target)`
|
|
8130
|
+
* per shot, `lost()` when nothing live is in range any more.
|
|
8131
|
+
*/
|
|
8132
|
+
var Turret = class extends Behavior {
|
|
8133
|
+
static props = {
|
|
8134
|
+
/** Who it shoots: the nearest live member of this group. */
|
|
8135
|
+
targetGroup: { default: "enemy" },
|
|
8136
|
+
/** How far it reaches, in the scene's units. */
|
|
8137
|
+
range: {
|
|
8138
|
+
default: 6,
|
|
8139
|
+
range: { min: 0 }
|
|
8140
|
+
},
|
|
8141
|
+
/** Hitscan damage per shot (ignored when a `prefab` does the hurting). */
|
|
8142
|
+
damage: { default: 10 },
|
|
8143
|
+
/** Seconds between shots. */
|
|
8144
|
+
cooldown: {
|
|
8145
|
+
default: .6,
|
|
8146
|
+
range: { min: 0 }
|
|
8147
|
+
},
|
|
8148
|
+
/** A `PrefabShelf` to take each shot from (empty = hitscan). */
|
|
8149
|
+
shelf: {
|
|
8150
|
+
default: "",
|
|
8151
|
+
nodePath: true
|
|
8152
|
+
},
|
|
8153
|
+
/** The template on that shelf. */
|
|
8154
|
+
prefab: { default: "" },
|
|
8155
|
+
/** Where clones start, relative to the turret. */
|
|
8156
|
+
muzzle: { default: [] },
|
|
8157
|
+
/** Where clones go; empty = the turret's parent. */
|
|
8158
|
+
into: {
|
|
8159
|
+
default: "",
|
|
8160
|
+
nodePath: true
|
|
8161
|
+
},
|
|
8162
|
+
...FACING_PROPS
|
|
8163
|
+
};
|
|
8164
|
+
static signals = [
|
|
8165
|
+
"acquired",
|
|
8166
|
+
"fired",
|
|
8167
|
+
"lost"
|
|
8168
|
+
];
|
|
8169
|
+
targetGroup = "enemy";
|
|
8170
|
+
range = 6;
|
|
8171
|
+
damage = 10;
|
|
8172
|
+
cooldown = .6;
|
|
8173
|
+
shelf = "";
|
|
8174
|
+
prefab = "";
|
|
8175
|
+
muzzle = [];
|
|
8176
|
+
into = "";
|
|
8177
|
+
facePath = "";
|
|
8178
|
+
turnSpeed = 100;
|
|
8179
|
+
/** What it is shooting at right now, or null. */
|
|
8180
|
+
target = null;
|
|
8181
|
+
/** Shots fired so far. */
|
|
8182
|
+
shots = 0;
|
|
8183
|
+
wait = 0;
|
|
8184
|
+
onReady() {
|
|
8185
|
+
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.`, {
|
|
8186
|
+
prop: this.shelf === "" ? "shelf" : "prefab",
|
|
8187
|
+
path: this.node.getPath()
|
|
8188
|
+
});
|
|
8189
|
+
}
|
|
8190
|
+
update(dt) {
|
|
8191
|
+
this.wait = Math.max(0, this.wait - dt);
|
|
8192
|
+
if (!hasPosition$1(this.node)) return;
|
|
8193
|
+
const here = worldPosition(this.node);
|
|
8194
|
+
let best = null;
|
|
8195
|
+
let bestD = this.range;
|
|
8196
|
+
for (const n of this.node.tree?.getNodesInGroup(this.targetGroup) ?? []) {
|
|
8197
|
+
if (n === this.node || !hasPosition$1(n)) continue;
|
|
8198
|
+
if (findHealth(n)?.health.isDead) continue;
|
|
8199
|
+
const d = distance$1(here, worldPosition(n));
|
|
8200
|
+
if (d <= bestD) {
|
|
8201
|
+
best = n;
|
|
8202
|
+
bestD = d;
|
|
8203
|
+
}
|
|
8204
|
+
}
|
|
8205
|
+
if (best !== this.target) {
|
|
8206
|
+
const had = this.target;
|
|
8207
|
+
this.target = best;
|
|
8208
|
+
if (best) this.emit("acquired", best);
|
|
8209
|
+
else if (had) this.emit("lost");
|
|
8210
|
+
}
|
|
8211
|
+
if (!best) return;
|
|
8212
|
+
const dir = worldPosition(best).map((v, i) => v - (here[i] ?? 0));
|
|
8213
|
+
if (this.facePath !== "") {
|
|
8214
|
+
const barrel = this.node.getNodeOrNull(this.facePath);
|
|
8215
|
+
if (barrel) faceTravel(barrel, dir, dt, this.turnSpeed);
|
|
8216
|
+
}
|
|
8217
|
+
if (this.wait > 0) return;
|
|
8218
|
+
this.wait = this.cooldown;
|
|
8219
|
+
this.shots += 1;
|
|
8220
|
+
if (this.shelf !== "") this.launch(best, here, dir);
|
|
8221
|
+
else findHealth(best)?.health.damage(this.damage);
|
|
8222
|
+
this.emit("fired", best);
|
|
8223
|
+
}
|
|
8224
|
+
/** One clone from the shelf, at the muzzle, aimed at the target, into the container. */
|
|
8225
|
+
launch(target, here, dir) {
|
|
8226
|
+
const shelf = this.node.getNodeOrNull(this.shelf)?.behavior;
|
|
8227
|
+
if (!shelf || typeof shelf.make !== "function") throw new IncantoError("NODE_NOT_FOUND", `Turret on '${this.node.getPath()}': "shelf" '${this.shelf}' is not a PrefabShelf.`, {
|
|
8228
|
+
prop: "shelf",
|
|
8229
|
+
path: this.node.getPath()
|
|
8230
|
+
});
|
|
8231
|
+
const clone = shelf.make(this.prefab);
|
|
8232
|
+
if (!hasPosition$1(clone)) return;
|
|
8233
|
+
const container = this.into !== "" ? this.node.getNodeOrNull(this.into) : this.node.parent;
|
|
8234
|
+
if (!container) return;
|
|
8235
|
+
const len = Math.hypot(...dir) || 1;
|
|
8236
|
+
const unit = dir.map((v) => v / len);
|
|
8237
|
+
const containerAt = hasPosition$1(container) ? worldPosition(container) : here.map(() => 0);
|
|
8238
|
+
clone.position = here.map((v, i) => v + (this.muzzle[i] ?? 0) - (containerAt[i] ?? 0));
|
|
8239
|
+
const shot = clone.behavior;
|
|
8240
|
+
if (shot && "direction" in shot) shot.direction = unit;
|
|
8241
|
+
container.addChild(clone);
|
|
8242
|
+
}
|
|
8243
|
+
};
|
|
8244
|
+
//#endregion
|
|
7701
8245
|
//#region src/gameplay/wander.ts
|
|
7702
8246
|
/**
|
|
7703
8247
|
* Aimless roaming inside a circle around the spawn point — idle critters,
|
|
@@ -7793,6 +8337,14 @@ var WaveSpawner = class extends Behavior {
|
|
|
7793
8337
|
parsed = [];
|
|
7794
8338
|
live = /* @__PURE__ */ new Set();
|
|
7795
8339
|
waveIndex = -1;
|
|
8340
|
+
/** The wave now running or being cleared, 1-based — what a HUD prints as `wave 2 / 5`. `0` before the first. */
|
|
8341
|
+
get wave() {
|
|
8342
|
+
return this.waveIndex < 0 ? 0 : Math.min(this.parsed.length, this.waveIndex + 1);
|
|
8343
|
+
}
|
|
8344
|
+
/** How many waves were declared. */
|
|
8345
|
+
get waveCount() {
|
|
8346
|
+
return this.parsed.length;
|
|
8347
|
+
}
|
|
7796
8348
|
phase = "idle";
|
|
7797
8349
|
timer = 0;
|
|
7798
8350
|
spawnedThisWave = 0;
|
|
@@ -8105,6 +8657,8 @@ const GAMEPLAY_BEHAVIORS = {
|
|
|
8105
8657
|
PathFollow,
|
|
8106
8658
|
Health,
|
|
8107
8659
|
Lifetime,
|
|
8660
|
+
Checkpoint,
|
|
8661
|
+
Turret,
|
|
8108
8662
|
ScoreKeeper,
|
|
8109
8663
|
Currency,
|
|
8110
8664
|
Pickup,
|
|
@@ -8138,4 +8692,4 @@ function registerGameplayBehaviors(opts) {
|
|
|
8138
8692
|
for (const [name, ctor] of Object.entries(GAMEPLAY_BEHAVIORS)) registerBehavior(name, ctor, { replace });
|
|
8139
8693
|
}
|
|
8140
8694
|
//#endregion
|
|
8141
|
-
export {
|
|
8695
|
+
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 };
|