@waica/archetype-third-person 0.30.0 → 0.31.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/README.md CHANGED
@@ -19,6 +19,11 @@ npx @waica/cli # New project → 3D → Third person
19
19
  - **Camera-relative controls.** `CharacterMotor.relativeTo: 'camera'`:
20
20
  `W` walks away from the camera. `Shift` runs, `C` rolls (invulnerable while
21
21
  it lasts), `Space` jumps, a click or `J` attacks.
22
+ - **Animation.** The Knight's idle, walk and run play one `Locomotion`
23
+ blend (ADR 0033): `Idle`, `Walking_A` and `Running_A` mixed by the planar
24
+ speed the `character` role writes to the Animator, so a half stick walks
25
+ slowly without its feet sliding and walk turns into run smoothly. The
26
+ attack and the dodge fade in over 0.05 s, the death over 0.3 s.
22
27
  - **Combat.** `MeleeAttack` strikes a box in front of the Knight's yaw;
23
28
  skeletons (the `enemy` role) patrol, chase, wind up, strike, flinch and
24
29
  die. Hits push back, blink the model and shake the camera.
@@ -30,6 +35,31 @@ npx @waica/cli # New project → 3D → Third person
30
35
  goal chest on a raised platform: reach it to win. The blank start is a
31
36
  ground plane, a Sun and the playable Knight.
32
37
 
