@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.
- package/editor.js +1231 -464
- package/package.json +2 -2
- package/prefabs/announcer/ai.md +9 -2
- package/prefabs/announcer/data.json +6 -7
- package/prefabs/announcer/scripts/announcer.tsx +16 -37
- package/prefabs/catalog.json +1 -1
- package/prefabs/game-flow/ai.md +6 -0
- package/prefabs/game-flow/data.json +7 -3
- package/prefabs/game-flow/scripts/game-flow.ts +22 -5
- package/prefabs/health-respawn/ai.md +25 -2
- package/prefabs/health-respawn/data.json +8 -1
- package/prefabs/health-respawn/scripts/health-respawn.ts +52 -20
- package/prefabs/health-respawn/scripts/health.ts +21 -0
- package/prefabs/health-respawn/scripts/pure/life.ts +59 -0
- package/prefabs/leaderboard/ai.md +27 -9
- package/prefabs/leaderboard/composite.json +2 -2
- package/prefabs/leaderboard/data.json +14 -7
- package/prefabs/leaderboard/scripts/leaderboard-hud.tsx +28 -0
- package/prefabs/leaderboard/scripts/leaderboard.ts +41 -5
- package/prefabs/leaderboard/scripts/pure/board.ts +10 -3
- package/prefabs/run-progress/ai.md +101 -0
- package/prefabs/run-progress/composite.json +9 -0
- package/prefabs/run-progress/data.json +43 -0
- package/prefabs/run-progress/scripts/moving-platform.ts +28 -0
- package/prefabs/run-progress/scripts/run-progress.tsx +170 -0
- package/prefabs/run-progress/thumbnail.png +0 -0
- package/prefabs/server-clock/ai.md +10 -7
- package/prefabs/server-clock/data.json +5 -1
- package/prefabs/server-clock/scripts/server-clock.ts +7 -6
- package/prefabs/trigger-zone/ai.md +13 -7
- package/prefabs/trigger-zone/data.json +2 -1
- package/prefabs/ui-theme/ai.md +30 -0
- package/prefabs/ui-theme/composite.json +31 -0
- package/prefabs/ui-theme/data.json +24 -0
- package/prefabs/ui-theme/scripts/ui-theme.ts +16 -0
- package/prefabs/ui-theme/thumbnail.png +0 -0
- package/scene/about +1 -1
- package/scene/{bafkreidzfny4im7pqpzuedreg2q5e6pd3vokz7h2py5b6zjxn3er6h4f5i → bafkreifyskofqvjvp22m3eaxz2mpwibp7j4wtimdj5bxtqsd7onkbnjlsa} +1 -1
- package/scene/bafybeiblcndgruoz3ewqrheei4moyeoehankul4yd43co33djksumfd3se +90 -0
- package/three/worker.js +3832 -0
- package/web-build/index.js +6 -3
- package/web-build/sdk-snapshot.json +1 -1
- package/web-build/worker.js +29 -26
- package/dcl-regenesislabs-web-editor-0.1.1-37509646646.commit-126b34d.tgz +0 -0
- package/prefabs/server-clock/scripts/clock-hud.tsx +0 -55
- 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
|
-
|
|
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}. ${
|
|
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
|
+
}
|
|
Binary file
|
|
@@ -13,9 +13,10 @@ second copy is custom/server_clock_2/.
|
|
|
13
13
|
|
|
14
14
|
## When to use
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
65
|
-
|
|
66
|
-
|
|
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.
|
|
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 {
|
|
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
|
-
/**
|
|
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
|
-
|
|
36
|
+
VisibilityComponent.createOrReplace(this.entity, { visible: false })
|
|
36
37
|
TextShape.deleteFrom(this.entity)
|
|
37
|
-
|
|
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
|
-
|
|
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
|
|
8
|
-
|
|
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
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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.
|
|
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
|
+
}
|