@dcl-regenesislabs/web-editor 0.1.1-37509646646.commit-126b34d → 0.1.1-38415884993.commit-9d6277f

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.
Files changed (46) hide show
  1. package/editor.js +1231 -464
  2. package/package.json +2 -2
  3. package/prefabs/announcer/ai.md +9 -2
  4. package/prefabs/announcer/data.json +6 -7
  5. package/prefabs/announcer/scripts/announcer.tsx +16 -37
  6. package/prefabs/catalog.json +1 -1
  7. package/prefabs/game-flow/ai.md +6 -0
  8. package/prefabs/game-flow/data.json +7 -3
  9. package/prefabs/game-flow/scripts/game-flow.ts +22 -5
  10. package/prefabs/health-respawn/ai.md +25 -2
  11. package/prefabs/health-respawn/data.json +8 -1
  12. package/prefabs/health-respawn/scripts/health-respawn.ts +52 -20
  13. package/prefabs/health-respawn/scripts/health.ts +21 -0
  14. package/prefabs/health-respawn/scripts/pure/life.ts +59 -0
  15. package/prefabs/leaderboard/ai.md +27 -9
  16. package/prefabs/leaderboard/composite.json +2 -2
  17. package/prefabs/leaderboard/data.json +14 -7
  18. package/prefabs/leaderboard/scripts/leaderboard-hud.tsx +28 -0
  19. package/prefabs/leaderboard/scripts/leaderboard.ts +41 -5
  20. package/prefabs/leaderboard/scripts/pure/board.ts +10 -3
  21. package/prefabs/run-progress/ai.md +101 -0
  22. package/prefabs/run-progress/composite.json +9 -0
  23. package/prefabs/run-progress/data.json +43 -0
  24. package/prefabs/run-progress/scripts/moving-platform.ts +28 -0
  25. package/prefabs/run-progress/scripts/run-progress.tsx +170 -0
  26. package/prefabs/run-progress/thumbnail.png +0 -0
  27. package/prefabs/server-clock/ai.md +10 -7
  28. package/prefabs/server-clock/data.json +5 -1
  29. package/prefabs/server-clock/scripts/server-clock.ts +7 -6
  30. package/prefabs/trigger-zone/ai.md +13 -7
  31. package/prefabs/trigger-zone/data.json +2 -1
  32. package/prefabs/ui-theme/ai.md +30 -0
  33. package/prefabs/ui-theme/composite.json +31 -0
  34. package/prefabs/ui-theme/data.json +24 -0
  35. package/prefabs/ui-theme/scripts/ui-theme.ts +16 -0
  36. package/prefabs/ui-theme/thumbnail.png +0 -0
  37. package/scene/about +1 -1
  38. package/scene/{bafkreidzfny4im7pqpzuedreg2q5e6pd3vokz7h2py5b6zjxn3er6h4f5i → bafkreifyskofqvjvp22m3eaxz2mpwibp7j4wtimdj5bxtqsd7onkbnjlsa} +1 -1
  39. package/scene/bafybeiblcndgruoz3ewqrheei4moyeoehankul4yd43co33djksumfd3se +90 -0
  40. package/three/worker.js +3832 -0
  41. package/web-build/index.js +6 -3
  42. package/web-build/sdk-snapshot.json +1 -1
  43. package/web-build/worker.js +29 -26
  44. package/dcl-regenesislabs-web-editor-0.1.1-37509646646.commit-126b34d.tgz +0 -0
  45. package/prefabs/server-clock/scripts/clock-hud.tsx +0 -55
  46. package/scene/bafybeihw3xlbff4sf5sc3kvmlmlwf2visnty5lfymawlunzt5vrkp5fd7u +0 -90
@@ -12,6 +12,7 @@ export interface BoardRow {
12
12
  rank: number
13
13
  player: string
14
14
  score: number
15
+ name?: string
15
16
  }
16
17
 
17
18
  const PLAYER_FIELDS = ['player', 'p', 'address', 'wallet', 'name']
@@ -28,6 +29,11 @@ export function shortName(player: string): string {
28
29
  return player.startsWith('0x') && player.length > 10 ? `${player.slice(0, 6)}…${player.slice(-4)}` : player
29
30
  }
30
31
 
32
+ /** The display name the writer stored or the viewer resolved, else the short wallet. */
33
+ export function rowLabel(row: { player: string; name?: string }): string {
34
+ return row.name?.trim() || shortName(row.player)
35
+ }
36
+
31
37
  /** Whole seconds as m:ss — how an 'asc' board's times want to read. */
