lecodes-sdk 0.20.2 → 1.1.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/global.d.ts +35 -9
- package/dist/types/animate/tween/Animation.d.ts +69 -0
- package/dist/types/animate/tween/Timeline.d.ts +55 -0
- package/dist/types/animate/tween/animateValue.d.ts +27 -0
- package/dist/types/animate/tween/easing.d.ts +29 -0
- package/dist/types/animate/tween/spec.d.ts +178 -0
- package/dist/types/canvas/Canvas.d.ts +2 -0
- package/dist/types/g2/Node2D.d.ts +16 -0
- package/dist/types/g2/Sprite.d.ts +11 -1
- package/dist/types/gl/Camera.d.ts +15 -1
- package/dist/types/gl/DecalSet.d.ts +48 -3
- package/dist/types/gl/Foliage.d.ts +47 -0
- package/dist/types/gl/Geometry.d.ts +36 -0
- package/dist/types/gl/Light.d.ts +25 -7
- package/dist/types/gl/Lightmap.d.ts +90 -51
- package/dist/types/gl/Material.d.ts +32 -20
- package/dist/types/gl/Mesh.d.ts +7 -1
- package/dist/types/gl/Model.d.ts +41 -5
- package/dist/types/gl/Node.d.ts +18 -0
- package/dist/types/gl/Particles.d.ts +53 -1
- package/dist/types/gl/Scene.d.ts +21 -1
- package/dist/types/gl/animation/AnimationClip.d.ts +19 -0
- package/dist/types/gl/animation/Animator.d.ts +27 -0
- package/dist/types/gl/animation/DynamicBone.d.ts +184 -0
- package/dist/types/gl/animation/IK.d.ts +109 -0
- package/dist/types/gl/{Locomotion.d.ts → animation/Locomotion.d.ts} +6 -6
- package/dist/types/gl/animation/Warp.d.ts +2 -1
- package/dist/types/gl/animation/core.d.ts +35 -4
- package/dist/types/gl/{AudioSource.d.ts → audio/AudioSource.d.ts} +6 -6
- package/dist/types/gl/{AudioZone.d.ts → audio/AudioZone.d.ts} +4 -4
- package/dist/types/gl/{SceneAudio.d.ts → audio/SceneAudio.d.ts} +1 -1
- package/dist/types/gl/{NavAgent.d.ts → nav/NavAgent.d.ts} +4 -4
- package/dist/types/gl/{NavMesh.d.ts → nav/NavMesh.d.ts} +4 -4
- package/dist/types/gl/{CharacterController.d.ts → physics/CharacterController.d.ts} +5 -5
- package/dist/types/gl/{Physics.d.ts → physics/Physics.d.ts} +6 -5
- package/dist/types/gl/physics/Ragdoll.d.ts +161 -0
- package/dist/types/gl/{Shape.d.ts → physics/Shape.d.ts} +3 -3
- package/dist/types/gl/{Trigger.d.ts → physics/Trigger.d.ts} +2 -2
- package/dist/types/gl/{Terrain.d.ts → terrain/Terrain.d.ts} +10 -8
- package/dist/types/gl/{terrainMesh.d.ts → terrain/terrainMesh.d.ts} +1 -1
- package/dist/types/gl/vehicle/Vehicle.d.ts +300 -0
- package/dist/types/gl/vehicle/Wheel.d.ts +147 -0
- package/dist/types/inject.d.ts +35 -27
- package/dist/types/runtime/files.d.ts +24 -1
- package/dist/types/scene/defineScene.d.ts +50 -31
- package/dist/types/ui/UIButton.d.ts +3 -1
- package/dist/types/ui/UIInput.d.ts +5 -1
- package/dist/types/ui/UINode.d.ts +24 -24
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/prompts/README.md +142 -142
- package/prompts/core-design.md +27 -4
- package/prompts/core.md +35 -6
- package/prompts/select.ts +19 -4
- package/src/animate/tween/Animation.ts +378 -0
- package/src/animate/tween/Timeline.ts +175 -0
- package/src/animate/tween/animateValue.ts +100 -0
- package/src/animate/tween/easing.ts +172 -0
- package/src/animate/tween/spec.ts +479 -0
- package/src/bridges.d.ts +1760 -1481
- package/src/canvas/Canvas.ts +21 -0
- package/src/compile/__tests__/assetMacro.test.ts +26 -0
- package/src/compile/__tests__/compile.test.ts +11 -0
- package/src/compile/__tests__/detectEntry.test.ts +19 -0
- package/src/compile/__tests__/serverSplit.test.ts +27 -0
- package/src/compile/bundler.ts +34 -4
- package/src/compile/compileProject.ts +31 -1
- package/src/compile/detectEntry.ts +8 -3
- package/src/compile/header.ts +6 -3
- package/src/compile/index.ts +3 -0
- package/src/compile/sceneEditor.ts +42 -1
- package/src/compile/serverSplit.ts +9 -3
- package/src/g2/Node2D.ts +38 -0
- package/src/g2/Sprite.ts +20 -1
- package/src/gl/Camera.ts +34 -1
- package/src/gl/DecalSet.ts +132 -5
- package/src/gl/Foliage.ts +102 -0
- package/src/gl/Geometry.ts +109 -0
- package/src/gl/Light.ts +46 -16
- package/src/gl/Lightmap.ts +440 -249
- package/src/gl/Material.ts +69 -36
- package/src/gl/Mesh.ts +120 -102
- package/src/gl/Model.ts +167 -124
- package/src/gl/Node.ts +40 -1
- package/src/gl/Particles.ts +82 -5
- package/src/gl/Scene.ts +35 -2
- package/src/gl/animation/AnimationClip.ts +52 -0
- package/src/gl/animation/Animator.ts +42 -2
- package/src/gl/animation/DynamicBone.ts +482 -0
- package/src/gl/animation/IK.ts +214 -0
- package/src/gl/{Locomotion.ts → animation/Locomotion.ts} +7 -7
- package/src/gl/animation/Playback.ts +5 -4
- package/src/gl/animation/Warp.ts +5 -2
- package/src/gl/animation/core.ts +65 -4
- package/src/gl/{AudioSource.ts → audio/AudioSource.ts} +7 -7
- package/src/gl/{AudioZone.ts → audio/AudioZone.ts} +75 -75
- package/src/gl/{SceneAudio.ts → audio/SceneAudio.ts} +2 -2
- package/src/gl/{NavAgent.ts → nav/NavAgent.ts} +5 -5
- package/src/gl/{NavMesh.ts → nav/NavMesh.ts} +8 -8
- package/src/gl/{CharacterController.ts → physics/CharacterController.ts} +5 -5
- package/src/gl/{Physics.ts → physics/Physics.ts} +12 -5
- package/src/gl/physics/Ragdoll.ts +451 -0
- package/src/gl/{Shape.ts → physics/Shape.ts} +3 -3
- package/src/gl/{Trigger.ts → physics/Trigger.ts} +45 -45
- package/src/gl/{physicsEvents.ts → physics/physicsEvents.ts} +1 -1
- package/src/gl/{Terrain.ts → terrain/Terrain.ts} +14 -12
- package/src/gl/{terrainMesh.ts → terrain/terrainMesh.ts} +1 -1
- package/src/gl/vehicle/Vehicle.ts +666 -0
- package/src/gl/vehicle/Wheel.ts +290 -0
- package/src/inject.ts +236 -224
- package/src/runtime/files.ts +32 -2
- package/src/scene/defineScene.ts +92 -66
- package/src/scene/level.ts +2 -2
- package/src/ui/UIButton.ts +2 -2
- package/src/ui/UIInput.ts +3 -3
- package/src/ui/UINode.ts +61 -36
- package/dist/types/animate/animate.d.ts +0 -20
- package/dist/types/gl/Gearbox.d.ts +0 -86
- package/dist/types/gl/IK.d.ts +0 -53
- package/dist/types/gl/Ragdoll.d.ts +0 -86
- package/dist/types/gl/Vehicle.d.ts +0 -191
- package/dist/types/gl/Wheel.d.ts +0 -95
- package/src/animate/animate.ts +0 -238
- package/src/gl/Gearbox.ts +0 -212
- package/src/gl/IK.ts +0 -193
- package/src/gl/Ragdoll.ts +0 -270
- package/src/gl/Vehicle.ts +0 -473
- package/src/gl/Wheel.ts +0 -240
- /package/dist/types/gl/{physicsEvents.d.ts → physics/physicsEvents.d.ts} +0 -0
package/src/scene/defineScene.ts
CHANGED
|
@@ -46,13 +46,16 @@ import { CameraPlace } from "../gl/CameraPlace"
|
|
|
46
46
|
import { Node } from "../gl/Node"
|
|
47
47
|
import { Mesh } from "../gl/Mesh"
|
|
48
48
|
import { Model } from "../gl/Model"
|
|
49
|
-
import { Physics } from "../gl/Physics"
|
|
50
|
-
import {
|
|
51
|
-
import {
|
|
52
|
-
import {
|
|
49
|
+
import { Physics } from "../gl/physics/Physics"
|
|
50
|
+
import { CharacterController } from "../gl/physics/CharacterController"
|
|
51
|
+
import { Lightmap, type LightmapTransmit } from "../gl/Lightmap"
|
|
52
|
+
import { Foliage, type FoliageOptions } from "../gl/Foliage"
|
|
53
|
+
import { NavMesh } from "../gl/nav/NavMesh"
|
|
54
|
+
import { Terrain, type TerrainLayer, type TerrainRegion, type TerrainOptions } from "../gl/terrain/Terrain"
|
|
53
55
|
import { Light, type SunOptions } from "../gl/Light"
|
|
56
|
+
import { Material } from "../gl/Material"
|
|
54
57
|
import { assignMaterialDef, MaterialHandle, resolveMaterialDef, type MaterialDef } from "./material"
|
|
55
|
-
import type { CylinderOptions, PlaneOptions, SphereOptions } from "../gl/Geometry"
|
|
58
|
+
import type { CapsuleOptions, CylinderOptions, PlaneOptions, SphereOptions } from "../gl/Geometry"
|
|
56
59
|
import { GizmoBuffer, Gizmos, withGizmoScope, type GizmoBatch } from "./gizmos"
|
|
57
60
|
|
|
58
61
|
// ---- the literal grammar (what the visual editor reads and writes) -----------
|
|
@@ -61,6 +64,7 @@ export type MeshDef =
|
|
|
61
64
|
| { kind: "box", size?: Vec3Like | number }
|
|
62
65
|
| ({ kind: "sphere" } & SphereOptions)
|
|
63
66
|
| ({ kind: "cylinder" } & CylinderOptions)
|
|
67
|
+
| ({ kind: "capsule" } & CapsuleOptions)
|
|
64
68
|
| ({ kind: "plane" } & PlaneOptions)
|
|
65
69
|
|
|
66
70
|
export type LightDef = { kind: "sun" } & SunOptions
|
|
@@ -182,6 +186,10 @@ export type SceneNodeDef = {
|
|
|
182
186
|
* Default: static unless a `Physics` aspect moves the node (`dynamic` — Physics' default — or
|
|
183
187
|
* `kinematic`). Set it only to override that rule; prefab subtrees inherit the verdict. */
|
|
184
188
|
lightmap?: boolean
|
|
189
|
+
/** `model` nodes: vegetation — load through the FOLIAGE tier (wind, touch bending, per-copy tint; see
|
|
190
|
+
* `Foliage`). `{ fade: [start, end] }` also thins the cards out over that distance range (m) and drops the
|
|
191
|
+
* copy past `end` — for ground cover. The fade is per ASSET: every node of the same GLB shares it. */
|
|
192
|
+
foliage?: boolean | { fade?: [number, number] }
|
|
185
193
|
/** Navigation (a scene with `env.navmesh`, see navmesh.md): every STATIC body is walkable by
|
|
186
194
|
* default. `false` leaves this one out of the bake; `'unwalkable'` cuts its footprint out
|
|
187
195
|
* (nobody stands on or crosses it). */
|
|
@@ -199,37 +207,44 @@ export type SceneCameraDef = CameraProjectionDef & {
|
|
|
199
207
|
target?: Vec3Like
|
|
200
208
|
}
|
|
201
209
|
|
|
202
|
-
/** `env.lightmap` — the level's baked lighting (
|
|
203
|
-
*
|
|
210
|
+
/** `env.lightmap` — the level's baked lighting (packages/creator-bake): the files `lecodes lightmap bake`
|
|
211
|
+
* writes. Absent = real-time only. */
|
|
204
212
|
export type SceneLightmapDef = {
|
|
205
|
-
/** `asset('./assets/lightmap/
|
|
213
|
+
/** `asset('./assets/lightmap/level.bake')` */
|
|
206
214
|
data: string
|
|
207
|
-
/** `asset('./assets/lightmap/
|
|
208
|
-
* (`lecodes lightmap bake --pages`), every page in order: `[asset('x.ktx2'), asset('
|
|
209
|
-
|
|
210
|
-
/** `asset('./assets/lightmap/
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
light
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
215
|
+
/** `asset('./assets/lightmap/level-light.ktx2')` — or, for a bake that took more than one atlas PAGE
|
|
216
|
+
* (`lecodes lightmap bake --pages`), every page in order: `[asset('x-light.ktx2'), asset('x-light_1.ktx2')]`. */
|
|
217
|
+
light: string | string[]
|
|
218
|
+
/** `asset('./assets/lightmap/level-aux.ktx2')` (pages like `light`) — sun / sky visibility + light direction. */
|
|
219
|
+
aux: string | string[]
|
|
220
|
+
/** DEBUG: the page sets a `lecodes lightmap bake --split` wrote (`<stem>-direct[_n].ktx2`, `<stem>-indirect[_n].ktx2`),
|
|
221
|
+
* pages like `light` — the `Lightmap.debug("direct" | "indirect")` views bind them in place of `light`. */
|
|
222
|
+
direct?: string | string[]
|
|
223
|
+
indirect?: string | string[]
|
|
224
|
+
/** `asset('./assets/lightmap/level-probes.ktx2')` — the bake's reflection probes (one prefiltered cubemap of the level's
|
|
225
|
+
* radiance per probe; every static reflects the nearest). Absent = statics reflect the sky's SH alone. */
|
|
226
|
+
probes?: string
|
|
227
|
+
/** FOR THE BAKE: the radiance in cd / m² of emission 1.0 on this level (emissive factor x strength x map) - every lit
|
|
228
|
+
* region of an emissive surface bakes as a rectangle lamp. An imported pack's emission is a LOOK (the school's panels
|
|
229
|
+
* say 10); this is where the level says what they give: a 1.3 m ceiling panel of ~8000 lm is ~1600 nits. Default: what
|
|
230
|
+
* the camera makes of 1.0 (`1.2 x 2^EV100`). 0 = emissive surfaces light nothing. `--emissive-nits` overrides. */
|
|
231
|
+
emissiveNits?: number
|
|
232
|
+
/** `false` = the emissive surfaces light nothing in the bake - the level is lit by its lamps (`Light.point` with a
|
|
233
|
+
* `bakeArea` next to every panel, the Unity packs' way) and the panels are decoration; `emissiveNits` then only says how
|
|
234
|
+
* bright their glow is DRAWN (scene-referred: nits through the camera's exposure). Default true. */
|
|
235
|
+
emissiveBake?: boolean
|
|
236
|
+
/** FOR THE BAKE: materials that let light THROUGH them, by glTF material name (`"name*"` = every name with that start) →
|
|
237
|
+
* the share of a shadow ray that passes, tinted by the material's base colour x map: `{ mat_awning: 0.3 }` puts the
|
|
238
|
+
* awning's warm, patterned light on the sand. The surface stays opaque - baked, drawn as before, a shadow caster.
|
|
239
|
+
* `{ through, diffuse }` splits it: `through` goes straight (the picture of the map), `diffuse` is scattered by the
|
|
240
|
+
* fibres (the underside glows, a soft fill with no picture) - dense canvas: `{ through: 0.08, diffuse: 0.25 }`. */
|
|
241
|
+
transmit?: Record<string, LightmapTransmit>
|
|
242
|
+
/** `asset('./assets/lightmap/level.lgrid')` — THE LIGHT GRID for movers: every node a Physics aspect or a
|
|
243
|
+
* CharacterController moves (and what code marks with `Lightmap.track`) takes its ambient light from the bake at the
|
|
244
|
+
* place it is at, whatever its shader. Absent = movers keep the scene's IBL. */
|
|
217
245
|
volume?: string
|
|
218
|
-
/**
|
|
219
|
-
|
|
220
|
-
/** Multiplier on the ambient share in the shadow math (1 = filament's own darkness). Default 1. */
|
|
221
|
-
ambientScale?: number
|
|
222
|
-
/** How much of the baked ambient occlusion applies: 1 = all of it, 0 = none. An interior lit by its
|
|
223
|
-
* ambient probe alone can want less than the geometric truth. Default 1. */
|
|
224
|
-
aoStrength?: number
|
|
225
|
-
/** Multiplier on the baked point lights' irradiance (default 1) - dial the lamps up or down
|
|
226
|
-
* without a re-bake, since their irradiance lives in the atlas. Scales the light volume with the
|
|
227
|
-
* atlas, so movers match the statics. See `LightmapLoadOptions.lightBoost`. */
|
|
228
|
-
lightBoost?: number
|
|
229
|
-
/** Multiplier on the light VOLUME's irradiance alone (default = `lightBoost`). Since the volume is an
|
|
230
|
-
* ambient cube (2026-08-31) movers shade like the statics and the default is right; it stays as a
|
|
231
|
-
* trim. See `LightmapLoadOptions.volumeBoost`. */
|
|
232
|
-
volumeBoost?: number
|
|
246
|
+
/** A DEBUG multiplier on the atlas' lux (default 1). */
|
|
247
|
+
lightScale?: number
|
|
233
248
|
}
|
|
234
249
|
|
|
235
250
|
/** `env.navmesh` — the level's navigation mesh (see navmesh.md): the file `lecodes navmesh bake`
|
|
@@ -244,7 +259,7 @@ export type SceneNavmeshDef = {
|
|
|
244
259
|
}
|
|
245
260
|
|
|
246
261
|
export type SceneDef = {
|
|
247
|
-
env?: SceneOptions & { lightmap?: SceneLightmapDef, navmesh?: SceneNavmeshDef }
|
|
262
|
+
env?: SceneOptions & { lightmap?: SceneLightmapDef, navmesh?: SceneNavmeshDef, foliage?: FoliageOptions }
|
|
248
263
|
camera?: SceneCameraDef
|
|
249
264
|
nodes?: Record<string, SceneNodeDef>
|
|
250
265
|
}
|
|
@@ -402,6 +417,7 @@ const createMesh = async (def: MeshDef, material?: MaterialDef): Promise<Mesh> =
|
|
|
402
417
|
case "box": mesh = Mesh.box({ size: def.size, material: mat }); break
|
|
403
418
|
case "sphere": { const { kind: _k, ...opts } = def; mesh = Mesh.sphere({ ...opts, material: mat }); break }
|
|
404
419
|
case "cylinder": { const { kind: _k, ...opts } = def; mesh = Mesh.cylinder({ ...opts, material: mat }); break }
|
|
420
|
+
case "capsule": { const { kind: _k, ...opts } = def; mesh = Mesh.capsule({ ...opts, material: mat }); break }
|
|
405
421
|
case "plane": { const { kind: _k, ...opts } = def; mesh = Mesh.plane({ ...opts, material: mat }); break }
|
|
406
422
|
}
|
|
407
423
|
if (material instanceof MaterialHandle) material._users.add({ node: mesh, slot: 0 })
|
|
@@ -415,21 +431,25 @@ const createMesh = async (def: MeshDef, material?: MaterialDef): Promise<Mesh> =
|
|
|
415
431
|
// rename honestly invalidates the rect. make() subtrees are code — they register themselves with
|
|
416
432
|
// Lightmap.add. Edit mode never bakes or applies (the editor shows real-time shadows).
|
|
417
433
|
|
|
418
|
-
type LightmapCtx = { prefix: string
|
|
434
|
+
type LightmapCtx = { prefix: string }
|
|
419
435
|
|
|
436
|
+
// A CharacterController moves its node as surely as a dynamic body does (2026-09-11: the hero's
|
|
437
|
+
// capsule was read as static, so its sword baked a shadow into the terrain at the spawn point).
|
|
420
438
|
const movesByPhysics = (def: SceneNodeDef): boolean =>
|
|
421
439
|
(def.aspects ?? []).some((e) =>
|
|
422
|
-
(e.ctor as unknown) === Physics && ((e.props as { motion?: string } | undefined)?.motion ?? "dynamic") !== "static")
|
|
440
|
+
((e.ctor as unknown) === Physics && ((e.props as { motion?: string } | undefined)?.motion ?? "dynamic") !== "static")
|
|
441
|
+
|| (e.ctor as unknown) === CharacterController)
|
|
423
442
|
|
|
424
|
-
/** The static verdict for one node def (the editor's "Baked lighting" switch shows the same rule).
|
|
443
|
+
/** The static verdict for one node def (the editor's "Baked lighting" switch shows the same rule).
|
|
444
|
+
* Children of a mover are movers too — buildNodes carries that down, this is the node's own verdict. */
|
|
425
445
|
export const isLightmapStatic = (def: SceneNodeDef): boolean => def.lightmap ?? !movesByPhysics(def)
|
|
426
446
|
|
|
427
|
-
// `lightmap` undefined = Model.load's own default ('dynamic' while a level with a light volume runs —
|
|
428
|
-
// this is how a weapon or arms scene instantiated from code reads the volume; an explicit false would win)
|
|
429
447
|
/** A terrain node: from its file or a fresh flat grid; textures load before the node appears (like a
|
|
430
448
|
* mesh's material). Stamps and holes apply later in buildNodes, once every target node exists. */
|
|
431
|
-
const createTerrain = async (def: TerrainNodeDef): Promise<Node> => {
|
|
432
|
-
const { data, stamps: _s, holes: _h, ...
|
|
449
|
+
const createTerrain = async (def: TerrainNodeDef, lightmap: boolean): Promise<Node> => {
|
|
450
|
+
const { data, stamps: _s, holes: _h, ...rest } = def
|
|
451
|
+
// a lightmap static is built with the baked material from the start, like a model is loaded lightmapped
|
|
452
|
+
const opts = lightmap ? { ...rest, material: Material.terrainLightmap() } : rest
|
|
433
453
|
const t = data !== undefined ? await Terrain.load(data, opts) : Terrain.create(opts)
|
|
434
454
|
await t.ready
|
|
435
455
|
return t.node
|
|
@@ -446,14 +466,11 @@ const createTerrain = async (def: TerrainNodeDef): Promise<Node> => {
|
|
|
446
466
|
// cannot leak into the next node that leaves the flag alone.
|
|
447
467
|
const modelTemplates = new Map<string, Promise<Model>>()
|
|
448
468
|
|
|
449
|
-
const loadModel = (source: string, lightmap: boolean
|
|
450
|
-
|
|
451
|
-
// sets — so the KEY has to hold the resolved mode, not the argument.
|
|
452
|
-
const mode = lightmap ?? Model._lightmapDefault
|
|
453
|
-
const key = `${mode}\u0000${source}`
|
|
469
|
+
const loadModel = (source: string, lightmap: boolean, foliage: boolean): Promise<Model> => {
|
|
470
|
+
const key = `${lightmap}\u0000${foliage ? "foliage" : ""}\u0000${source}`
|
|
454
471
|
let template = modelTemplates.get(key)
|
|
455
472
|
if (template === undefined) {
|
|
456
|
-
template = Model.load(source, { lightmap
|
|
473
|
+
template = Model.load(source, { lightmap, foliage })
|
|
457
474
|
modelTemplates.set(key, template)
|
|
458
475
|
// a failed load must not poison the cache — the next node retries
|
|
459
476
|
void template.catch(() => modelTemplates.delete(key))
|
|
@@ -461,12 +478,12 @@ const loadModel = (source: string, lightmap: boolean | "dynamic" | undefined): P
|
|
|
461
478
|
return template.then((t) => t.clone())
|
|
462
479
|
}
|
|
463
480
|
|
|
464
|
-
const createSource = (path: string, def: SceneNodeDef, lightmap: boolean
|
|
481
|
+
const createSource = (path: string, def: SceneNodeDef, lightmap: boolean): Node | Promise<Node> => {
|
|
465
482
|
const sources = [ def.mesh, def.model, def.light, def.make, def.prefab, def.camera, def.terrain ].filter((s) => s !== undefined).length
|
|
466
483
|
if (sources > 1) throw new Error(`Scene node "${path}" declares more than one source (mesh/model/light/make/prefab/camera/terrain)`)
|
|
467
|
-
if (def.model !== undefined) return loadModel(def.model, lightmap)
|
|
484
|
+
if (def.model !== undefined) return loadModel(def.model, lightmap, !!def.foliage)
|
|
468
485
|
if (def.mesh !== undefined) return createMesh(def.mesh, def.material)
|
|
469
|
-
if (def.terrain !== undefined) return createTerrain(def.terrain)
|
|
486
|
+
if (def.terrain !== undefined) return createTerrain(def.terrain, lightmap)
|
|
470
487
|
if (def.light !== undefined) { const { kind: _k, ...opts } = def.light; return Light.sun(opts) }
|
|
471
488
|
// make/prefab/camera/group nodes are plain wrappers — make/prefab subtrees mount under them
|
|
472
489
|
// during the build (attachPrefab) or in phase 2 (attachMake); a camera node drives scene.camera
|
|
@@ -520,6 +537,8 @@ const applyNode = (node: Node, name: string, def: SceneNodeDef): void => {
|
|
|
520
537
|
if (t && def.receiveShadows !== undefined) t.receiveShadows = def.receiveShadows
|
|
521
538
|
}
|
|
522
539
|
if (def.overrides && def.model !== undefined) applyModelOverrides(node, def.overrides)
|
|
540
|
+
// the foliage fade is per asset — one node's range is every copy's (the last def wins)
|
|
541
|
+
if (typeof def.foliage === "object" && def.foliage.fade && node instanceof Model && node._foliage) Foliage.fade(node, def.foliage.fade[0], def.foliage.fade[1])
|
|
523
542
|
}
|
|
524
543
|
|
|
525
544
|
// ---- edit-mode node markers + the play-mode camera rig -------------------------------------------
|
|
@@ -859,8 +878,11 @@ const buildNodes = async (
|
|
|
859
878
|
lm: LightmapCtx | null = null,
|
|
860
879
|
): Promise<void> => {
|
|
861
880
|
const pending: { path: string, node: Node, def: SceneNodeDef }[] = []
|
|
881
|
+
const moverRoots: Node[] = []
|
|
862
882
|
|
|
863
|
-
|
|
883
|
+
// `parentMoves`: a child of a mover moves with it (the body under the hero's capsule) — it never
|
|
884
|
+
// bakes, whatever its own def says short of an explicit `lightmap: true`
|
|
885
|
+
const build = async (name: string, nd: SceneNodeDef, parentNode: Node | null, parentPath: string, parentMoves = false): Promise<void> => {
|
|
864
886
|
// names are path segments — '/' would fork the path, ':' would ambiguate editor card keys
|
|
865
887
|
if (name === "" || name.includes("/") || name.includes(":")) {
|
|
866
888
|
throw new Error(`Scene node name "${name}" is invalid — names are non-empty and contain no '/' or ':'`)
|
|
@@ -868,10 +890,9 @@ const buildNodes = async (
|
|
|
868
890
|
// the path is derived from def keys BEFORE any await, so it is deterministic even though
|
|
869
891
|
// Promise.all makes build completion (and record insertion) order nondeterministic
|
|
870
892
|
const path = parentPath === "" ? name : `${parentPath}/${name}`
|
|
871
|
-
const lmStatic = lm !== null &&
|
|
893
|
+
const lmStatic = lm !== null && (nd.lightmap ?? (!parentMoves && isLightmapStatic(nd)))
|
|
872
894
|
// a mover in a level with a light volume reads the volume (models: the engine binds it; meshes: registered below)
|
|
873
|
-
const
|
|
874
|
-
const node = await createSource(path, nd, lmStatic ? true : lmDynamic ? "dynamic" : undefined)
|
|
895
|
+
const node = await createSource(path, nd, lmStatic)
|
|
875
896
|
// a `mount` def parents to an internal part of the parent asset — safe here: the parent's
|
|
876
897
|
// model was awaited and attachPrefab completed before its children build
|
|
877
898
|
if (parentNode) attachHost(parentNode, nd, path).add(node)
|
|
@@ -879,14 +900,15 @@ const buildNodes = async (
|
|
|
879
900
|
scene.add(node)
|
|
880
901
|
applyNode(node, name, nd) // engine-side name stays the bare sibling segment
|
|
881
902
|
if (lmStatic && (node instanceof Model || node instanceof Mesh)) Lightmap._register(node, lm!.prefix + path)
|
|
882
|
-
else if (lmStatic &&
|
|
883
|
-
|
|
903
|
+
else if (lmStatic && nd.terrain !== undefined) Lightmap._registerGround(node, lm!.prefix + path)
|
|
904
|
+
// the top of a moving subtree: everything under it reads the level's light grid (Lightmap.track walks it at load)
|
|
905
|
+
if (lm !== null && !lmStatic && !parentMoves) moverRoots.push(node)
|
|
884
906
|
if (isEditMode()) addEditorMarker(node, nd)
|
|
885
907
|
// a prefab under a static wrapper bakes; under a mover its internals stay movers (volume-lit)
|
|
886
|
-
if (nd.prefab !== undefined) await attachPrefab(node, path, nd, scene, stack, lmStatic ? { prefix: `${lm!.prefix}${path}
|
|
908
|
+
if (nd.prefab !== undefined) await attachPrefab(node, path, nd, scene, stack, lmStatic ? { prefix: `${lm!.prefix}${path}/` } : null)
|
|
887
909
|
pending.push({ path, node, def: nd })
|
|
888
910
|
nodes[path] = node
|
|
889
|
-
await Promise.all(Object.entries(nd.children ?? {}).map(([ childName, child ]) => build(childName, child, node, path)))
|
|
911
|
+
await Promise.all(Object.entries(nd.children ?? {}).map(([ childName, child ]) => build(childName, child, node, path, parentMoves || (lm !== null && !lmStatic))))
|
|
890
912
|
}
|
|
891
913
|
|
|
892
914
|
await Promise.all(Object.entries(defs).map(([ name, nd ]) => build(name, nd, parent, "")))
|
|
@@ -913,6 +935,8 @@ const buildNodes = async (
|
|
|
913
935
|
for (const h of td.holes ?? []) t.hole(h)
|
|
914
936
|
t.commit()
|
|
915
937
|
}
|
|
938
|
+
// the movers read the level's light grid: their subtrees are whole by now (prefab internals, children)
|
|
939
|
+
for (const n of moverRoots) Lightmap.track(n)
|
|
916
940
|
}
|
|
917
941
|
|
|
918
942
|
// ---- navigation (env.navmesh) ------------------------------------------------------------------
|
|
@@ -941,14 +965,14 @@ const registerNavOverrides = (defs: Record<string, SceneNodeDef> | undefined, no
|
|
|
941
965
|
}
|
|
942
966
|
|
|
943
967
|
const instantiate = async (def: SceneDef, editorRuns: EditorRun[]): Promise<{ scene: Scene, nodes: Record<string, Node> }> => {
|
|
944
|
-
const { lightmap, navmesh, ...env } = def.env ?? {}
|
|
968
|
+
const { lightmap, navmesh, foliage, ...env } = def.env ?? {}
|
|
945
969
|
const scene = new Scene(env)
|
|
970
|
+
// the vegetation tier's level-wide knobs (wind, sway, variation) — before the models load
|
|
971
|
+
if (foliage) Foliage.configure(foliage)
|
|
946
972
|
const nodes: Record<string, Node> = {}
|
|
947
973
|
// baked lighting is a play-mode concern; the level owns the registry (a rebuild starts clean)
|
|
948
|
-
const lm: LightmapCtx | null = lightmap && !isEditMode() ? { prefix: ""
|
|
974
|
+
const lm: LightmapCtx | null = lightmap && !isEditMode() ? { prefix: "" } : null
|
|
949
975
|
if (lm) Lightmap.clear()
|
|
950
|
-
// with a light volume every model loaded from now on (weapons, spawns, make() subtrees) is a mover by default
|
|
951
|
-
if (lm) Model._lightmapDefault = lm.volume ? "dynamic" : false
|
|
952
976
|
await buildNodes(def.nodes ?? {}, null, scene, nodes, editorRuns, new Set([ def ]), lm)
|
|
953
977
|
if (navmesh && !isEditMode()) registerNavOverrides(def.nodes, nodes)
|
|
954
978
|
|
|
@@ -981,9 +1005,8 @@ const instantiate = async (def: SceneDef, editorRuns: EditorRun[]): Promise<{ sc
|
|
|
981
1005
|
|
|
982
1006
|
// the level is complete: apply the bake — or, under `lecodes lightmap bake`, run it
|
|
983
1007
|
if (lm && lightmap) {
|
|
984
|
-
await Lightmap.load(scene, { data: lightmap.data,
|
|
985
|
-
|
|
986
|
-
lightBoost: lightmap.lightBoost, volumeBoost: lightmap.volumeBoost })
|
|
1008
|
+
await Lightmap.load(scene, { data: lightmap.data, light: lightmap.light, aux: lightmap.aux, direct: lightmap.direct, indirect: lightmap.indirect, probes: lightmap.probes, volume: lightmap.volume },
|
|
1009
|
+
{ lightScale: lightmap.lightScale, emissiveNits: lightmap.emissiveNits, emissiveBake: lightmap.emissiveBake, transmit: lightmap.transmit })
|
|
987
1010
|
}
|
|
988
1011
|
// navigation: load the baked mesh — or, under `lecodes navmesh bake`, dump the level's collision
|
|
989
1012
|
// geometry for it (the static bodies exist by now; edit mode never loads it — v1)
|
|
@@ -1112,11 +1135,14 @@ export class SceneHandle<D extends SceneDef = SceneDef> {
|
|
|
1112
1135
|
if (parent) attachHost(parent, d, p).add(existing)
|
|
1113
1136
|
return existing
|
|
1114
1137
|
}
|
|
1115
|
-
const node = await createSource(p, d,
|
|
1138
|
+
const node = await createSource(p, d, false)
|
|
1116
1139
|
if (parent) attachHost(parent, d, p).add(node)
|
|
1117
1140
|
scene.add(node)
|
|
1118
1141
|
applyNode(node, p.slice(p.lastIndexOf("/") + 1), d)
|
|
1119
1142
|
if (isEditMode()) addEditorMarker(node, d)
|
|
1143
|
+
// a prefab wrapper instantiates its file's nodes (same path as buildNodes; no lightmap
|
|
1144
|
+
// context — a patched node is an edit-mode rebuild, baked lighting is the level's business)
|
|
1145
|
+
if (d.prefab !== undefined) await attachPrefab(node, p, d, scene, new Set([ this.def ]), null)
|
|
1120
1146
|
attachAspects(node, p, d, nodes, scene, this._editorRuns)
|
|
1121
1147
|
nodes[p] = node
|
|
1122
1148
|
await Promise.all(Object.entries(d.children ?? {}).map(([ cn, cd ]) => build(`${p}/${cn}`, cd, node)))
|
package/src/scene/level.ts
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
|
|
15
15
|
import { Node } from "../gl/Node"
|
|
16
16
|
import { Model } from "../gl/Model"
|
|
17
|
-
import { Shape } from "../gl/Shape"
|
|
18
|
-
import { Physics } from "../gl/Physics"
|
|
17
|
+
import { Shape } from "../gl/physics/Shape"
|
|
18
|
+
import { Physics } from "../gl/physics/Physics"
|
|
19
19
|
import { cx, cy, cz, type Vec3Like } from "../math/vec"
|
|
20
20
|
|
|
21
21
|
/** One placement handed to a `build` function (all coordinates local to the group). */
|
package/src/ui/UIButton.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { ClickEvent, TouchStartEvent, LongPressEvent } from "../runtime/touch"
|
|
2
2
|
import type { ClickEvent as ClickEventType, TouchStartEvent as TouchStartEventType, LongPressEvent as LongPressEventType } from "../runtime/touch"
|
|
3
|
-
import { type ElementStyle, type ContainerStyle, ContainerElement, buildUI, type DrawableStyle, type UIChildArg, type UIContainerBase, type UINodeChild, type ChildrenFn, type BaseStyle, type Color } from "./UINode"
|
|
3
|
+
import { type ElementStyle, type ContainerStyle, ContainerElement, buildUI, type DrawableStyle, type UIChildArg, type UIContainerBase, type UINodeChild, type ChildrenFn, type BaseStyle, type Color, type LayerEasing } from "./UINode"
|
|
4
4
|
|
|
5
5
|
export type UIButtonStyle = ElementStyle & DrawableStyle & ContainerStyle & { rippleColor?: Color | "default" }
|
|
6
6
|
|
|
7
7
|
/** A tappable container — row + centered children by default. Press feedback via the `onPressed`
|
|
8
8
|
* style block, the `$pressed` class, or `rippleColor`. */
|
|
9
|
-
export interface UIButton extends UIContainerBase<UIButtonStyle & { onPressed?: UIButtonStyle & { duration?: number } }, DrawableStyle & BaseStyle> {
|
|
9
|
+
export interface UIButton extends UIContainerBase<UIButtonStyle & { onPressed?: UIButtonStyle & { duration?: number, delay?: number, easing?: LayerEasing } }, DrawableStyle & BaseStyle> {
|
|
10
10
|
readonly type: "button",
|
|
11
11
|
|
|
12
12
|
/** Fired on a completed tap/click. */
|
package/src/ui/UIInput.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ElementStyle, type DrawableStyle, Element, type TextStyle, type Color, type BaseStyle, type UIElementBase } from "./UINode";
|
|
1
|
+
import { type ElementStyle, type DrawableStyle, Element, type TextStyle, type Color, type BaseStyle, type UIElementBase, type LayerEasing } from "./UINode";
|
|
2
2
|
|
|
3
3
|
type InputStyle = {
|
|
4
4
|
placeholder?: string,
|
|
@@ -51,7 +51,7 @@ type SubmitStyle = {
|
|
|
51
51
|
|
|
52
52
|
/** A single-line text input. Keyboard/picker kind via the `type` style, return-key behavior via
|
|
53
53
|
* `enterKey` + `onSubmit`; keyboard policy on `keyboardShrink`/`keyboardDismiss`. */
|
|
54
|
-
export interface UIInput extends UIElementBase<UIInputStyle & SubmitStyle & { onFocused?: UIInputStyle & { duration?: number } }, DrawableStyle & BaseStyle> {
|
|
54
|
+
export interface UIInput extends UIElementBase<UIInputStyle & SubmitStyle & { onFocused?: UIInputStyle & { duration?: number, delay?: number, easing?: LayerEasing } }, DrawableStyle & BaseStyle> {
|
|
55
55
|
readonly type: "input",
|
|
56
56
|
|
|
57
57
|
/**
|
|
@@ -78,7 +78,7 @@ export interface UIInput extends UIElementBase<UIInputStyle & SubmitStyle & { on
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
/** A multi-line text field — see the {@link UITextArea} factory for the auto-grow contract. */
|
|
81
|
-
export interface UITextArea extends UIElementBase<UIInputStyle & { onFocused?: UIInputStyle & { duration?: number } }, DrawableStyle & BaseStyle> {
|
|
81
|
+
export interface UITextArea extends UIElementBase<UIInputStyle & { onFocused?: UIInputStyle & { duration?: number, delay?: number, easing?: LayerEasing } }, DrawableStyle & BaseStyle> {
|
|
82
82
|
readonly type: "textarea",
|
|
83
83
|
|
|
84
84
|
/** The field's text — same contract as {@link UIInput.value}: settable before mount to pre-fill. */
|
package/src/ui/UINode.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { FetchResponse } from "../runtime/fetch"
|
|
2
2
|
import { createBinding, disposeBinding } from "../core/signals"
|
|
3
|
+
import type { Animation } from "../animate/tween/Animation"
|
|
4
|
+
import { CLOCK_UI, DOM_UI, KIND_DISCRETE, KIND_TRANSFORM, KIND_TRANSFORM_MATRIX, parseTransformList, uiValue, type TweenChannel, type TweenMeta } from "../animate/tween/spec"
|
|
5
|
+
import { tweenBag } from "../animate/tween/Timeline"
|
|
3
6
|
|
|
4
7
|
export interface UINode {
|
|
5
8
|
readonly type: string
|
|
@@ -330,8 +333,13 @@ const classHandler: ProxyHandler<any> = {
|
|
|
330
333
|
* static — bindings are extracted at the top level only. */
|
|
331
334
|
export type Reactive<T> = { [K in keyof T]: NonNullable<T[K]> extends object ? T[K] : T[K] | (() => T[K]) }
|
|
332
335
|
|
|
336
|
+
/** The curve of a state block's swap (`$class` / `onPressed` / `onFocused` with a `duration`): a
|
|
337
|
+
* curve name (`outCubic`, `inOutSine`, `smoothstep` — the default — …), `cubic-bezier(x1,y1,x2,y2)`
|
|
338
|
+
* or `steps(n)`. A function cannot ride a style block; use `animateTo` for those. */
|
|
339
|
+
export type LayerEasing = string
|
|
340
|
+
|
|
333
341
|
/** User-defined style classes: any `$`-prefixed key in `.style()` declares a state block toggled
|
|
334
|
-
* from code via the `el.class` proxy; `duration`/`delay` animate the swap. Class state INHERITS
|
|
342
|
+
* from code via the `el.class` proxy; `duration`/`delay`/`easing` animate the swap. Class state INHERITS
|
|
335
343
|
* down the tree (CSS-`.dark`-on-body style): a class set on a node also activates same-name `$`
|
|
336
344
|
* blocks on all descendants within the same root (docs/style-class-cascade-plan.md).
|
|
337
345
|
* `$pressed`/`$focused` are reserved — hosts toggle them on press/focus (they beat other
|
|
@@ -340,7 +348,7 @@ export type Reactive<T> = { [K in keyof T]: NonNullable<T[K]> extends object ? T
|
|
|
340
348
|
* Instantiated with the node's FULL style `T` — a class block accepts everything the node's
|
|
341
349
|
* `.style()` does, like `onLandscape`. Narrowing it wrongly rejects valid props such as
|
|
342
350
|
* `$active: { color }`; guarded by tests/ui-types.test.ts. */
|
|
343
|
-
export type ClassStyles<T> = { [key: `$${string}`]: T & { duration?: number, delay?: number } }
|
|
351
|
+
export type ClassStyles<T> = { [key: `$${string}`]: T & { duration?: number, delay?: number, easing?: LayerEasing } }
|
|
344
352
|
|
|
345
353
|
/** Value accepted by a `el.class` write: a boolean sets the class, a `() => boolean` binds it —
|
|
346
354
|
* the class then tracks the signals the function reads (re-evaluated on change). Reads through
|
|
@@ -425,28 +433,15 @@ export type StyleFn <R, T extends object> = ((style: Reactive<T> & OrientationSt
|
|
|
425
433
|
* installs a reactive binding). */
|
|
426
434
|
export type Style <R, T extends object> = StyleFn<R,T> & T & OrientationStyles<T> & ClassStyles<T>
|
|
427
435
|
|
|
428
|
-
/**
|
|
429
|
-
* (
|
|
430
|
-
export type
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
/** `false` = animate without writing the targets into the element's style (e.g. a fade-out right before `.hide()`). */
|
|
438
|
-
commit?: boolean,
|
|
439
|
-
/** Repeat the animation: `true` = forever, a number = that many cycles. A looping animation is an
|
|
440
|
-
* effect, not a state change — it never commits, and the element ends back at its base style.
|
|
441
|
-
* The loop stops when a later `animateTo`/`animateFrom` runs on the element or it leaves the screen. */
|
|
442
|
-
loop?: boolean | number,
|
|
443
|
-
/** `"ping-pong"` (default) animates there and back each cycle — no jump. `"restart"` snaps back and
|
|
444
|
-
* replays forward — for full-turn spinners (`transform: "rotate(360deg)"`) and shimmers. Only meaningful with `loop`. */
|
|
445
|
-
loopMode?: "restart" | "ping-pong",
|
|
446
|
-
}) => R)
|
|
447
|
-
|
|
448
|
-
/** Meta keys of the animateTo/animateFrom options bag — everything that is not a style target. */
|
|
449
|
-
const ANIMATE_META = new Set(["duration", "delay", "layer", "commit", "loop", "loopMode"])
|
|
436
|
+
/** Style props of an animate bag: a single value (tween from the current value) or an array of
|
|
437
|
+
* KEYFRAMES (`opacity: [1, 0.3, 1]`, offsets via `times`). */
|
|
438
|
+
export type AnimateProps<T extends object> = { [K in keyof T]?: T[K] | T[K][] }
|
|
439
|
+
|
|
440
|
+
/** The `animateTo`/`animateFrom` surface: target style props (single values or keyframe arrays) plus
|
|
441
|
+
* the flat meta keys — `duration` / `delay` / `easing` / `times` / `loop` / `loopMode` / `commit` /
|
|
442
|
+
* `clock`, see {@link TweenMeta}. Returns the {@link Animation} handle (seek, rate, `finished`). */
|
|
443
|
+
export type AnimateStyle<R, T extends object> = ((style: AnimateProps<T> & TweenMeta) => Animation)
|
|
444
|
+
|
|
450
445
|
|
|
451
446
|
export type AppearStyle <R, T> = (arg: { from: T, duration?: number, delay?: number }) => R
|
|
452
447
|
export type DisappearStyle <R, T> = (arg: { to: T, duration?: number, delay?: number }) => R
|
|
@@ -518,20 +513,50 @@ export class Element<T extends string> {
|
|
|
518
513
|
}
|
|
519
514
|
|
|
520
515
|
/** Tween to the target style and commit it (unless `commit: false` or looping — a loop is an
|
|
521
|
-
* effect, not a state change).
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
516
|
+
* effect, not a state change). A prop value may be an array of keyframes; meta keys
|
|
517
|
+
* `duration`/`delay`/`easing`/`times`/`loop`/… — see {@link AnimateStyle}. Returns the
|
|
518
|
+
* {@link Animation} handle. */
|
|
519
|
+
animateTo(style: any): Animation {
|
|
520
|
+
// The last keyframe commits into the stored style at play (Animation.play) — reads and later
|
|
521
|
+
// `.style()` merges see the final state while the host is still tweening, as before.
|
|
522
|
+
return tweenBag(this, style, true)
|
|
523
|
+
}
|
|
524
|
+
/** Tween FROM the given style to the element's current one (entrance animations): the element's
|
|
525
|
+
* own style is the implicit last keyframe. Never modifies the stored style. */
|
|
526
|
+
animateFrom(style: any): Animation {
|
|
527
|
+
return tweenBag(this, style, false)
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** @internal Keyframe-core target protocol (animate/tween/spec.ts): every style prop is a channel
|
|
531
|
+
* on the UI domain; `text` is a discrete channel on text elements. */
|
|
532
|
+
readonly _tweenClock = CLOCK_UI
|
|
533
|
+
/** @internal */
|
|
534
|
+
_tweenChannel(prop: string): TweenChannel | null {
|
|
535
|
+
if (prop === "text") {
|
|
536
|
+
if (!("text" in this)) return null
|
|
537
|
+
return {
|
|
538
|
+
domain: DOM_UI, id: () => this._id,
|
|
539
|
+
value: (v) => (typeof v === "string" || typeof v === "number" ? { kind: KIND_DISCRETE, str: String(v) } : null),
|
|
540
|
+
commit: (v) => { (this as any).text = String(v) },
|
|
527
541
|
}
|
|
528
542
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
543
|
+
if (prop === "transform") {
|
|
544
|
+
// A function list tweens per argument when every key shares the list; anything else is handed
|
|
545
|
+
// to the host as a raw string for matrix decomposition (its old path).
|
|
546
|
+
return {
|
|
547
|
+
domain: DOM_UI, id: () => this._id,
|
|
548
|
+
value: (v) => {
|
|
549
|
+
if (typeof v !== "string") return null
|
|
550
|
+
const list = parseTransformList(v)
|
|
551
|
+
return list ? { kind: KIND_TRANSFORM, sig: list.sig, lanes: list.lanes } : { kind: KIND_TRANSFORM_MATRIX, str: v }
|
|
552
|
+
},
|
|
553
|
+
commit: (v) => { this._style.transform = v },
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return {
|
|
557
|
+
domain: DOM_UI, id: () => this._id, value: uiValue,
|
|
558
|
+
commit: (v) => { this._style[prop] = v },
|
|
559
|
+
}
|
|
535
560
|
}
|
|
536
561
|
|
|
537
562
|
/** @internal Active style-class names (stored without the `$`). Kept on the element — not just in
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
type mat4 = Float32Array | number[];
|
|
2
|
-
type quat = Float32Array | number[];
|
|
3
|
-
type vec2 = Float32Array | number[];
|
|
4
|
-
type vec3 = Float32Array | number[];
|
|
5
|
-
import type { Easing } from "./easings";
|
|
6
|
-
type AnimateProps = number | string | vec2 | vec3 | quat;
|
|
7
|
-
export type AnimateOptions<T> = {
|
|
8
|
-
from: T;
|
|
9
|
-
to: T;
|
|
10
|
-
duration: number;
|
|
11
|
-
onUpdate(val: T): void;
|
|
12
|
-
onComplete?(): void;
|
|
13
|
-
easing?: Easing;
|
|
14
|
-
};
|
|
15
|
-
export declare const animate: <T extends AnimateProps>(options: AnimateOptions<T>) => number;
|
|
16
|
-
export declare const stopAnimation: (id: number) => void;
|
|
17
|
-
export declare const pauseAnimation: (id: number) => void;
|
|
18
|
-
export declare const resumeAnimation: (id: number) => void;
|
|
19
|
-
export declare const animateMat4: (options: AnimateOptions<mat4>) => number;
|
|
20
|
-
export {};
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { Aspect } from "../core/Aspect";
|
|
2
|
-
import type { FieldMeta } from "../core/fields";
|
|
3
|
-
import type { Node } from "./Node";
|
|
4
|
-
import type { Vehicle } from "./Vehicle";
|
|
5
|
-
export declare class Gearbox extends Aspect<"gearbox", Node> {
|
|
6
|
-
static readonly aspect = "gearbox";
|
|
7
|
-
/** Forward gear ratios, first gear first. Engine RPM = wheel speed × ratio × the differential. */
|
|
8
|
-
ratios: number[];
|
|
9
|
-
/** Reverse ratio — negative, because a ratio's SIGN is what reverse means to the engine. */
|
|
10
|
-
reverse: number;
|
|
11
|
-
/** Shift up when the engine passes this fraction of its redline. */
|
|
12
|
-
shiftUp: number;
|
|
13
|
-
/** Shift down when it falls below this fraction. */
|
|
14
|
-
shiftDown: number;
|
|
15
|
-
/** How long a shift takes, in seconds — the clutch is out for this long. */
|
|
16
|
-
shiftTime: number;
|
|
17
|
-
/** How long the clutch takes to re-engage afterwards. */
|
|
18
|
-
clutchTime: number;
|
|
19
|
-
/** How long to wait after a shift before another is allowed — without it a car sitting on a
|
|
20
|
-
* threshold hunts between two gears. */
|
|
21
|
-
latency: number;
|
|
22
|
-
/** How far the clutch bites the instant drive is asked for from rest, 0..1. It only has to COUPLE
|
|
23
|
-
* the shafts — how much torque that passes is the engine's business, so a feathered throttle
|
|
24
|
-
* still pulls away gently at the same bite point. */
|
|
25
|
-
bite: number;
|
|
26
|
-
static fields: FieldMeta<Gearbox>;
|
|
27
|
-
/** Current gear: −1 reverse, 0 neutral, 1… forward. Read it for a HUD. */
|
|
28
|
-
gear: number;
|
|
29
|
-
/** True while a shift is in progress — the clutch is out and the engine is not driving the wheels. */
|
|
30
|
-
get shifting(): boolean;
|
|
31
|
-
private _shiftLeft;
|
|
32
|
-
private _engageLeft;
|
|
33
|
-
private _waitLeft;
|
|
34
|
-
private _take;
|
|
35
|
-
/** EARLY phase: the pair has to be latched before this frame's step consumes it. */
|
|
36
|
-
updateBefore(dt: number): void;
|
|
37
|
-
/**
|
|
38
|
-
* The driver's left foot: where the clutch pedal is, ignoring shifts. Returns the new engagement.
|
|
39
|
-
*
|
|
40
|
-
* A driver does not compute anything. They watch the tacho: **revs sinking to idle means the
|
|
41
|
-
* wheels are about to drag the engine under, and that is when you push the clutch in.** That
|
|
42
|
-
* single test replaces every ratio and threshold — the crossover it finds is the exact speed where
|
|
43
|
-
* engine braking stops and the engine would start PUSHING instead, and it finds it per gear
|
|
44
|
-
* without being told the gear.
|
|
45
|
-
*
|
|
46
|
-
* The pedal comes back UP only for the throttle. Healthy revs are not on their own a reason to
|
|
47
|
-
* engage: a car standing still shows healthy revs too — the wheels simply are not turning to say
|
|
48
|
-
* otherwise — and a clutch dropped on that evidence lurches the car off with no throttle at all.
|
|
49
|
-
*
|
|
50
|
-
* Three things fall out of it for free:
|
|
51
|
-
*
|
|
52
|
-
* - **A braked car stops.** The engine cannot stall, so a clutch left in at 0 km/h feeds idle
|
|
53
|
-
* torque to the wheels forever and the car creeps against its own brakes. Here the revs pin at
|
|
54
|
-
* idle, so the pedal goes down and the brakes have nothing to fight.
|
|
55
|
-
* - **Reverse is not a special case.** No ratio takes part, so it needs no separate number — which
|
|
56
|
-
* is exactly why a fixed road-speed threshold got reverse wrong.
|
|
57
|
-
* - **Hill starts judder instead of stalling.** Let the pedal out, the load pulls the revs down,
|
|
58
|
-
* the rule pushes it straight back in, and the clutch sits slipping at the bite point until the
|
|
59
|
-
* car moves. So the test deliberately ignores the throttle — gating it on "no throttle" would
|
|
60
|
-
* stall the car under load. Throttle only holds the floor at `bite`.
|
|
61
|
-
*
|
|
62
|
-
* The cost is honest and small: once the pedal is down the revs sit at idle and cannot rise on
|
|
63
|
-
* their own, so **coasting downhill gives no engine braking until you touch the throttle** — which
|
|
64
|
-
* is precisely what a car with the clutch in does. Slipping it permanently to keep listening would
|
|
65
|
-
* bring the creep straight back.
|
|
66
|
-
*/
|
|
67
|
-
protected takeUp(v: Vehicle, dt: number): number;
|
|
68
|
-
/**
|
|
69
|
-
* Which gear the car should be in. The default is an ordinary automatic: pull away from neutral in
|
|
70
|
-
* the direction of the throttle, shift up past `shiftUp` of the redline and down below `shiftDown`,
|
|
71
|
-
* and refuse to shift again for `latency` seconds. Override this and nothing else.
|
|
72
|
-
*/
|
|
73
|
-
select(v: Vehicle): number;
|
|
74
|
-
/** Change gear now, with the clutch envelope. Safe to call from `select()` or from game code
|
|
75
|
-
* (a sequential manual is `if (Input.key('KeyE')) car.gearbox.shift(car.gearbox.gear + 1)`). */
|
|
76
|
-
shift(gear: number): void;
|
|
77
|
-
/** The ratio the engine should be running: 0 in neutral, negative in reverse. */
|
|
78
|
-
get ratio(): number;
|
|
79
|
-
/**
|
|
80
|
-
* The clutch scalar 0..1 the engine is given: the SHIFT envelope (out during a shift, ramping back
|
|
81
|
-
* over `clutchTime`) times the driver's pedal (`takeUp`). Two independent reasons for the clutch
|
|
82
|
-
* to be out, so they multiply: mid-shift is out however healthy the revs are, and an engine being
|
|
83
|
-
* dragged to idle is out whatever gear it just picked.
|
|
84
|
-
*/
|
|
85
|
-
get clutch(): number;
|
|
86
|
-
}
|