incanto 0.64.1 → 0.66.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/dist/2d.d.ts +38 -3
- package/dist/2d.js +3 -3
- package/dist/3d.d.ts +130 -3
- package/dist/3d.js +5 -5
- package/dist/{audio-player-D5GJgb_x.d.ts → audio-player-_UAcHxnC.d.ts} +1 -1
- package/dist/{behavior-DsgayMsH.d.ts → behavior-BXNLfIJk.d.ts} +128 -0
- package/dist/{create-game-B746rYbr.js → create-game-DYJCIzO0.js} +67 -6
- package/dist/{create-game-B1KM6dA6.js → create-game-Dd7H4bJV.js} +6 -6
- package/dist/debug.d.ts +1 -1
- package/dist/{duplicate-BOOKmkQ7.js → duplicate-CqSAtdrh.js} +1 -1
- package/dist/{environment-presets-D2vzw583.js → environment-presets-BlPsEmq6.js} +70 -6
- package/dist/{gameplay-CLgFdkh5.js → gameplay-D1RADWu3.js} +374 -18
- package/dist/gameplay.d.ts +94 -3
- package/dist/gameplay.js +2 -2
- package/dist/index.d.ts +4 -4
- package/dist/index.js +5 -5
- package/dist/{loader-DolLJWJn.d.ts → loader-Cga7FVP4.d.ts} +1 -1
- package/dist/{loader-zDynoew_.js → loader-lQDCwNag.js} +59 -3
- package/dist/net.d.ts +2 -2
- package/dist/net.js +1 -1
- package/dist/{physics-2d-BPJcJRRP.js → physics-2d-Cji5A6sX.js} +32 -4
- package/dist/{physics-3d-BOO58xzw.js → physics-3d-BkHjwJgI.js} +49 -5
- package/dist/{quiet-rapier-BAJ4K94N.js → quiet-rapier-C6fW4zcW.js} +14 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{register-Btm7_Emq.js → register-BmuqYTiY.js} +44 -1
- package/dist/{register-dGsbnJ87.js → register-D0CxCveZ.js} +38 -4
- package/dist/{replay-C5x2vPF5.d.ts → replay-BHoB6fCU.d.ts} +1 -1
- package/dist/{replay-DKRmiVjk.js → replay-IsZbNu6d.js} +2 -2
- package/dist/{split-screen-Dx0LvzqS.js → split-screen-D_i7GRcY.js} +2 -2
- package/dist/{split-screen-D7OopelJ.d.ts → split-screen-eJUFjhi1.d.ts} +2 -2
- package/dist/{src-CNS2xQB1.js → src-BTLbXFPZ.js} +1 -1
- package/dist/{teardown-BwhkcNt8.js → teardown-byR9USax.js} +1 -1
- package/dist/{test-Ccob3X5i.js → test-DgrD0jHD.js} +13 -13
- package/dist/test.d.ts +4 -4
- package/dist/test.js +2 -2
- package/dist/vite.js +2 -2
- package/editor/assets/{agent8-Di-UEn5O.js → agent8-9N-Pd_YS.js} +1 -1
- package/editor/assets/{debug-DWztJ5_y.js → debug-CkbJICYp.js} +1 -1
- package/editor/assets/{index-fct4H89G.js → index-CeDhIPTC.js} +53 -53
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/schemas/scene.schema.json +19 -0
- package/skills/incanto-3d-character.md +11 -1
- package/skills/incanto-behaviors-and-scripts.md +21 -0
- package/skills/incanto-gameplay-behaviors.md +177 -9
- package/skills/incanto-node-reference.md +59 -1
- package/skills/incanto-physics-and-input.md +84 -1
- package/skills/incanto-web-integration.md +24 -3
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/platformer-2d/package.json +1 -1
- package/templates-app/star-survivor/package.json +1 -1
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/village-quest-3d/package.json +1 -1
- package/templates-app/beacon-isle-3d/coverage.json +0 -9
- package/templates-app/platformer-2d/coverage.json +0 -5
- package/templates-app/star-survivor/coverage.json +0 -5
- package/templates-app/tps-3d/coverage.json +0 -5
- package/templates-app/village-quest-3d/coverage.json +0 -9
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { O as Node, P as Behavior, _ as SaveSlots, k as diagnose, n as loadScene, w as effectiveOrder, z as registerBehavior } from "./loader-
|
|
1
|
+
import { O as Node, P as Behavior, _ as SaveSlots, k as diagnose, n as loadScene, w as effectiveOrder, z as registerBehavior } from "./loader-lQDCwNag.js";
|
|
2
2
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
3
3
|
import { t as jsonClone } from "./json-CwwhxQgb.js";
|
|
4
|
-
import { t as duplicateNode } from "./duplicate-
|
|
4
|
+
import { t as duplicateNode } from "./duplicate-CqSAtdrh.js";
|
|
5
5
|
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";
|
|
6
6
|
//#region src/3d/frustum.ts
|
|
7
7
|
const scratchFrustum = new Frustum();
|
|
@@ -2534,6 +2534,36 @@ var StaticBody3D = class extends PhysicsBody3D {
|
|
|
2534
2534
|
/** Sensor volume emitting `triggerEnter(other)` / `triggerExit(other)`. */
|
|
2535
2535
|
var Area3D = class extends PhysicsBody3D {
|
|
2536
2536
|
static typeName = "Area3D";
|
|
2537
|
+
/**
|
|
2538
|
+
* Who is inside RIGHT NOW — the standing answer behind the enter/exit pair.
|
|
2539
|
+
*
|
|
2540
|
+
* Enter and exit tell you about crossings; a pressure plate, a capture point,
|
|
2541
|
+
* a "how many enemies are in the blast" count and a shop trigger all want
|
|
2542
|
+
* OCCUPANCY. Without this every one of them kept a private `Set` fed by the
|
|
2543
|
+
* two signals, and had to guard it, because a body that is teleported or
|
|
2544
|
+
* freed inside a sensor does not reliably announce its exit. Physics already
|
|
2545
|
+
* knows; now it says.
|
|
2546
|
+
*
|
|
2547
|
+
* ```ts
|
|
2548
|
+
* const load = this.area.overlapping()
|
|
2549
|
+
* .reduce((kg, b) => kg + ((b as RigidBody3D).mass ?? 0), 0);
|
|
2550
|
+
* door.open = load >= 60;
|
|
2551
|
+
* ```
|
|
2552
|
+
*
|
|
2553
|
+
* It reports everything the solver says is inside, STATIC WORLD INCLUDED — a
|
|
2554
|
+
* plate laid into the floor genuinely contains the floor. Pass a group to ask
|
|
2555
|
+
* the question you usually mean:
|
|
2556
|
+
*
|
|
2557
|
+
* ```ts
|
|
2558
|
+
* area.overlapping('crate') // only things tagged `crate`
|
|
2559
|
+
* ```
|
|
2560
|
+
*
|
|
2561
|
+
* Empty in a scene with no physics world.
|
|
2562
|
+
*/
|
|
2563
|
+
overlapping(group) {
|
|
2564
|
+
const all = this._physics?.overlapping(this) ?? [];
|
|
2565
|
+
return group === void 0 ? all : all.filter((n) => n.isInGroup(group));
|
|
2566
|
+
}
|
|
2537
2567
|
};
|
|
2538
2568
|
/** Dynamic simulated body. */
|
|
2539
2569
|
var RigidBody3D = class extends PhysicsBody3D {
|
|
@@ -2548,6 +2578,11 @@ var RigidBody3D = class extends PhysicsBody3D {
|
|
|
2548
2578
|
0,
|
|
2549
2579
|
0,
|
|
2550
2580
|
0
|
|
2581
|
+
] },
|
|
2582
|
+
angularVelocity: { default: [
|
|
2583
|
+
0,
|
|
2584
|
+
0,
|
|
2585
|
+
0
|
|
2551
2586
|
] }
|
|
2552
2587
|
};
|
|
2553
2588
|
mass = 1;
|
|
@@ -2561,6 +2596,18 @@ var RigidBody3D = class extends PhysicsBody3D {
|
|
|
2561
2596
|
0,
|
|
2562
2597
|
0
|
|
2563
2598
|
];
|
|
2599
|
+
/**
|
|
2600
|
+
* rad/s about each world axis. Read back every step; write to spin.
|
|
2601
|
+
*
|
|
2602
|
+
* A puzzle builder could neither launch a spinning body nor read how fast a
|
|
2603
|
+
* lever was swinging — only its angle, differenced by hand — because this had
|
|
2604
|
+
* no counterpart to `linearVelocity`. `fixedRotation` pins it at zero.
|
|
2605
|
+
*/
|
|
2606
|
+
angularVelocity = [
|
|
2607
|
+
0,
|
|
2608
|
+
0,
|
|
2609
|
+
0
|
|
2610
|
+
];
|
|
2564
2611
|
/** @internal set by Physics3D */
|
|
2565
2612
|
_physics3d = null;
|
|
2566
2613
|
/** World-space impulse (kg·m/s) — the floating-capsule controller's verb. */
|
|
@@ -4360,6 +4407,65 @@ function firstWater(node) {
|
|
|
4360
4407
|
return null;
|
|
4361
4408
|
}
|
|
4362
4409
|
//#endregion
|
|
4410
|
+
//#region src/gameplay/face-travel.ts
|
|
4411
|
+
const RAD2DEG = 180 / Math.PI;
|
|
4412
|
+
/**
|
|
4413
|
+
* Turn a node to face the direction something is travelling.
|
|
4414
|
+
*
|
|
4415
|
+
* `CharacterController3D` does this for the PLAYER (`skinPath`/`turnSpeed`) and
|
|
4416
|
+
* nothing did it for anyone else — so every game with a patrolling or chasing
|
|
4417
|
+
* enemy hand-wrote the same six lines, against a formula the docs simultaneously
|
|
4418
|
+
* teach and warn about: `incanto-3d-character.md` calls the wrong version of it
|
|
4419
|
+
* "the #1 recurring bug" (a character that runs BACKWARDS, showing you its back
|
|
4420
|
+
* while charging at you). This is the right version, once, in the engine.
|
|
4421
|
+
*
|
|
4422
|
+
* The rule, for 3D: the model is **+Z-FORWARD**, so `yaw = atan2(dx, dz)` — x
|
|
4423
|
+
* and z, never y, and NO 180° added. The 180° is only the at-rest mount for a
|
|
4424
|
+
* behind-the-shoulder camera, not a facing formula.
|
|
4425
|
+
*
|
|
4426
|
+
* In 2D `rotation` is a scalar and the plane is x/y, so it is `atan2(dy, dx)` —
|
|
4427
|
+
* correct for the top-down games that use `Patrol`/`Chase`. A platformer faces
|
|
4428
|
+
* with `flipX` instead and should not set `facePath`.
|
|
4429
|
+
*
|
|
4430
|
+
* `turnSpeed` is rad/s, the same units and the same shortest-arc, rate-limited
|
|
4431
|
+
* step the controller uses; at the default 100 it is effectively an instant
|
|
4432
|
+
* snap. A zero-length direction leaves the rotation alone — a node that is not
|
|
4433
|
+
* moving has nothing to face.
|
|
4434
|
+
*/
|
|
4435
|
+
function faceTravel(target, dir, dt, turnSpeed) {
|
|
4436
|
+
const spatial = target;
|
|
4437
|
+
const rot = spatial.rotation;
|
|
4438
|
+
const is3d = Array.isArray(rot);
|
|
4439
|
+
const [a, b] = is3d ? [dir[0] ?? 0, dir[2] ?? 0] : [dir[0] ?? 0, dir[1] ?? 0];
|
|
4440
|
+
if (Math.hypot(a, b) < 1e-6) return;
|
|
4441
|
+
const want = (is3d ? Math.atan2(a, b) : Math.atan2(b, a)) * RAD2DEG;
|
|
4442
|
+
const current = is3d ? rot[1] ?? 0 : rot ?? 0;
|
|
4443
|
+
let delta = (want - current + 540) % 360 - 180;
|
|
4444
|
+
const step = turnSpeed * dt * RAD2DEG;
|
|
4445
|
+
if (Math.abs(delta) > step) delta = Math.sign(delta) * step;
|
|
4446
|
+
const next = current + delta;
|
|
4447
|
+
if (is3d) spatial.rotation = [
|
|
4448
|
+
rot[0] ?? 0,
|
|
4449
|
+
next,
|
|
4450
|
+
rot[2] ?? 0
|
|
4451
|
+
];
|
|
4452
|
+
else spatial.rotation = next;
|
|
4453
|
+
}
|
|
4454
|
+
/** The shared `facePath`/`turnSpeed` prop pair for the AI movement behaviors. */
|
|
4455
|
+
const FACING_PROPS = {
|
|
4456
|
+
/**
|
|
4457
|
+
* Node path of the thing to TURN toward the travel direction — usually the
|
|
4458
|
+
* `ModelInstance3D` skin ('../Skin'). Empty (default) turns nothing, so a
|
|
4459
|
+
* scene that never asks is unchanged.
|
|
4460
|
+
*/
|
|
4461
|
+
facePath: {
|
|
4462
|
+
default: "",
|
|
4463
|
+
nodePath: true
|
|
4464
|
+
},
|
|
4465
|
+
/** Turn rate in rad/s; 100 (default) is an instant snap, like the controller. */
|
|
4466
|
+
turnSpeed: { default: 100 }
|
|
4467
|
+
};
|
|
4468
|
+
//#endregion
|
|
4363
4469
|
//#region src/gameplay/move-body.ts
|
|
4364
4470
|
function moveBody(node, to, dt) {
|
|
4365
4471
|
const body = node;
|
|
@@ -4521,6 +4627,8 @@ function localFromWorld(node, world) {
|
|
|
4521
4627
|
* it leaves), so wire it to an attack.
|
|
4522
4628
|
* - `loseRange` (>0) gives up the chase when the target gets that far away,
|
|
4523
4629
|
* emitting `lostTarget` once.
|
|
4630
|
+
* - `facePath`/`turnSpeed` turn a skin toward the direction of travel — the job
|
|
4631
|
+
* `CharacterController3D` does for the player and nobody did for an NPC.
|
|
4524
4632
|
* - `moveParent` (default false) moves the parent node instead of this one.
|
|
4525
4633
|
* A node carries ONE behavior, so an enemy whose ROOT must hold `Health`
|
|
4526
4634
|
* (e.g. for `Health.freeOnDeath` clone-safe cleanup) puts `Chase` on a CHILD
|
|
@@ -4542,7 +4650,8 @@ var Chase = class extends Behavior {
|
|
|
4542
4650
|
/** Give up (emit lostTarget) beyond this distance (0 = never lose). */
|
|
4543
4651
|
loseRange: { default: 0 },
|
|
4544
4652
|
/** Move the parent node instead of this one (AI-on-a-child pattern). */
|
|
4545
|
-
moveParent: { default: false }
|
|
4653
|
+
moveParent: { default: false },
|
|
4654
|
+
...FACING_PROPS
|
|
4546
4655
|
};
|
|
4547
4656
|
static signals = ["reachedTarget", "lostTarget"];
|
|
4548
4657
|
target = "";
|
|
@@ -4550,6 +4659,8 @@ var Chase = class extends Behavior {
|
|
|
4550
4659
|
stopRange = 0;
|
|
4551
4660
|
loseRange = 0;
|
|
4552
4661
|
moveParent = false;
|
|
4662
|
+
facePath = "";
|
|
4663
|
+
turnSpeed = 100;
|
|
4553
4664
|
inRange = false;
|
|
4554
4665
|
lost = false;
|
|
4555
4666
|
onReady() {
|
|
@@ -4586,8 +4697,13 @@ var Chase = class extends Behavior {
|
|
|
4586
4697
|
}
|
|
4587
4698
|
this.inRange = false;
|
|
4588
4699
|
const step = Math.min(this.speed * dt, this.stopRange > 0 ? d - this.stopRange : d);
|
|
4700
|
+
const from = [...mover.position];
|
|
4589
4701
|
const { position } = moveToward(mover.position, target.position, Math.max(0, step));
|
|
4590
4702
|
moveBody(mover, position, dt);
|
|
4703
|
+
if (this.facePath !== "") {
|
|
4704
|
+
const skin = this.node.getNodeOrNull(this.facePath);
|
|
4705
|
+
if (skin) faceTravel(skin, position.map((v, i) => v - (from[i] ?? 0)), dt, this.turnSpeed);
|
|
4706
|
+
}
|
|
4591
4707
|
}
|
|
4592
4708
|
};
|
|
4593
4709
|
//#endregion
|
|
@@ -4621,9 +4737,7 @@ var Clickable = class extends Behavior {
|
|
|
4621
4737
|
/** Mouse button: 0 left, 1 middle, 2 right. */
|
|
4622
4738
|
button: { default: 0 },
|
|
4623
4739
|
/** Ignore clicks further away than this (0 = any distance). */
|
|
4624
|
-
maxDistance: { default: 0 }
|
|
4625
|
-
/** Stop responding without removing the behavior. */
|
|
4626
|
-
enabled: { default: true }
|
|
4740
|
+
maxDistance: { default: 0 }
|
|
4627
4741
|
};
|
|
4628
4742
|
static signals = [
|
|
4629
4743
|
"clicked",
|
|
@@ -4632,20 +4746,19 @@ var Clickable = class extends Behavior {
|
|
|
4632
4746
|
];
|
|
4633
4747
|
button = 0;
|
|
4634
4748
|
maxDistance = 0;
|
|
4635
|
-
enabled = true;
|
|
4636
4749
|
/** True while the cursor is over this node — read it to tint a hover state. */
|
|
4637
4750
|
hovering = false;
|
|
4638
4751
|
pressedHere = false;
|
|
4639
4752
|
update() {
|
|
4640
4753
|
const engine = this.node.tree?.engine;
|
|
4641
|
-
if (!engine
|
|
4754
|
+
if (!engine) return;
|
|
4642
4755
|
const picker = engine.picker;
|
|
4643
4756
|
const pointer = engine.input.pointerPosition();
|
|
4644
4757
|
if (!picker || !pointer) {
|
|
4645
4758
|
if (this.hovering) this.setHover(false);
|
|
4646
4759
|
return;
|
|
4647
4760
|
}
|
|
4648
|
-
const hit =
|
|
4761
|
+
const hit = engine.pickAt(pointer.x, pointer.y);
|
|
4649
4762
|
const over = hit !== null && this.isSelfOrDescendant(hit) && this.withinRange(hit);
|
|
4650
4763
|
if (over !== this.hovering) this.setHover(over);
|
|
4651
4764
|
if (engine.input.mouseJustPressed(this.button)) this.pressedHere = over;
|
|
@@ -4715,6 +4828,104 @@ group: { default: "player" } };
|
|
|
4715
4828
|
}
|
|
4716
4829
|
};
|
|
4717
4830
|
//#endregion
|
|
4831
|
+
//#region src/gameplay/currency.ts
|
|
4832
|
+
/**
|
|
4833
|
+
* Money. Earn it, spend it, and refuse what you cannot afford.
|
|
4834
|
+
*
|
|
4835
|
+
* `ScoreKeeper` is the closest thing the library had and it is a SCORE: it
|
|
4836
|
+
* counts up, it has no spend, no affordability question, and its counter is
|
|
4837
|
+
* already wired to the win condition. So a tower defense built on 0.65.0 wrote
|
|
4838
|
+
* its own wallet — and so does every shop, every build mode, every upgrade tree
|
|
4839
|
+
* and every economy game.
|
|
4840
|
+
*
|
|
4841
|
+
* `spend` is the whole point: it returns whether it went through, and emits
|
|
4842
|
+
* `refused` when it did not, so "you cannot afford that" is a wire rather than
|
|
4843
|
+
* a comparison every caller repeats and one caller forgets.
|
|
4844
|
+
*/
|
|
4845
|
+
var Currency = class extends Behavior {
|
|
4846
|
+
static props = {
|
|
4847
|
+
/** What you start with. */
|
|
4848
|
+
amount: { default: 0 },
|
|
4849
|
+
/** Ceiling (0 = none). */
|
|
4850
|
+
max: { default: 0 }
|
|
4851
|
+
};
|
|
4852
|
+
static signals = [
|
|
4853
|
+
"changed",
|
|
4854
|
+
"earned",
|
|
4855
|
+
"spent",
|
|
4856
|
+
"refused"
|
|
4857
|
+
];
|
|
4858
|
+
amount = 0;
|
|
4859
|
+
max = 0;
|
|
4860
|
+
/** Can this be paid right now? */
|
|
4861
|
+
canAfford(cost) {
|
|
4862
|
+
return cost <= this.amount;
|
|
4863
|
+
}
|
|
4864
|
+
/** Pay `cost` if it is there. Returns whether it went through. */
|
|
4865
|
+
spend(cost) {
|
|
4866
|
+
if (!this.canAfford(cost)) {
|
|
4867
|
+
this.emit("refused", cost);
|
|
4868
|
+
return false;
|
|
4869
|
+
}
|
|
4870
|
+
this.amount -= cost;
|
|
4871
|
+
this.emit("spent", cost);
|
|
4872
|
+
this.emit("changed", this.amount);
|
|
4873
|
+
return true;
|
|
4874
|
+
}
|
|
4875
|
+
/** Take payment in. */
|
|
4876
|
+
earn(gain) {
|
|
4877
|
+
this.amount = this.max > 0 ? Math.min(this.max, this.amount + gain) : this.amount + gain;
|
|
4878
|
+
this.emit("earned", gain);
|
|
4879
|
+
this.emit("changed", this.amount);
|
|
4880
|
+
}
|
|
4881
|
+
/** Set it outright (a shop that grants, a cheat, a restore). */
|
|
4882
|
+
setAmount(next) {
|
|
4883
|
+
this.amount = this.max > 0 ? Math.min(this.max, next) : next;
|
|
4884
|
+
this.emit("changed", this.amount);
|
|
4885
|
+
}
|
|
4886
|
+
serialize() {
|
|
4887
|
+
return { amount: this.amount };
|
|
4888
|
+
}
|
|
4889
|
+
deserialize(data) {
|
|
4890
|
+
const d = data;
|
|
4891
|
+
if (typeof d.amount === "number") this.amount = d.amount;
|
|
4892
|
+
}
|
|
4893
|
+
};
|
|
4894
|
+
//#endregion
|
|
4895
|
+
//#region src/gameplay/free-target.ts
|
|
4896
|
+
/**
|
|
4897
|
+
* Free the ENTITY a despawning behavior belongs to, not just the node it sits on.
|
|
4898
|
+
*
|
|
4899
|
+
* `Lifetime` and `DamageOnContact` both remove their own node, which is right
|
|
4900
|
+
* when the behavior IS the thing. It is wrong for the composition the docs
|
|
4901
|
+
* teach: a node carries ONE script, so a bullet that both hurts and expires must
|
|
4902
|
+
* put those on CHILD nodes — and then each child quietly removed itself while
|
|
4903
|
+
* the bullet flew on forever, leaking a node per shot.
|
|
4904
|
+
*
|
|
4905
|
+
* `freeParent: true` is the other half. It is the same escape hatch the movement
|
|
4906
|
+
* behaviors call `moveParent`, named the same way for the same reason.
|
|
4907
|
+
*/
|
|
4908
|
+
function freeOwner(behavior, freeParent, who) {
|
|
4909
|
+
if (!freeParent) {
|
|
4910
|
+
behavior.node.queueFree();
|
|
4911
|
+
return;
|
|
4912
|
+
}
|
|
4913
|
+
const parent = behavior.node.parent;
|
|
4914
|
+
if (!parent) {
|
|
4915
|
+
diagnose(behavior.node.tree?.engine ?? null, "error", `[incanto] ${who} on '${behavior.node.getPath()}': "freeParent" is set and this node has no parent to free — it is the scene root. Nothing was removed. Drop the prop, or move this behavior onto a child of the thing you meant to despawn.`);
|
|
4916
|
+
return;
|
|
4917
|
+
}
|
|
4918
|
+
parent.queueFree();
|
|
4919
|
+
}
|
|
4920
|
+
/** The shared prop, so both behaviors declare it identically. */
|
|
4921
|
+
const FREE_PARENT_PROP = {
|
|
4922
|
+
/**
|
|
4923
|
+
* Free this node's PARENT instead of this node — the composition where a
|
|
4924
|
+
* bullet carries `Projectile` and its children carry the damage and the
|
|
4925
|
+
* timer. Without it each child removes only itself and the bullet leaks.
|
|
4926
|
+
*/
|
|
4927
|
+
freeParent: { default: false } };
|
|
4928
|
+
//#endregion
|
|
4718
4929
|
//#region src/gameplay/health.ts
|
|
4719
4930
|
/**
|
|
4720
4931
|
* Hit points with regeneration and post-hit invulnerability (i-frames) —
|
|
@@ -4905,7 +5116,9 @@ function onTriggerEnter(behavior, fn) {
|
|
|
4905
5116
|
* - `repeatEvery` (seconds, 0 = off) re-damages targets that STAY overlapped —
|
|
4906
5117
|
* lava pools, poison clouds, an enemy standing on you. Contact events fire
|
|
4907
5118
|
* only on entry/exit; this is the "and it keeps hurting" knob.
|
|
4908
|
-
* - `destroySelf` frees the hazard after a hit (single-use projectiles)
|
|
5119
|
+
* - `destroySelf` frees the hazard after a hit (single-use projectiles), and
|
|
5120
|
+
* `freeParent` makes that free the PARENT — a bullet whose hitbox is a child,
|
|
5121
|
+
* which is the only shape a node-carries-one-script engine allows.
|
|
4909
5122
|
*
|
|
4910
5123
|
* SCORING PATTERN (clone-safe): wire the KILLER's `dealtDamage` →
|
|
4911
5124
|
* `ScoreKeeper.addScore`. The weapon is usually a non-cloned node (it lives on
|
|
@@ -4923,7 +5136,8 @@ var DamageOnContact = class extends Behavior {
|
|
|
4923
5136
|
/** Re-damage targets still overlapping every N seconds (0 = entry only). */
|
|
4924
5137
|
repeatEvery: { default: 0 },
|
|
4925
5138
|
/** queueFree() this node after the first successful hit. */
|
|
4926
|
-
destroySelf: { default: false }
|
|
5139
|
+
destroySelf: { default: false },
|
|
5140
|
+
...FREE_PARENT_PROP
|
|
4927
5141
|
};
|
|
4928
5142
|
static signals = ["dealtDamage"];
|
|
4929
5143
|
amount = 10;
|
|
@@ -4931,6 +5145,7 @@ var DamageOnContact = class extends Behavior {
|
|
|
4931
5145
|
oncePerTarget = true;
|
|
4932
5146
|
repeatEvery = 0;
|
|
4933
5147
|
destroySelf = false;
|
|
5148
|
+
freeParent = false;
|
|
4934
5149
|
hit = /* @__PURE__ */ new WeakSet();
|
|
4935
5150
|
/** Targets currently overlapping → seconds until their next repeat tick. */
|
|
4936
5151
|
inside = /* @__PURE__ */ new Map();
|
|
@@ -4966,7 +5181,7 @@ var DamageOnContact = class extends Behavior {
|
|
|
4966
5181
|
if (this.oncePerTarget) this.hit.add(other);
|
|
4967
5182
|
found.health.damage(this.amount);
|
|
4968
5183
|
this.emit("dealtDamage", this.amount, found.owner);
|
|
4969
|
-
if (this.destroySelf) this.
|
|
5184
|
+
if (this.destroySelf) freeOwner(this, this.freeParent, "DamageOnContact");
|
|
4970
5185
|
}
|
|
4971
5186
|
};
|
|
4972
5187
|
/**
|
|
@@ -5080,6 +5295,109 @@ function phaseOf(hour) {
|
|
|
5080
5295
|
return "dusk";
|
|
5081
5296
|
}
|
|
5082
5297
|
//#endregion
|
|
5298
|
+
//#region src/gameplay/face-target.ts
|
|
5299
|
+
/**
|
|
5300
|
+
* Turn toward the nearest thing in a group, and say when you are on it.
|
|
5301
|
+
*
|
|
5302
|
+
* A turret is the shape every tower defense, sentry gun, security camera and
|
|
5303
|
+
* idle-NPC-that-watches-you needs, and it could not be composed. `Chase`
|
|
5304
|
+
* already finds the nearest target and turns toward it — and then MOVES, which
|
|
5305
|
+
* is the one thing a turret must not do; `Patrol`/`Chase`'s `facePath` only
|
|
5306
|
+
* turns as a side effect of locomotion, so a stationary node cannot use it.
|
|
5307
|
+
* A tower defense built on 0.65.0 hand-wrote acquisition (~20 lines) and
|
|
5308
|
+
* turn-and-gate-the-shot (~15) because of that.
|
|
5309
|
+
*
|
|
5310
|
+
* `aimed` fires when the facing settles onto the target and `lostAim` when it
|
|
5311
|
+
* comes off, so "only fire while actually pointed at it" is a wire rather than
|
|
5312
|
+
* an angle comparison you write yourself.
|
|
5313
|
+
*/
|
|
5314
|
+
var FaceTarget = class extends Behavior {
|
|
5315
|
+
static props = {
|
|
5316
|
+
/** Group to acquire from — the nearest live member wins. */
|
|
5317
|
+
targetGroup: {
|
|
5318
|
+
default: "",
|
|
5319
|
+
required: true
|
|
5320
|
+
},
|
|
5321
|
+
/** Only acquire within this distance (0 = anywhere). */
|
|
5322
|
+
range: { default: 0 },
|
|
5323
|
+
/** Node to TURN (usually a skin); empty turns this behavior's own node. */
|
|
5324
|
+
facePath: {
|
|
5325
|
+
default: "",
|
|
5326
|
+
nodePath: true
|
|
5327
|
+
},
|
|
5328
|
+
/** Turn rate rad/s; 100 is an instant snap, as on the controller. */
|
|
5329
|
+
turnSpeed: { default: 6 },
|
|
5330
|
+
/** Degrees of slop that still counts as pointed at it. */
|
|
5331
|
+
aimTolerance: { default: 6 }
|
|
5332
|
+
};
|
|
5333
|
+
static signals = [
|
|
5334
|
+
"acquired",
|
|
5335
|
+
"lostTarget",
|
|
5336
|
+
"aimed",
|
|
5337
|
+
"lostAim"
|
|
5338
|
+
];
|
|
5339
|
+
targetGroup = "";
|
|
5340
|
+
range = 0;
|
|
5341
|
+
facePath = "";
|
|
5342
|
+
turnSpeed = 6;
|
|
5343
|
+
aimTolerance = 6;
|
|
5344
|
+
/** The node currently being tracked, or null. */
|
|
5345
|
+
target = null;
|
|
5346
|
+
/** True while the facing is within `aimTolerance` of the target. */
|
|
5347
|
+
onTarget = false;
|
|
5348
|
+
onReady() {
|
|
5349
|
+
if (this.targetGroup === "") throw new IncantoError("PROP_TYPE_MISMATCH", `FaceTarget on '${this.node.getPath()}': "targetGroup" is required — it is the group this turret acquires from.`, { prop: "targetGroup" });
|
|
5350
|
+
}
|
|
5351
|
+
update(dt) {
|
|
5352
|
+
const self = this.node;
|
|
5353
|
+
if (!hasPosition$1(self)) return;
|
|
5354
|
+
const found = this.nearest();
|
|
5355
|
+
if (found !== this.target) {
|
|
5356
|
+
this.target = found;
|
|
5357
|
+
this.emit(found ? "acquired" : "lostTarget", found);
|
|
5358
|
+
if (!found) this.setAim(false);
|
|
5359
|
+
}
|
|
5360
|
+
if (!found) return;
|
|
5361
|
+
const turret = this.facePath === "" ? self : this.node.getNodeOrNull(this.facePath);
|
|
5362
|
+
if (!turret) return;
|
|
5363
|
+
const dir = found.position.map((v, i) => v - (self.position[i] ?? 0));
|
|
5364
|
+
faceTravel(turret, dir, dt, this.turnSpeed);
|
|
5365
|
+
this.setAim(this.pointedAt(turret, dir));
|
|
5366
|
+
}
|
|
5367
|
+
nearest() {
|
|
5368
|
+
const self = this.node;
|
|
5369
|
+
let best = null;
|
|
5370
|
+
let bestD = Number.POSITIVE_INFINITY;
|
|
5371
|
+
for (const candidate of this.node.tree?.getNodesInGroup(this.targetGroup) ?? []) {
|
|
5372
|
+
if (!hasPosition$1(candidate)) continue;
|
|
5373
|
+
const d = Math.hypot(...candidate.position.map((v, i) => v - (self.position[i] ?? 0)));
|
|
5374
|
+
if (this.range > 0 && d > this.range) continue;
|
|
5375
|
+
if (d < bestD) {
|
|
5376
|
+
bestD = d;
|
|
5377
|
+
best = candidate;
|
|
5378
|
+
}
|
|
5379
|
+
}
|
|
5380
|
+
return best;
|
|
5381
|
+
}
|
|
5382
|
+
/** Is the turret's forward within tolerance of the direction to the target? */
|
|
5383
|
+
pointedAt(turret, dir) {
|
|
5384
|
+
const rot = turret.rotation;
|
|
5385
|
+
const is3d = Array.isArray(rot);
|
|
5386
|
+
const [a, b] = is3d ? [dir[0] ?? 0, dir[2] ?? 0] : [dir[0] ?? 0, dir[1] ?? 0];
|
|
5387
|
+
const len = Math.hypot(a, b);
|
|
5388
|
+
if (len < 1e-6) return true;
|
|
5389
|
+
const yaw = (is3d ? rot[1] ?? 0 : rot ?? 0) * Math.PI / 180;
|
|
5390
|
+
const fx = is3d ? Math.sin(yaw) : Math.cos(yaw);
|
|
5391
|
+
const fy = is3d ? Math.cos(yaw) : Math.sin(yaw);
|
|
5392
|
+
return (fx * a + fy * b) / len >= Math.cos(this.aimTolerance * Math.PI / 180);
|
|
5393
|
+
}
|
|
5394
|
+
setAim(on) {
|
|
5395
|
+
if (on === this.onTarget) return;
|
|
5396
|
+
this.onTarget = on;
|
|
5397
|
+
this.emit(on ? "aimed" : "lostAim", this.target);
|
|
5398
|
+
}
|
|
5399
|
+
};
|
|
5400
|
+
//#endregion
|
|
5083
5401
|
//#region src/gameplay/float-away.ts
|
|
5084
5402
|
/**
|
|
5085
5403
|
* Rise, fade, and be gone — the second half of a damage number.
|
|
@@ -5714,6 +6032,10 @@ const hitStops = /* @__PURE__ */ new Map();
|
|
|
5714
6032
|
* pickups that vanish. Accumulates `dt`; on elapse emits `expired` then
|
|
5715
6033
|
* `queueFree()`s its node.
|
|
5716
6034
|
*
|
|
6035
|
+
* `freeParent: true` frees the node's PARENT instead — the shape a bullet needs,
|
|
6036
|
+
* since a node carries ONE script so `Projectile`, the damage and the timer have
|
|
6037
|
+
* to live on separate nodes.
|
|
6038
|
+
*
|
|
5717
6039
|
* With `startOnSignal: true` the countdown is armed manually via `startTimer()`
|
|
5718
6040
|
* (wire a signal → `startTimer`), so the lifetime begins on an event rather
|
|
5719
6041
|
* than at spawn.
|
|
@@ -5723,11 +6045,13 @@ var Lifetime = class extends Behavior {
|
|
|
5723
6045
|
/** Seconds before the node frees itself. */
|
|
5724
6046
|
seconds: { default: 1 },
|
|
5725
6047
|
/** Defer the countdown until `startTimer()` is called (default: start at ready). */
|
|
5726
|
-
startOnSignal: { default: false }
|
|
6048
|
+
startOnSignal: { default: false },
|
|
6049
|
+
...FREE_PARENT_PROP
|
|
5727
6050
|
};
|
|
5728
6051
|
static signals = ["expired"];
|
|
5729
6052
|
seconds = 1;
|
|
5730
6053
|
startOnSignal = false;
|
|
6054
|
+
freeParent = false;
|
|
5731
6055
|
elapsed = 0;
|
|
5732
6056
|
running = false;
|
|
5733
6057
|
fired = false;
|
|
@@ -5748,7 +6072,7 @@ var Lifetime = class extends Behavior {
|
|
|
5748
6072
|
this.fired = true;
|
|
5749
6073
|
this.running = false;
|
|
5750
6074
|
this.emit("expired");
|
|
5751
|
-
this.
|
|
6075
|
+
freeOwner(this, this.freeParent, "Lifetime");
|
|
5752
6076
|
}
|
|
5753
6077
|
}
|
|
5754
6078
|
};
|
|
@@ -6004,6 +6328,13 @@ function distanceBetween(a, b) {
|
|
|
6004
6328
|
*
|
|
6005
6329
|
* - `loop` (default) — after the last point, head back to the first.
|
|
6006
6330
|
* - `mode: 'pingpong'` — reverse direction at each end instead of wrapping.
|
|
6331
|
+
* - `moveParent` (default false) moves the parent node instead of this one,
|
|
6332
|
+
* exactly as `Chase` does. A node carries ONE behavior, so a guard that must
|
|
6333
|
+
* patrol AND hold `Health` on its body puts `Patrol` on a CHILD. Without
|
|
6334
|
+
* this the patrolling body had to carry `Patrol` itself and could hold
|
|
6335
|
+
* nothing else — so a patrolling enemy could not be hurt.
|
|
6336
|
+
* - `facePath`/`turnSpeed` turn a skin toward the direction of travel, the job
|
|
6337
|
+
* `CharacterController3D` does for the player and nobody did for an NPC.
|
|
6007
6338
|
*/
|
|
6008
6339
|
var Patrol = class extends Behavior {
|
|
6009
6340
|
static props = {
|
|
@@ -6022,7 +6353,10 @@ var Patrol = class extends Behavior {
|
|
|
6022
6353
|
options: ["loop", "pingpong"]
|
|
6023
6354
|
},
|
|
6024
6355
|
/** Seconds to wait at each reached point before continuing. */
|
|
6025
|
-
pauseAt: { default: 0 }
|
|
6356
|
+
pauseAt: { default: 0 },
|
|
6357
|
+
/** Move the parent node instead of this one (AI-on-a-child pattern). */
|
|
6358
|
+
moveParent: { default: false },
|
|
6359
|
+
...FACING_PROPS
|
|
6026
6360
|
};
|
|
6027
6361
|
static signals = ["reachedPoint"];
|
|
6028
6362
|
points = [];
|
|
@@ -6030,33 +6364,53 @@ var Patrol = class extends Behavior {
|
|
|
6030
6364
|
loop = true;
|
|
6031
6365
|
mode = "loop";
|
|
6032
6366
|
pauseAt = 0;
|
|
6367
|
+
moveParent = false;
|
|
6368
|
+
facePath = "";
|
|
6369
|
+
turnSpeed = 100;
|
|
6033
6370
|
index = 0;
|
|
6034
6371
|
direction = 1;
|
|
6035
6372
|
pauseTimer = 0;
|
|
6036
6373
|
onReady() {
|
|
6037
6374
|
if (!Array.isArray(this.points) || this.points.length === 0) throw new IncantoError("PROP_TYPE_MISMATCH", `Patrol on '${this.node.getPath()}': "points" must be a non-empty array of position arrays or node paths.`, { prop: "points" });
|
|
6038
6375
|
if (this.mode !== "loop" && this.mode !== "pingpong") throw new IncantoError("PROP_TYPE_MISMATCH", `Patrol on '${this.node.getPath()}': "mode" must be 'loop' or 'pingpong', got '${this.mode}'.`, { prop: "mode" });
|
|
6039
|
-
|
|
6376
|
+
if (this.moveParent) {
|
|
6377
|
+
const parent = this.node.parent;
|
|
6378
|
+
if (!parent || !hasPosition$1(parent)) throw new IncantoError("PROP_TYPE_MISMATCH", `Patrol on '${this.node.getPath()}': "moveParent" needs a spatial parent (a Node2D/Node3D) to move. '${this.node.name}' has none.`, {
|
|
6379
|
+
prop: "moveParent",
|
|
6380
|
+
path: this.node.getPath()
|
|
6381
|
+
});
|
|
6382
|
+
} else requirePosition(this);
|
|
6040
6383
|
}
|
|
6041
6384
|
update(dt) {
|
|
6042
6385
|
if (this.pauseTimer > 0) {
|
|
6043
6386
|
this.pauseTimer = Math.max(0, this.pauseTimer - dt);
|
|
6044
6387
|
return;
|
|
6045
6388
|
}
|
|
6046
|
-
const
|
|
6389
|
+
const mover = this.moveParent ? this.node.parent : this.node;
|
|
6390
|
+
if (!mover || !hasPosition$1(mover)) return;
|
|
6391
|
+
const node = mover;
|
|
6047
6392
|
const target = this.pointAt(this.index);
|
|
6048
6393
|
if (!target) {
|
|
6049
6394
|
this.reportDeadWaypoint(this.index);
|
|
6050
6395
|
return;
|
|
6051
6396
|
}
|
|
6397
|
+
const from = [...node.position];
|
|
6052
6398
|
const { position, reached } = moveToward(node.position, target, this.speed * dt);
|
|
6053
6399
|
moveBody(node, position, dt);
|
|
6400
|
+
this.face(from, position, dt);
|
|
6054
6401
|
if (reached) {
|
|
6055
6402
|
this.emit("reachedPoint", this.index);
|
|
6056
6403
|
this.advance();
|
|
6057
6404
|
if (this.pauseAt > 0) this.pauseTimer = this.pauseAt;
|
|
6058
6405
|
}
|
|
6059
6406
|
}
|
|
6407
|
+
/** Turn the skin toward the step just taken. */
|
|
6408
|
+
face(from, to, dt) {
|
|
6409
|
+
if (this.facePath === "") return;
|
|
6410
|
+
const skin = this.node.getNodeOrNull(this.facePath);
|
|
6411
|
+
if (!skin) return;
|
|
6412
|
+
faceTravel(skin, to.map((v, i) => v - (from[i] ?? 0)), dt, this.turnSpeed);
|
|
6413
|
+
}
|
|
6060
6414
|
/** Waypoints already reported dead, so a per-frame stall says it once. */
|
|
6061
6415
|
reportedDead = /* @__PURE__ */ new Set();
|
|
6062
6416
|
/**
|
|
@@ -7006,6 +7360,7 @@ const GAMEPLAY_BEHAVIORS = {
|
|
|
7006
7360
|
Health,
|
|
7007
7361
|
Lifetime,
|
|
7008
7362
|
ScoreKeeper,
|
|
7363
|
+
Currency,
|
|
7009
7364
|
Pickup,
|
|
7010
7365
|
Collector,
|
|
7011
7366
|
DamageOnContact,
|
|
@@ -7015,6 +7370,7 @@ const GAMEPLAY_BEHAVIORS = {
|
|
|
7015
7370
|
FollowCamera,
|
|
7016
7371
|
Patrol,
|
|
7017
7372
|
Chase,
|
|
7373
|
+
FaceTarget,
|
|
7018
7374
|
Wander,
|
|
7019
7375
|
ZombieAI,
|
|
7020
7376
|
MoveTo,
|
|
@@ -7035,4 +7391,4 @@ function registerGameplayBehaviors(opts) {
|
|
|
7035
7391
|
for (const [name, ctor] of Object.entries(GAMEPLAY_BEHAVIORS)) registerBehavior(name, ctor, { replace });
|
|
7036
7392
|
}
|
|
7037
7393
|
//#endregion
|
|
7038
|
-
export {
|
|
7394
|
+
export { DamageOnContact as A, rejectScale as B, restartScene as C, FaceTarget as D, FloatAway as E, localFromWorld as F, StaticBody3D as G, CharacterBody3D as H, worldPosition as I, createCausticsQuad as J, Node3D as K, Water3D as L, Currency as M, Collector as N, DayNight as O, Chase as P, WATER_CUTOUT_MAX as R, goToScene as S, FollowCamera as T, PhysicsBody3D as U, Area3D as V, RigidBody3D as W, colliderFootDrop as X, WATER_MAX_RIPPLES as Y, Cooldown as _, Wander as a, Interactable as b, SavePoint as c, Patrol as d, PathFollow as f, CameraShake as g, Lifetime as h, WaveSpawner as i, Health as j, phaseOf as k, Projectile as l, MoveTo as m, registerGameplayBehaviors as n, Spawner as o, Oscillate as p, validateCollider3D as q, ZombieAI as r, ScoreKeeper as s, GAMEPLAY_BEHAVIORS as t, Pickup as u, hitStop as v, tolerateUnknownAction as w, GameFlow as x, screenFlash as y, WaterCutout3D as z };
|