32
38
  export function formatScore(score: number, sort: BoardSort): string {
33
39
  if (sort === 'desc') return `${Math.round(score)}`
@@ -48,13 +54,14 @@ export function clampRows(value: number): number {
48
54
  */
49
55
  export function boardRows(value: unknown, sort: BoardSort, limit: number): BoardRow[] {
50
56
  if (!Array.isArray(value)) return []
51
- const rows: { player: string; score: number }[] = []
57
+ const rows: { player: string; score: number; name?: string }[] = []
52
58
  for (const item of value) {
53
59
  if (!isRecord(item)) continue
54
60
  const playerKey = PLAYER_FIELDS.find((key) => typeof item[key] === 'string' && item[key] !== '')
55
61
  const scoreKey = SCORE_FIELDS.find((key) => typeof item[key] === 'number' && Number.isFinite(item[key]))
56
62
  if (playerKey === undefined || scoreKey === undefined) continue
57
- rows.push({ player: String(item[playerKey]), score: Number(item[scoreKey]) })
63
+ const name = playerKey !== 'name' && typeof item.name === 'string' && item.name.trim() !== '' ? item.name : undefined
64
+ rows.push({ player: String(item[playerKey]), score: Number(item[scoreKey]), ...(name === undefined ? {} : { name }) })
58
65
  }
59
66
  rows.sort((a, b) => (sort === 'desc' ? b.score - a.score : a.score - b.score))
60
67
  return rows.slice(0, clampRows(limit)).map((row, index) => ({ rank: index + 1, ...row }))
@@ -66,6 +73,6 @@ export function renderPanel(title: string, rows: BoardRow[], sort: BoardSort, pl
66
73
  lines.push(placeholder)
67
74
  return lines.join('\n')
68
75
  }
69
- for (const row of rows) lines.push(`${row.rank}. ${shortName(row.player)} ${formatScore(row.score, sort)}`)
76
+ for (const row of rows) lines.push(`${row.rank}. ${rowLabel(row)} ${formatScore(row.score, sort)}`)
70
77
  return lines.join('\n')
71
78
  }
@@ -0,0 +1,101 @@
1
+ ---
2
+ prefab: run-progress
3
+ ---
4
+
5
+ # Run Progress — AI guide
6
+
7
+ Timed courses with per-player checkpoints, collectibles, best times and a HUD.
8
+
9
+ ## When to use
10
+
11
+ A race, obstacle course or collection route with start, finish and restart.
12
+ Place Health & Respawn too; read its guide and set its fallback respawnAt and
13
+ dieBelowHeight. Run Progress supplies each player's latest destination.
14
+ The project copy is normally custom/run_progress/; inspect the actual folder.
15
+
16
+ ## API
17
+
18
+ Configure the placed script, with one distinct courseId per course:
19
+
20
+ - `courseId`: state namespace, default "course". One course has ONE owner: this
21
+ placed Run Progress. Never place a second one or call `createRun` with the same
22
+ id ("needs a unique courseId"); custom scripts read `runOf`/`restartRun` only.
23
+ - `title`: completion screen title.
24
+ - `objective`: short instruction below the status row.
25
+ - `startArea`, `finishArea`: names of existing Trigger Areas.
26
+ - `startMarker`: named entity at a safe feet position before the start area.
27
+ A missing marker falls back to its area's floor (logged), and with no area
28
+ either to Health & Respawn's respawnAt.
29
+ - `checkpointAreas`: ordered Trigger Area picker; stored as comma-separated names, or pass an array through setScriptParams.
30
+ - `checkpointMarkers`: one spawn marker per checkpoint, same order; a count mismatch is a problem card.
31
+ - `collectibleAreas`: comma-separated Trigger Area names, each counts once per player.
32
+ - `accentColor`: fallback six-digit hex color. Place UI Theme to style the entire scene together.
33
+ - `collectibleVisuals`: comma-separated names of plain visual entities, matching collectibleAreas in order.
34
+ - `hud`: "Full" (run panel, corner clock, controls), "Big timer" (one large
35
+ centred clock plus Restart and the results screen) or "Hidden" (draws nothing).
36
+ The creator's HUD request is THIS setting: "one big timer" = "Big timer". For a
37
+ custom HUD set "Hidden" and read `runOf()`; never draw a second clock beside it.
38
+
39
+ An empty checkpointAreas or collectibleAreas disables that goal.
40
+ All area names must be distinct. Place every area with fireWhen "every time" so
41
+ it can trigger on later attempts. Each spawn marker must be above solid ground,
42
+ clear of hazards, and outside its own checkpoint trigger. Names must be unique.
43
+
44
+ Keep each visible orb/model on a plain entity and list it in collectibleVisuals.
45
+ Attach custom bobbing or glow scripts there. Do not parent custom-script entities
46
+ under a Trigger Area prefab: Update from prefab would remove those children.
47
+ Collecting hides the named visual and its children for that player; restart shows it.
48
+ When collectibleVisuals is empty, older scenes still hide the trigger and its children.
49
+ Put static geometry, markers and areas in the authored scene so they remain editable.
50
+
51
+ The server validates area entries through the shared game module. It starts the
52
+ timer at startArea, requires ordered checkpoints and all collectibles at finishArea,
53
+ and keeps independent progress per player. Falling retains progress. Restart moves
54
+ only that player to startMarker, resets the run and retains their best time.
55
+ Local entries immediately update the timer, pickups, checkpoints and completion HUD.
56
+ Confirmed state reconciles that presentation; rejected or unconfirmed predictions
57
+ roll back. Only server-confirmed finishes update personal bests. Use this behavior
58
+ instead of adding another pickup request or waiting for server state to show feedback.
59
+
60
+ Read `src/scripts/runtime/runs.ts` for state and restart functions and
61
+ `src/scripts/runtime/gameHud.tsx` for reusable presentation components. Game state
62
+ at `run:<courseId>` contains each player's phase, checkpoint, collected IDs,
63
+ attempt, startedAt, elapsedMs and bestMs. Do not write it yourself.
64
+
65
+ Confirmed finishes also publish a persisted top-25 board at
66
+ `run:<courseId>:leaderboard`, with `{ player, score }` rows in seconds, fastest
67
+ first. Place Leaderboard, choose Source "Run best times" and the same Course Id.
68
+ It works without another script; restart and player departure retain the board.
69
+
70
+ For a moving deck, attach `scripts/moving-platform.ts` from this prefab's actual
71
+ project folder to an authored platform with a MeshCollider. Its `axis` is x/y/z,
72
+ `distance` is the travel amplitude in metres, and `periodSeconds` is one cycle.
73
+ Keep its whole path inside the parcels. Every player uses the same shared clock;
74
+ test riding and jumping from it in the engine before treating it as reachable.
75
+
76
+ ## Do / Don't
77
+
78
+ - DO use the existing Health & Respawn and Trigger Area guides.
79
+ - DO size trigger areas generously enough for the moving avatar.
80
+ - DO walk toward a safe takeoff edge before longer jumps; target landing centres.
81
+ - DO keep jumps within the existing controller's reach and test the actual route.
82
+ - DON'T add a second timer, checkpoint map, collectible counter or restart handler,
83
+ and never rewrite the run in your own script to change its look: set `hud`.
84
+ - DON'T use Game Flow merely for individual restarts; reserve it for shared rounds.
85
+ - DON'T claim completion from a build alone. Test falling, an incomplete finish,
86
+ all pickups/checkpoints, the finish screen and Run again.
87
+
88
+ ## Example
89
+
90
+ Place areas Start, Finish, Checkpoint 1–3 and Orb 1–5; create safe markers
91
+ Start Spawn and Spawn 1–3. Place Health & Respawn with respawnAt Start Spawn and
92
+ dieBelowHeight below the platforms. Place Run Progress with its default names.
93
+ The HUD and full run loop then work without another scene script.
94
+
95
+ ## Creator workflow
96
+
97
+ Add and name Trigger Areas, then place Run Progress. In its inspector choose Start Area, Finish Area and
98
+ Start Marker, and use **+ Add** in the checkpoint, spawn-marker and collectible lists; row order is the
99
+ matching order. These are scene entity pickers, so no CSV or generated script is needed; names have no commas.
100
+ Place **UI Theme**, choose one accent color and Dark or Light, and Play. The HUD, result dialog, progress,
101
+ buttons and Announcer use that theme. Keep one UI Theme per scene; its settings override the fallback accent.
@@ -0,0 +1,9 @@
1
+ {
2
+ "version": 1,
3
+ "components": [
4
+ { "name": "core-schema::Name", "data": { "0": { "json": { "value": "Run Progress" } } } },
5
+ { "name": "asset-packs::Script", "data": { "0": { "json": { "value": [
6
+ { "path": "{assetPath}/scripts/run-progress.tsx", "priority": 0, "layout": "{\"params\":{},\"actions\":[]}" }
7
+ ] } } } }
8
+ ]
9
+ }
@@ -0,0 +1,43 @@
1
+ {
2
+ "id": "8ea22784-81dd-4d60-b0ae-54fef4852fb7",
3
+ "name": "Run Progress",
4
+ "description": "Timed runs with per-player checkpoints, collectibles, finish conditions, restart and a configurable HUD.",
5
+ "category": "custom",
6
+ "group": "Multiplayer Server",
7
+ "tags": [
8
+ "course",
9
+ "race",
10
+ "checkpoint",
11
+ "collectible",
12
+ "timer",
13
+ "hud",
14
+ "restart"
15
+ ],
16
+ "requiresSdk": "auth-server",
17
+ "version": "1.3.1",
18
+ "changelog": [
19
+ {
20
+ "version": "1.3.1",
21
+ "notes": "Hit points show unless Health & Respawn's Health Bar is turned off, so older Health & Respawn copies keep their bar."
22
+ },
23
+ {
24
+ "version": "1.3.0",
25
+ "notes": "A missing spawn marker respawns players on its area's floor, not mid-air, and a checkpoint/marker count mismatch shows as a problem."
26
+ },
27
+ {
28
+ "version": "1.2.0",
29
+ "notes": "HUD setting: Full, one big centred timer, or Hidden for a custom HUD. Health shows only when Health & Respawn turns its bar on."
30
+ },
31
+ {
32
+ "version": "1.1.0",
33
+ "notes": "A missing spawn marker no longer stops the course: players respawn at the matching area, or at Health & Respawn's point."
34
+ },
35
+ {
36
+ "version": "1.0.0",
37
+ "notes": "Timed course runs with per-player checkpoints, collectible goals, restart, best times, a persisted leaderboard and a themed HUD."
38
+ }
39
+ ],
40
+ "origin": {
41
+ "source": "builtin"
42
+ }
43
+ }
@@ -0,0 +1,28 @@
1
+ import { Transform, type Entity } from '@dcl/sdk/ecs'
2
+ import { game } from '~runtime/game'
3
+
4
+ export class MovingPlatform {
5
+ private origin: { x: number; y: number; z: number } | null = null
6
+
7
+ constructor(
8
+ public src: string,
9
+ public entity: Entity,
10
+ public axis: 'x' | 'y' | 'z' = 'x',
11
+ public distance: number = 2,
12
+ public periodSeconds: number = 6
13
+ ) {}
14
+
15
+ start(): void {
16
+ const transform = Transform.getOrNull(this.entity)
17
+ if (transform !== null) this.origin = { ...transform.position }
18
+ }
19
+
20
+ update(): void {
21
+ if (this.origin === null || !['x', 'y', 'z'].includes(this.axis)) return
22
+ const transform = Transform.getMutableOrNull(this.entity)
23
+ if (transform === null) return
24
+ const period = Number.isFinite(this.periodSeconds) ? Math.max(2, this.periodSeconds) : 6
25
+ const distance = Number.isFinite(this.distance) ? Math.max(0, Math.min(16, this.distance)) : 0
26
+ transform.position[this.axis] = this.origin[this.axis] + Math.sin(game.now() / 1000 / period * Math.PI * 2) * distance
27
+ }
28
+ }
@@ -0,0 +1,170 @@
1
+ import ReactEcs, { ReactEcsRenderer } from '@dcl/sdk/react-ecs'
2
+ import { engine, PlayerIdentityData, VisibilityComponent, type Entity } from '@dcl/sdk/ecs'
3
+ import { isServer } from '@dcl/sdk/network'
4
+ import { game, reportProblem } from '~runtime/game'
5
+ import { RunHud } from '~runtime/gameHud'
6
+ import { UI_CANVAS } from '~runtime/uiTheme'
7
+ import type { ColorHex, EntityName, EntityNames, ZoneName, ZoneNames } from '~runtime/authoring'
8
+ import { createRun, restartRun, runOf } from '~runtime/runs'
9
+ import { entityWorld, namedEntity } from '~runtime/sceneNames'
10
+ import type { Vec3 } from '~runtime/pure/worldTransform'
11
+ import { elapsedRun, freshRun, type RunAction, type RunState } from '~runtime/pure/runState'
12
+ import { RunPrediction } from '~runtime/pure/runPrediction'
13
+
14
+ function list(value: string): string[] {
15
+ return value.split(',').map((name) => name.trim()).filter(Boolean)
16
+ }
17
+
18
+ function named(name: string | undefined) {
19
+ const entity = name?.trim() ? namedEntity(name) : null
20
+ return entity === null ? null : entityWorld(entity)
21
+ }
22
+
23
+ // A missing marker must not stop the course: respawn on the floor of its area, and
24
+ // with no area either, let Health & Respawn's respawnAt decide.
25
+ function spawnPosition(marker: string | undefined, area: string): Vec3 | undefined {
26
+ const exact = named(marker)
27
+ if (exact !== null) return exact.position
28
+ const zone = named(area)
29
+ console.log(zone === null
30
+ ? `[runProgress] no spawn marker "${marker ?? ''}" or area "${area}" — Health & Respawn's respawnAt is used`
31
+ : `[runProgress] no spawn marker "${marker ?? ''}" — respawning on area "${area}"`)
32
+ if (zone === null) return undefined
33
+ return { ...zone.position, y: zone.position.y - Math.abs(zone.scale.y) / 2 }
34
+ }
35
+
36
+ function record(value: unknown): Record<string, unknown> {
37
+ return typeof value === 'object' && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {}
38
+ }
39
+
40
+ export class RunProgress {
41
+ private items: string[] = []
42
+ private points: string[] = []
43
+ private visuals: string[] = []
44
+ private prediction: RunPrediction | null = null
45
+ private notice = ''
46
+ private noticeUntil = 0
47
+ private pending = false
48
+ private lastVisual = ''
49
+
50
+ constructor(
51
+ public src: string,
52
+ public entity: Entity,
53
+ public courseId: string = 'course',
54
+ public title: string = 'Sky Lantern Run',
55
+ public objective: string = 'Collect every orb and reach the finish',
56
+ public startArea: ZoneName = 'Start',
57
+ public finishArea: ZoneName = 'Finish',
58
+ public startMarker: EntityName = 'Start Spawn',
59
+ public checkpointAreas: ZoneNames = 'Checkpoint 1, Checkpoint 2, Checkpoint 3',
60
+ public checkpointMarkers: EntityNames = 'Spawn 1, Spawn 2, Spawn 3',
61
+ public collectibleAreas: ZoneNames = 'Orb 1, Orb 2, Orb 3, Orb 4, Orb 5',
62
+ public accentColor: ColorHex = '#FFC459',
63
+ public collectibleVisuals: EntityNames = '',
64
+ /** "Big timer" shows one large clock at the top; "Hidden" draws nothing, for a custom HUD. */
65
+ public hud: 'Full' | 'Big timer' | 'Hidden' = 'Full'
66
+ ) {}
67
+
68
+ start(): void {
69
+ this.items = list(this.collectibleAreas)
70
+ this.points = list(this.checkpointAreas)
71
+ this.visuals = list(this.collectibleVisuals)
72
+ if (this.visuals.length !== 0 && this.visuals.length !== this.items.length) throw new Error('Each collectible area needs a matching visual name.')
73
+ const markers = list(this.checkpointMarkers)
74
+ if (isServer() && markers.length !== this.points.length) {
75
+ reportProblem('Run Progress', `${this.points.length} checkpoint areas but ${markers.length} spawn markers — give each checkpoint one marker, in the same order.`)
76
+ }
77
+ if (isServer()) {
78
+ createRun({
79
+ id: this.courseId, startArea: this.startArea, finishArea: this.finishArea,
80
+ startPosition: spawnPosition(this.startMarker, this.startArea),
81
+ checkpoints: this.points.map((area, index) => ({ area, position: spawnPosition(markers[index], area) })),
82
+ collectibles: this.items
83
+ })
84
+ return
85
+ }
86
+ this.prediction = new RunPrediction({ checkpoints: this.points, collectibles: this.items })
87
+ const watch = (area: string, action: RunAction) => game.onLocalAreaEnter(area, (validation) => {
88
+ this.current()
89
+ const predicted = this.prediction!.predict(action, game.now())
90
+ if (predicted.message) this.say(predicted.message)
91
+ this.update()
92
+ void validation.then((accepted) => {
93
+ this.current()
94
+ if (!accepted && predicted.id !== null && this.prediction!.reject(predicted.id)) {
95
+ this.say('Progress could not be confirmed. Try again.')
96
+ this.update()
97
+ }
98
+ })
99
+ })
100
+ watch(this.startArea, { type: 'start' })
101
+ watch(this.finishArea, { type: 'finish' })
102
+ this.points.forEach((area, index) => watch(area, { type: 'checkpoint', index }))
103
+ this.items.forEach((id) => watch(id, { type: 'collect', id }))
104
+ if (this.hud !== 'Hidden') ReactEcsRenderer.addUiRenderer(this.entity, () => this.render(), UI_CANVAS)
105
+ }
106
+
107
+ update(): void {
108
+ if (isServer()) return
109
+ const state = this.current()
110
+ if (state === null) return
111
+ const signature = `${state.attempt}:${state.collected.join(',')}`
112
+ if (signature === this.lastVisual) return
113
+ let complete = true
114
+ for (const [index, name] of this.items.entries()) {
115
+ const entity = namedEntity(this.visuals[index] ?? name)
116
+ if (entity === null) { complete = false; continue }
117
+ VisibilityComponent.createOrReplace(entity, { visible: !state.collected.includes(name), propagateToChildren: true })
118
+ }
119
+ if (complete) this.lastVisual = signature
120
+ }
121
+
122
+ private player(): string {
123
+ return (PlayerIdentityData.getOrNull(engine.PlayerEntity)?.address ?? '').toLowerCase()
124
+ }
125
+
126
+ private current(): RunState | null {
127
+ const state = runOf(this.courseId, this.player())
128
+ this.prediction?.reconcile(state, game.now())
129
+ return this.prediction?.view(game.now()) ?? state
130
+ }
131
+
132
+ private say(text: string): void {
133
+ this.notice = text
134
+ this.noticeUntil = game.now() + 3500
135
+ }
136
+
137
+ private restart(): void {
138
+ const state = runOf(this.courseId, this.player())
139
+ if (state === null || this.pending) return
140
+ this.pending = true
141
+ this.current()
142
+ const predicted = this.prediction?.predict({ type: 'restart' }, game.now())
143
+ this.update()
144
+ void restartRun(this.courseId, state.attempt).then((result) => {
145
+ if (record(result).ok !== true) throw new Error('Restart was refused')
146
+ }).catch((error: unknown) => {
147
+ if (predicted?.id != null) this.prediction?.reject(predicted.id)
148
+ this.say('Restart failed. Try again.')
149
+ console.log(`[runProgress] restart failed: ${String(error)}`)
150
+ }).finally(() => { this.pending = false })
151
+ }
152
+
153
+ private render(): ReactEcs.JSX.Element {
154
+ const player = this.player()
155
+ const state = this.current() ?? freshRun()
156
+ const health = game.state.healthBar === false ? null : record(game.state.health)[player]
157
+ const life = record(record(game.state.healthLife)[player])
158
+ return <RunHud
159
+ title={this.title} objective={this.objective} phase={state.phase}
160
+ elapsedMs={elapsedRun(state, game.now())} bestMs={state.bestMs}
161
+ checkpoint={state.checkpoint} checkpoints={this.points.length}
162
+ collected={state.collected.length} collectibles={this.items.length}
163
+ health={typeof health === 'number' ? health : null}
164
+ maxHealth={typeof game.state.healthMax === 'number' ? game.state.healthMax : 100}
165
+ respawnSeconds={life.dead === true && typeof life.respawnAt === 'number' ? Math.max(0, life.respawnAt - game.now()) / 1000 : 0}
166
+ notice={game.now() < this.noticeUntil ? this.notice : ''}
167
+ accentColor={this.accentColor} restart={() => this.restart()} hud={this.hud === 'Big timer' ? 'Big timer' : 'Full'}
168
+ />
169
+ }
170
+ }
@@ -13,9 +13,10 @@ second copy is custom/server_clock_2/.
13
13
 
14
14
  ## When to use
15
15
 
16
- Any shared deadline or shared elapsed time: a round timer, a countdown, a daily
17
- event, a scoreboard's "started at". Also just to show the time. Needs an
18
- scene with a Multiplayer Server (data.json requiresSdk: auth-server).
16
+ Show the current server time, or reuse its synchronization module for a custom
17
+ shared deadline. For a ready-made round countdown use Game Flow with display
18
+ "2D UI". For a player's elapsed course time use Run Progress. This prefab shows
19
+ clock time; it does not start or end rounds. Needs a Multiplayer Server scene.
19
20
 
20
21
  ## API
21
22
 
@@ -39,6 +40,9 @@ Params of the prefab's script — set them in the placePrefab request:
39
40
  - position: where the 2D overlay sits — 'top' (default), 'top left', 'top right'
40
41
  or 'bottom'. Ignored for '3D text'.
41
42
 
43
+ The overlay uses UI Theme. Multiple placed clocks keep their own label, time and
44
+ position; choose different corners so they remain visible.
45
+
42
46
  ## Shared deadlines
43
47
 
44
48
  Express a deadline as an absolute server timestamp, share THAT, and derive the
@@ -61,7 +65,6 @@ countdown locally every frame:
61
65
 
62
66
  ## Example
63
67
 
64
- "Show a 60-second round timer everyone sees the same": place Server Clock (or
65
- call initTimeSync() in a script of your own), store endsAt as a server timestamp
66
- when the round starts, and render Math.ceil((endsAt - getServerTime()) / 1000)
67
- in update() while isTimeSyncReady().
68
+ "Show a 60-second round timer everyone sees the same": place Game Flow with
69
+ roundSeconds 60, countdownSeconds 3 and display "2D UI". Place UI Theme to choose
70
+ its palette. Use Server Clock separately if the scene also needs the time of day.
@@ -11,11 +11,15 @@
11
11
  "time"
12
12
  ],
13
13
  "requiresSdk": "auth-server",
14
- "version": "1.0.0",
14
+ "version": "1.1.0",
15
15
  "changelog": [
16
16
  {
17
17
  "version": "1.0.0",
18
18
  "notes": "Initial release: the Multiplayer Server's NTP-synced time, identical for every player, shown as 3D text at the item or as a 2D overlay on the player's client."
19
+ },
20
+ {
21
+ "version": "1.1.0",
22
+ "notes": "Shared UI theme and independent clocks when multiple items use the overlay."
19
23
  }
20
24
  ],
21
25
  "origin": {
@@ -1,7 +1,7 @@
1
- import { Entity, TextShape } from '@dcl/sdk/ecs'
1
+ import { Entity, TextShape, VisibilityComponent } from '@dcl/sdk/ecs'
2
2
  import { isServer } from '@dcl/sdk/network'
3
3
  import { initTimeSync, getServerTime, isTimeSyncReady } from '~runtime/timeSync'
4
- import { mountClockHud, setClockHudText } from './clock-hud'
4
+ import { mountStatusHud } from '~runtime/statusHud'
5
5
 
6
6
  // Displays the Multiplayer Server's clock, NTP-synced, identical for every
7
7
  // player — as floating 3D text, or as a 2D overlay on the player's client. Both
@@ -14,6 +14,7 @@ function pad(n: number): string {
14
14
 
15
15
  export class ServerClock {
16
16
  private accum = 0
17
+ private time = '--:--:--'
17
18
 
18
19
  constructor(
19
20
  public src: string,
@@ -21,7 +22,7 @@ export class ServerClock {
21
22
  /** Label above the time */ public label: string = 'SERVER TIME',
22
23
  /** Show in UTC instead of each viewer's timezone */ public utc: boolean = true,
23
24
  /** How the clock is shown */ public display: '3D text' | '2D UI' = '3D text',
24
- /** Where the 2D UI sits in the player's view */ public position: 'top' | 'top left' | 'top right' | 'bottom' = 'top'
25
+ /** For "2D UI": where the clock sits on screen. */ public position: 'top' | 'top left' | 'top right' | 'bottom' = 'top'
25
26
  ) {}
26
27
 
27
28
  private get onScreen(): boolean {
@@ -32,9 +33,9 @@ export class ServerClock {
32
33
  initTimeSync()
33
34
  if (isServer()) return
34
35
  if (this.onScreen) {
35
- // the 3D text becomes the overlay — remove the placed shape
36
+ VisibilityComponent.createOrReplace(this.entity, { visible: false })
36
37
  TextShape.deleteFrom(this.entity)
37
- mountClockHud(this.entity, this.position)
38
+ mountStatusHud(this.entity, this.position, () => ({ label: this.label, value: this.time }))
38
39
  }
39
40
  this.render('--:--:--')
40
41
  }
@@ -57,7 +58,7 @@ export class ServerClock {
57
58
 
58
59
  private render(time: string): void {
59
60
  if (this.onScreen) {
60
- setClockHudText(this.label, time)
61
+ this.time = time
61
62
  return
62
63
  }
63
64
  TextShape.createOrReplace(this.entity, {
@@ -4,9 +4,8 @@ prefab: trigger-zone
4
4
 
5
5
  # Trigger Area — AI guide
6
6
 
7
- An invisible named volume that knows who is standing in it. Other scripts react
8
- to it BY NAME through the bus module in the project's shared runtime — nothing is
9
- wired, and a reacting script pasted into another scene keeps working.
7
+ An invisible named volume. Scripts react to its name through the shared runtime
8
+ bus; the same reaction works in another scene.
10
9
 
11
10
  ## When to use
12
11
 
@@ -97,10 +96,12 @@ reactions. Decide by what the reaction acts on:
97
96
  working for everybody. The prefab owns those callbacks; you consume the bus.
98
97
  - DON'T hand-roll proximity (distance loops against the player). The prefab
99
98
  exists so you never do.
100
- - The volume IS the entity's Transform scale in metres, so the scale gizmo is the
101
- resize tool. Neither you nor the editor can measure a model's bounding box —
102
- place the area NEAR the object at a sensible size and say in one line to drag
103
- the scale handles to fit. Never claim you fitted it.
99
+ - The volume IS the entity's Transform scale in metres. For a model, use
100
+ `asset_request inspect_model` and account for its scale, rotation and pivot.
101
+ Fit the area to the interaction space and verify entry/exit in play. Bounds
102
+ exclude animation; leave clearance for moving models and avatar height.
103
+ - Keep the whole volume and its movement range inside the active parcels.
104
+ Resize with sceneSettings first and wait for its successful preview result.
104
105
  - Detection is client-side only: the headless server has no avatar colliders, so
105
106
  an area never fires there. Fine for doors, lights, sound, ambience. When an area
106
107
  gates something valuable (a reward, a score, a paid area) put the decision in a
@@ -110,6 +111,11 @@ reactions. Decide by what the reaction acts on:
110
111
 
111
112
  ## Example
112
113
 
114
+ For a course, read the Run Progress and Health & Respawn guides too. Use Run
115
+ Progress's start/checkpoint/finish references and its local feedback. Do not add
116
+ a second score or checkpoint owner to the areas. Use the inspector's message
117
+ action for a local notice; the server still validates progression and rewards.
118
+
113
119
  "Make the door open when someone walks up": placePrefab trigger-zone named
114
120
  "Front Door Area" (4×3×4 at the doorway, who: 'any player'), then a script on
115
121
  the Door entity whose update() runs the isInZone snippet above.
@@ -14,8 +14,9 @@
14
14
  "code": "onZone('Front Door', 'enter', (event) => { /* what happens */ })",
15
15
  "next": "Use the Add a reaction button below and the new Script arrives already listening to this area."
16
16
  },
17
- "version": "1.1.0",
17
+ "version": "1.1.1",
18
18
  "changelog": [
19
+ { "version": "1.1.1", "notes": "Guide covers model bounds, active parcels and course prefab integration." },
19
20
  {
20
21
  "version": "1.0.0",
21
22
  "notes": "Initial release: an invisible named area that publishes who is standing in it, so a Script anywhere in the scene reacts to it by name with nothing wired."
@@ -0,0 +1,30 @@
1
+ ---
2
+ prefab: ui-theme
3
+ ---
4
+
5
+ # UI Theme — AI guide
6
+
7
+ ## When to use
8
+
9
+ Place one UI Theme from the library. Pick Accent Color and Dark or Light in its inspector, then Play. Run Progress, Announcer, and the 2D UI modes of Game Flow, Server Clock and Leaderboard share the palette. A single color change restyles their buttons, timers, scores, progress indicators and results together; text contrast is derived automatically. Keep one theme per scene.
10
+
11
+ ## API
12
+
13
+ - `accentColor`: six-digit hex color, edited with a color picker.
14
+ - `appearance`: Dark or Light.
15
+
16
+ For custom UI, import `sceneUiTheme` and `UI_CANVAS` from `~runtime/uiTheme`, and `UiPanel`, `UiText`, `UiButton`, `ProgressPips`, `UiNotice` from `~runtime/uiComponents`. Register with `ReactEcsRenderer.addUiRenderer(entity, render, UI_CANVAS)`. Keep viewport roots pointer-transparent and handlers on actual buttons. Labels and stacks need explicit dimensions. Inspect a screenshot in the real preview, including completion and a smaller viewport.
17
+
18
+ `ColorHex`, `EntityName`, `EntityNames`, `ZoneName` and `ZoneNames` from `~runtime/authoring` are string aliases that give constructor parameters color or entity pickers. Plural references are ordered comma-separated names; creators pick and reorder them in the inspector. Names must be unique and contain no commas. The assistant can provide arrays to the plural parameters.
19
+
20
+ ## Do / Don't
21
+
22
+ - Use one theme per scene, readable text and small non-blocking HUDs.
23
+ - Do not write a separate palette for each prefab or use default pink SDK buttons.
24
+ - Verify running and finished states in a real preview screenshot.
25
+
26
+ ## Example
27
+
28
+ Place UI Theme with accentColor "#59E0AC" and appearance "Dark", then place Run Progress and Announcer.
29
+ Their buttons, progress indicators, result panel and messages all share mint accents on dark surfaces.
30
+ Choose a new accent in the inspector to change them together. No scene script is needed.
@@ -0,0 +1,31 @@
1
+ {
2
+ "version": 1,
3
+ "components": [
4
+ {
5
+ "name": "core-schema::Name",
6
+ "data": {
7
+ "0": {
8
+ "json": {
9
+ "value": "UI Theme"
10
+ }
11
+ }
12
+ }
13
+ },
14
+ {
15
+ "name": "asset-packs::Script",
16
+ "data": {
17
+ "0": {
18
+ "json": {
19
+ "value": [
20
+ {
21
+ "path": "{assetPath}/scripts/ui-theme.ts",
22
+ "priority": 0,
23
+ "layout": "{\"params\":{},\"actions\":[]}"
24
+ }
25
+ ]
26
+ }
27
+ }
28
+ }
29
+ }
30
+ ]
31
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "id": "41939776-72b2-45f1-b40a-097fa66c9790",
3
+ "name": "UI Theme",
4
+ "description": "One accent color and appearance for game HUDs, timers, leaderboards, buttons, results and announcements.",
5
+ "category": "custom",
6
+ "group": "Scene UI",
7
+ "tags": [
8
+ "ui",
9
+ "theme",
10
+ "color",
11
+ "hud",
12
+ "design"
13
+ ],
14
+ "version": "1.0.0",
15
+ "changelog": [
16
+ {
17
+ "version": "1.0.0",
18
+ "notes": "One accent color and dark or light appearance shared by the scene's game HUDs, buttons, timers, leaderboards and results."
19
+ }
20
+ ],
21
+ "origin": {
22
+ "source": "builtin"
23
+ }
24
+ }