38
+ ## Shadows
39
+
40
+ Demo and blank explicitly use `render.shadows: { quality: 'medium', maxDistance: 40 }`.
41
+ Every Model casts/receives; the Sun and all four demo torches cast. Static and
42
+ animated glTF assets retain their materials and gameplay.
43
+
44
+ Profiles (Sun / PointLight per face): Low 1024²/256², Medium 2048²/512²,
45
+ High 4096²/1024². Set `render.shadows.quality: 'off'` to restore the previous
46
+ shadow-free image/cost. Existing 3D scenes change; 2D projects do not.
47
+
48
+ Sun follows the active camera within finite reach. Torch shadows use the smaller
49
+ of lighting distance and shadow reach, without shortening illumination. Each
50
+ PointLight can add six views, hence 25 for four torches plus Sun. No FPS-based
51
+ light caps or universal frame-rate guarantee are introduced.
52
+
53
+ The editor exposes scene quality/reach and nullable per-light overrides. `null`
54
+ inherits the scene, not zero; resetting an instance to its prefab is separate.
55
+ See the [engine format](../engine/README.md#3d-shadows-and-migration) and
56
+ [benchmark notes](../bench/README.md). WebGPU remains unverified on this host;
57
+ its two external evidence gates were withdrawn from this delivery by the user,
58
+ not marked as passing. Implementation and strict tests remain available.
59
+ Automatic CI requires the new visual shadow checks on WebGL2; request their
60
+ strict WebGPU counterparts with `pnpm test:e2e --webgpu-shadows`. Legacy backend
61
+ checks remain required on both APIs.
62
+
33
63
  ## Controls
34
64
 
35
65
  | Action | Keys | Gamepad |
package/dist/prefabs.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import type { PrefabJson } from '@waica/engine';
2
- /** The Knight's clip for each `character` state (assets/ATTRIBUTION.md). */
2
+ /**
3
+ * The Knight's clip for each `character` state (assets/ATTRIBUTION.md): idle,
4
+ * walk and run reach theirs through the Locomotion blend (issue #205).
5
+ */
3
6
  export declare const KNIGHT_CLIPS: Readonly<Record<string, string>>;
4
7
  /** The Skeleton Warrior's clip for each `enemy` state (assets/ATTRIBUTION.md). */
5
8
  export declare const SKELETON_CLIPS: Readonly<Record<string, string>>;
package/dist/prefabs.js CHANGED
@@ -3,7 +3,10 @@ import { CHARACTER_STATE_GRAPH, ENEMY_STATE_GRAPH } from '@waica/behaviors';
3
3
  function withClips(states, clips) {
4
4
  return Object.fromEntries(Object.entries(states).map(([name, state]) => [name, clips[name] ? { ...state, clip: clips[name] } : state]));
5
5
  }
6
- /** The Knight's clip for each `character` state (assets/ATTRIBUTION.md). */
6
+ /**
7
+ * The Knight's clip for each `character` state (assets/ATTRIBUTION.md): idle,
8
+ * walk and run reach theirs through the Locomotion blend (issue #205).
9
+ */
7
10
  export const KNIGHT_CLIPS = {
8
11
  idle: 'Idle',
9
12
  walk: 'Walking_A',
@@ -31,19 +34,50 @@ function fixedBox(size, offset = [0, size[1] / 2, 0]) {
31
34
  function capsule(height, layer, strikes) {
32
35
  return { type: 'Collider', props: { shape: 'capsule', radius: 0.5, height, offset: [0, height / 2, 0], layer, collidesWith: [strikes] } };
33
36
  }
37
+ /** Every character and level piece participates in the scene's shadows. */
38
+ function gltfModel(src) {
39
+ return { type: 'Model', props: { src, castShadows: true, receiveShadows: true } };
40
+ }
41
+ /**
42
+ * The Knight's walk cycle (issue #205 CA-15): Idle, Walking_A and Running_A
43
+ * mixed by the planar speed the `character` role writes, at the motor's
44
+ * speeds (still, `speed` 6 and `runSpeed` 10), so a half stick walks slowly
45
+ * without sliding its feet. idle, walk and run all play it: switching among
46
+ * them keeps it playing, and the smoothed speed does the transition.
47
+ */
48
+ const LOCOMOTION = 'Locomotion';
49
+ const LOCOMOTION_BLEND = {
50
+ param: 'speed',
51
+ points: [{ clip: KNIGHT_CLIPS.idle, at: 0 }, { clip: KNIGHT_CLIPS.walk, at: 6 }, { clip: KNIGHT_CLIPS.run, at: 10 }],
52
+ smoothing: 0.15,
53
+ };
34
54
  const PLAYER = {
35
55
  waicaPrefab: 1,
36
56
  type: 'character',
37
57
  components: [
38
- { type: 'Model', props: { src: 'waica:tp-knight' } },
58
+ gltfModel('waica:tp-knight'),
39
59
  {
40
60
  type: 'Animator',
41
61
  props: {
42
62
  initialClip: 'Idle',
43
- clips: { Jump_Start: { loop: false }, '1H_Melee_Attack_Chop': { loop: false }, Dodge_Forward: { loop: false }, Death_A: { loop: false } },
63
+ // The attack and the dodge answer the press at once; the death settles in slower.
64
+ clips: {
65
+ Jump_Start: { loop: false },
66
+ '1H_Melee_Attack_Chop': { loop: false, fade: 0.05 },
67
+ Dodge_Forward: { loop: false, fade: 0.05 },
68
+ Death_A: { loop: false, fade: 0.3 },
69
+ },
70
+ blends: { [LOCOMOTION]: LOCOMOTION_BLEND },
71
+ },
72
+ },
73
+ {
74
+ type: 'StateMachine',
75
+ props: {
76
+ role: 'player',
77
+ initial: CHARACTER_STATE_GRAPH.initial,
78
+ states: withClips(CHARACTER_STATE_GRAPH.states, { ...KNIGHT_CLIPS, idle: LOCOMOTION, walk: LOCOMOTION, run: LOCOMOTION }),
44
79
  },
45
80
  },
46
- { type: 'StateMachine', props: { role: 'player', initial: CHARACTER_STATE_GRAPH.initial, states: withClips(CHARACTER_STATE_GRAPH.states, KNIGHT_CLIPS) } },
47
81
  capsule(2.3, 'player', 'enemy'),
48
82
  { type: 'RigidBody', props: { type: 'kinematic' } },
49
83
  { type: 'CharacterMotor', props: { relativeTo: 'camera' } },
@@ -60,7 +94,7 @@ const SKELETON = {
60
94
  waicaPrefab: 1,
61
95
  type: 'character',
62
96
  components: [
63
- { type: 'Model', props: { src: 'waica:tp-skeleton-warrior' } },
97
+ gltfModel('waica:tp-skeleton-warrior'),
64
98
  {
65
99
  type: 'Animator',
66
100
  props: { initialClip: 'Walking_A', clips: { '1H_Melee_Attack_Chop': { loop: false }, Hit_A: { loop: false }, Death_A: { loop: false } } },
@@ -100,7 +134,7 @@ export const THIRD_PERSON_PREFABS = {
100
134
  waicaPrefab: 1,
101
135
  type: 'object',
102
136
  components: [
103
- { type: 'Model', props: { src: 'waica:tp-banner' } },
137
+ gltfModel('waica:tp-banner'),
104
138
  // A wall banner: its origin on the wall, its sensor and spawn on the floor in front of the wall (local +z).
105
139
  { type: 'Collider', props: { shape: 'box', size: [2, 3, 2], offset: [0, 1.5, CHECKPOINT_REACH], sensor: true, layer: 'world' } },
106
140
  { type: 'Checkpoint', props: { spawnOffset: [0, 0, CHECKPOINT_REACH] } },
@@ -110,26 +144,26 @@ export const THIRD_PERSON_PREFABS = {
110
144
  waicaPrefab: 1,
111
145
  type: 'object',
112
146
  components: [
113
- { type: 'Model', props: { src: 'waica:tp-chest' } },
147
+ gltfModel('waica:tp-chest'),
114
148
  { type: 'Collider', props: { shape: 'box', size: [2.4, 2, 2.4], offset: [0, 1, 0], sensor: true, layer: 'world' } },
115
149
  { type: 'GoalChest' },
116
150
  ],
117
151
  },
118
- 'tiles/wall': { waicaPrefab: 1, type: 'tile', components: [{ type: 'Model', props: { src: 'waica:tp-wall' } }, fixedBox([4, 4, 1])] },
152
+ 'tiles/wall': { waicaPrefab: 1, type: 'tile', components: [gltfModel('waica:tp-wall'), fixedBox([4, 4, 1])] },
119
153
  'tiles/floor': {
120
154
  waicaPrefab: 1,
121
155
  type: 'tile',
122
- components: [{ type: 'Model', props: { src: 'waica:tp-floor' } }, fixedBox([4, 0.2, 4], [0, -0.05, 0])],
156
+ components: [gltfModel('waica:tp-floor'), fixedBox([4, 0.2, 4], [0, -0.05, 0])],
123
157
  },
124
- 'tiles/step': { waicaPrefab: 1, type: 'tile', components: [{ type: 'Model', props: { src: 'waica:tp-foundation' } }, fixedBox([2.2, 2, 2.2])] },
125
- 'tiles/column': { waicaPrefab: 1, type: 'tile', components: [{ type: 'Model', props: { src: 'waica:tp-pillar' } }, fixedBox([1.5, 4, 1.5])] },
158
+ 'tiles/step': { waicaPrefab: 1, type: 'tile', components: [gltfModel('waica:tp-foundation'), fixedBox([2.2, 2, 2.2])] },
159
+ 'tiles/column': { waicaPrefab: 1, type: 'tile', components: [gltfModel('waica:tp-pillar'), fixedBox([1.5, 4, 1.5])] },
126
160
  'tiles/torch': {
127
161
  waicaPrefab: 1,
128
162
  type: 'tile',
129
163
  components: [
130
- { type: 'Model', props: { src: 'waica:tp-torch' } },
164
+ gltfModel('waica:tp-torch'),
131
165
  fixedBox([0.6, 1.1, 0.7], [0, 0.15, 0.3]),
132
- { type: 'PointLight', props: { color: '#ffb060', intensity: 14, distance: 9, offsetY: 0.6, offsetZ: 0.6 } },
166
+ { type: 'PointLight', props: { color: '#ffb060', intensity: 14, distance: 9, offsetY: 0.6, offsetZ: 0.6, castShadows: true } },
133
167
  ],
134
168
  },
135
169
  };
@@ -19,14 +19,18 @@ function orbitCamera() {
19
19
  function threeDScene(entities) {
20
20
  return {
21
21
  waicaScene: 3,
22
- render: { space: '3d', lighting: { ambient: { color: '#b8c8ff', intensity: 0.55 } } },
22
+ render: {
23
+ space: '3d',
24
+ lighting: { ambient: { color: '#b8c8ff', intensity: 0.55 } },
25
+ shadows: { quality: 'medium', maxDistance: 40 },
26
+ },
23
27
  simulation: { gravity: [0, -9.81, 0] },
24
28
  camera: orbitCamera(),
25
29
  entities,
26
30
  ui: ['player-health', 'controls-hint'],
27
31
  };
28
32
  }
29
- const SUN = { name: 'Sun', components: [{ type: 'Sun', props: { direction: [-0.5, -1, -0.35], intensity: 2.2 } }] };
33
+ const SUN = { name: 'Sun', components: [{ type: 'Sun', props: { direction: [-0.5, -1, -0.35], intensity: 2.2, castShadows: true } }] };
30
34
  /** Half the courtyard's side: 5 × 5 floor tiles of 4 units, walls on the edge. */
31
35
  const HALF = 10;
32
36
  const TILE_CENTERS = [-8, -4, 0, 4, 8];
@@ -91,7 +95,7 @@ export const THIRD_PERSON_BLANK_SCENE = threeDScene([
91
95
  name: 'Ground',
92
96
  scale: [24, 1, 24],
93
97
  components: [
94
- { type: 'Model', props: { shape: 'plane', color: '#5a6b4e' } },
98
+ { type: 'Model', props: { shape: 'plane', color: '#5a6b4e', castShadows: true, receiveShadows: true } },
95
99
  { type: 'Collider', props: { shape: 'box', size: [1, 1, 1], offset: [0, -0.5, 0], layer: 'world' } },
96
100
  ],
97
101
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waica/archetype-third-person",
3
- "version": "0.30.0",
3
+ "version": "0.31.0",
4
4
  "description": "Waica third-person archetype — an orbit camera, camera-relative controls and 3D melee combat in a dungeon courtyard",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -24,7 +24,7 @@
24
24
  }
25
25
  },
26
26
  "dependencies": {
27
- "@waica/behaviors": "^0.30.0",
28
- "@waica/engine": "^0.30.0"
27
+ "@waica/behaviors": "^0.31.0",
28
+ "@waica/engine": "^0.31.0"
29
29
  }
30
30
  }