@dcl-regenesislabs/web-editor 0.0.0-stage → 0.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/editor.css +1 -0
- package/editor.js +33891 -0
- package/package.json +8 -4
- package/prefabs/admin-tools/composite.json +77 -0
- package/prefabs/admin-tools/data.json +28 -0
- package/prefabs/admin-tools/icons/admin-panel-control-button.png +0 -0
- package/prefabs/admin-tools/icons/admin-panel-moderation-control-button.png +0 -0
- package/prefabs/admin-tools/icons/admin-panel-rewards-control-button.png +0 -0
- package/prefabs/admin-tools/icons/admin-panel-smart-item-control-button.png +0 -0
- package/prefabs/admin-tools/icons/admin-panel-text-announcement-control-button.png +0 -0
- package/prefabs/admin-tools/icons/admin-panel-video-control-button.png +0 -0
- package/prefabs/admin-tools/icons/admin-tool-background.png +0 -0
- package/prefabs/admin-tools/icons/announcements/text-announcement-chat-message.png +0 -0
- package/prefabs/admin-tools/icons/announcements/text-announcement-check.png +0 -0
- package/prefabs/admin-tools/icons/announcements/text-announcement-close-button.png +0 -0
- package/prefabs/admin-tools/icons/moderation/admin-panel-verified-user.png +0 -0
- package/prefabs/admin-tools/icons/moderation/ban.png +0 -0
- package/prefabs/admin-tools/icons/moderation/chevron-back.png +0 -0
- package/prefabs/admin-tools/icons/moderation/chevron-forward.png +0 -0
- package/prefabs/admin-tools/icons/moderation/close.png +0 -0
- package/prefabs/admin-tools/icons/moderation/error.png +0 -0
- package/prefabs/admin-tools/icons/moderation/person-outline.png +0 -0
- package/prefabs/admin-tools/icons/moderation-control-icon.png +0 -0
- package/prefabs/admin-tools/icons/rewards/close.png +0 -0
- package/prefabs/admin-tools/icons/rewards/send.png +0 -0
- package/prefabs/admin-tools/icons/rewards-control.png +0 -0
- package/prefabs/admin-tools/icons/smart-item-control.png +0 -0
- package/prefabs/admin-tools/icons/text-announcement-control.png +0 -0
- package/prefabs/admin-tools/icons/video/chevron-down.png +0 -0
- package/prefabs/admin-tools/icons/video/chevron-up.png +0 -0
- package/prefabs/admin-tools/icons/video/copy-to-clipboard.png +0 -0
- package/prefabs/admin-tools/icons/video/error.png +0 -0
- package/prefabs/admin-tools/icons/video/eye-off.png +0 -0
- package/prefabs/admin-tools/icons/video/eye.png +0 -0
- package/prefabs/admin-tools/icons/video/help.png +0 -0
- package/prefabs/admin-tools/icons/video/info.png +0 -0
- package/prefabs/admin-tools/icons/video/person-outline.png +0 -0
- package/prefabs/admin-tools/icons/video/star.png +0 -0
- package/prefabs/admin-tools/icons/video/video-control-dcl-cast.png +0 -0
- package/prefabs/admin-tools/icons/video/video-control-live.png +0 -0
- package/prefabs/admin-tools/icons/video/video-control-loop.png +0 -0
- package/prefabs/admin-tools/icons/video/video-control-mute.png +0 -0
- package/prefabs/admin-tools/icons/video/video-control-pause-button.png +0 -0
- package/prefabs/admin-tools/icons/video/video-control-play-button.png +0 -0
- package/prefabs/admin-tools/icons/video/video-control-video-icon.png +0 -0
- package/prefabs/admin-tools/icons/video/video-control-volume-minus-button.png +0 -0
- package/prefabs/admin-tools/icons/video/video-control-volume-plus-button.png +0 -0
- package/prefabs/admin-tools/icons/video-control.png +0 -0
- package/prefabs/admin-tools/scripts/actions.ts +206 -0
- package/prefabs/admin-tools/scripts/admin.tsx +253 -0
- package/prefabs/admin-tools/scripts/api.ts +125 -0
- package/prefabs/admin-tools/scripts/components.ts +190 -0
- package/prefabs/admin-tools/scripts/icons.ts +53 -0
- package/prefabs/admin-tools/scripts/message-bus.ts +297 -0
- package/prefabs/admin-tools/scripts/state.ts +69 -0
- package/prefabs/admin-tools/scripts/tabs/Moderation.tsx +155 -0
- package/prefabs/admin-tools/scripts/tabs/Rewards.tsx +136 -0
- package/prefabs/admin-tools/scripts/tabs/SmartItemActions.tsx +167 -0
- package/prefabs/admin-tools/scripts/tabs/TextAnnouncements.tsx +195 -0
- package/prefabs/admin-tools/scripts/tabs/VideoControl.tsx +156 -0
- package/prefabs/admin-tools/scripts/tabs/announcements/Overlay.tsx +94 -0
- package/prefabs/admin-tools/scripts/tabs/announcements/icons.ts +15 -0
- package/prefabs/admin-tools/scripts/tabs/announcements/state.ts +71 -0
- package/prefabs/admin-tools/scripts/tabs/announcements/ui.tsx +37 -0
- package/prefabs/admin-tools/scripts/tabs/moderation/AddUserInput.tsx +184 -0
- package/prefabs/admin-tools/scripts/tabs/moderation/RemoveAdminConfirmation.tsx +93 -0
- package/prefabs/admin-tools/scripts/tabs/moderation/UsersList.tsx +228 -0
- package/prefabs/admin-tools/scripts/tabs/moderation/api.ts +35 -0
- package/prefabs/admin-tools/scripts/tabs/moderation/icons.ts +23 -0
- package/prefabs/admin-tools/scripts/tabs/moderation/kick.ts +95 -0
- package/prefabs/admin-tools/scripts/tabs/moderation/state.ts +69 -0
- package/prefabs/admin-tools/scripts/tabs/moderation/ui.tsx +172 -0
- package/prefabs/admin-tools/scripts/tabs/moderation/utils.ts +88 -0
- package/prefabs/admin-tools/scripts/tabs/rewards/Button.tsx +52 -0
- package/prefabs/admin-tools/scripts/tabs/rewards/Captcha.tsx +88 -0
- package/prefabs/admin-tools/scripts/tabs/rewards/api.ts +138 -0
- package/prefabs/admin-tools/scripts/tabs/rewards/claim.ts +99 -0
- package/prefabs/admin-tools/scripts/tabs/rewards/controller.ts +103 -0
- package/prefabs/admin-tools/scripts/tabs/rewards/icons.ts +13 -0
- package/prefabs/admin-tools/scripts/tabs/rewards/state.ts +32 -0
- package/prefabs/admin-tools/scripts/tabs/smart-items/selection.ts +90 -0
- package/prefabs/admin-tools/scripts/tabs/video/DclCast.tsx +226 -0
- package/prefabs/admin-tools/scripts/tabs/video/LiveStream.tsx +284 -0
- package/prefabs/admin-tools/scripts/tabs/video/SpeakerShowcase.tsx +112 -0
- package/prefabs/admin-tools/scripts/tabs/video/VideoUrl.tsx +107 -0
- package/prefabs/admin-tools/scripts/tabs/video/Volume.tsx +69 -0
- package/prefabs/admin-tools/scripts/tabs/video/api.ts +172 -0
- package/prefabs/admin-tools/scripts/tabs/video/controls.ts +108 -0
- package/prefabs/admin-tools/scripts/tabs/video/icons.ts +59 -0
- package/prefabs/admin-tools/scripts/tabs/video/speakers.ts +43 -0
- package/prefabs/admin-tools/scripts/tabs/video/state.ts +82 -0
- package/prefabs/admin-tools/scripts/tabs/video/ui.tsx +243 -0
- package/prefabs/admin-tools/scripts/types.ts +46 -0
- package/prefabs/admin-tools/scripts/ui.tsx +122 -0
- package/prefabs/admin-tools/thumbnail.png +0 -0
- package/prefabs/announcer/ai.md +76 -0
- package/prefabs/announcer/composite.json +31 -0
- package/prefabs/announcer/data.json +35 -0
- package/prefabs/announcer/scripts/announcer.tsx +79 -0
- package/prefabs/announcer/scripts/pure/toast.ts +30 -0
- package/prefabs/announcer/thumbnail.png +0 -0
- package/prefabs/black-chair/HighBackChair.glb +0 -0
- package/prefabs/black-chair/composite.json +74 -0
- package/prefabs/black-chair/data.json +30 -0
- package/prefabs/black-chair/scripts/seat.ts +191 -0
- package/prefabs/black-chair/thumbnail.png +0 -0
- package/prefabs/catalog.json +1 -0
- package/prefabs/classic-bench/Bench_01.glb +0 -0
- package/prefabs/classic-bench/composite.json +100 -0
- package/prefabs/classic-bench/data.json +34 -0
- package/prefabs/classic-bench/file1.png +0 -0
- package/prefabs/classic-bench/scripts/seat.ts +191 -0
- package/prefabs/classic-bench/thumbnail.png +0 -0
- package/prefabs/classic-bench-armrests/Bench 1.glb +0 -0
- package/prefabs/classic-bench-armrests/composite.json +100 -0
- package/prefabs/classic-bench-armrests/data.json +30 -0
- package/prefabs/classic-bench-armrests/scripts/seat.ts +191 -0
- package/prefabs/classic-bench-armrests/thumbnail.png +0 -0
- package/prefabs/classroom-chair/Chair_02.glb +0 -0
- package/prefabs/classroom-chair/composite.json +74 -0
- package/prefabs/classroom-chair/data.json +34 -0
- package/prefabs/classroom-chair/file1.png +0 -0
- package/prefabs/classroom-chair/scripts/seat.ts +191 -0
- package/prefabs/classroom-chair/thumbnail.png +0 -0
- package/prefabs/court-chair/Chair_Wood_01.glb +0 -0
- package/prefabs/court-chair/FanstasyPack_TX.png +0 -0
- package/prefabs/court-chair/composite.json +74 -0
- package/prefabs/court-chair/data.json +34 -0
- package/prefabs/court-chair/scripts/seat.ts +191 -0
- package/prefabs/court-chair/thumbnail.png +0 -0
- package/prefabs/curved-couch/CurvedCouch.glb +0 -0
- package/prefabs/curved-couch/composite.json +100 -0
- package/prefabs/curved-couch/data.json +30 -0
- package/prefabs/curved-couch/scripts/seat.ts +191 -0
- package/prefabs/curved-couch/thumbnail.png +0 -0
- package/prefabs/game-flow/ai.md +94 -0
- package/prefabs/game-flow/composite.json +45 -0
- package/prefabs/game-flow/data.json +27 -0
- package/prefabs/game-flow/scripts/game-flow.ts +168 -0
- package/prefabs/game-flow/scripts/pure/flow.ts +185 -0
- package/prefabs/game-flow/thumbnail.png +0 -0
- package/prefabs/health-respawn/ai.md +76 -0
- package/prefabs/health-respawn/composite.json +31 -0
- package/prefabs/health-respawn/data.json +38 -0
- package/prefabs/health-respawn/scripts/health-respawn.ts +98 -0
- package/prefabs/health-respawn/scripts/health.ts +22 -0
- package/prefabs/health-respawn/scripts/pure/health.ts +60 -0
- package/prefabs/health-respawn/thumbnail.png +0 -0
- package/prefabs/high-stool/HighStool.glb +0 -0
- package/prefabs/high-stool/composite.json +74 -0
- package/prefabs/high-stool/data.json +30 -0
- package/prefabs/high-stool/scripts/seat.ts +191 -0
- package/prefabs/high-stool/thumbnail.png +0 -0
- package/prefabs/large-couch/ThreeSeater_Couch.glb +0 -0
- package/prefabs/large-couch/composite.json +100 -0
- package/prefabs/large-couch/data.json +30 -0
- package/prefabs/large-couch/scripts/seat.ts +191 -0
- package/prefabs/large-couch/thumbnail.png +0 -0
- package/prefabs/leaderboard/ai.md +75 -0
- package/prefabs/leaderboard/board-panel.glb +0 -0
- package/prefabs/leaderboard/composite.json +82 -0
- package/prefabs/leaderboard/data.json +35 -0
- package/prefabs/leaderboard/scripts/leaderboard.ts +66 -0
- package/prefabs/leaderboard/scripts/pure/board.ts +71 -0
- package/prefabs/leaderboard/thumbnail.png +0 -0
- package/prefabs/loveseat/Couch_01.glb +0 -0
- package/prefabs/loveseat/composite.json +100 -0
- package/prefabs/loveseat/data.json +34 -0
- package/prefabs/loveseat/file1.png +0 -0
- package/prefabs/loveseat/scripts/seat.ts +191 -0
- package/prefabs/loveseat/thumbnail.png +0 -0
- package/prefabs/modern-sofa/ModernSofa.glb +0 -0
- package/prefabs/modern-sofa/composite.json +100 -0
- package/prefabs/modern-sofa/data.json +30 -0
- package/prefabs/modern-sofa/scripts/seat.ts +191 -0
- package/prefabs/modern-sofa/thumbnail.png +0 -0
- package/prefabs/outdoor-chair/Chair_07.glb +0 -0
- package/prefabs/outdoor-chair/composite.json +74 -0
- package/prefabs/outdoor-chair/data.json +34 -0
- package/prefabs/outdoor-chair/file1.png +0 -0
- package/prefabs/outdoor-chair/scripts/seat.ts +191 -0
- package/prefabs/outdoor-chair/thumbnail.png +0 -0
- package/prefabs/rustic-bench/Bench_01.glb +0 -0
- package/prefabs/rustic-bench/PiratesPack_TX.png.png +0 -0
- package/prefabs/rustic-bench/composite.json +100 -0
- package/prefabs/rustic-bench/data.json +34 -0
- package/prefabs/rustic-bench/scripts/seat.ts +191 -0
- package/prefabs/rustic-bench/thumbnail.png +0 -0
- package/prefabs/rustic-round-stool/Chairwood_01.glb +0 -0
- package/prefabs/rustic-round-stool/PiratesPack_TX.png.png +0 -0
- package/prefabs/rustic-round-stool/composite.json +74 -0
- package/prefabs/rustic-round-stool/data.json +34 -0
- package/prefabs/rustic-round-stool/scripts/seat.ts +191 -0
- package/prefabs/rustic-round-stool/thumbnail.png +0 -0
- package/prefabs/rustic-square-stool/Chairwood_02.glb +0 -0
- package/prefabs/rustic-square-stool/PiratesPack_TX.png.png +0 -0
- package/prefabs/rustic-square-stool/composite.json +74 -0
- package/prefabs/rustic-square-stool/data.json +34 -0
- package/prefabs/rustic-square-stool/scripts/seat.ts +191 -0
- package/prefabs/rustic-square-stool/thumbnail.png +0 -0
- package/prefabs/server-clock/ai.md +67 -0
- package/prefabs/server-clock/composite.json +45 -0
- package/prefabs/server-clock/data.json +24 -0
- package/prefabs/server-clock/scripts/clock-hud.tsx +55 -0
- package/prefabs/server-clock/scripts/server-clock.ts +71 -0
- package/prefabs/server-clock/thumbnail.png +0 -0
- package/prefabs/simple-chair/Chair_03.glb +0 -0
- package/prefabs/simple-chair/composite.json +74 -0
- package/prefabs/simple-chair/data.json +34 -0
- package/prefabs/simple-chair/file1.png +0 -0
- package/prefabs/simple-chair/scripts/seat.ts +191 -0
- package/prefabs/simple-chair/thumbnail.png +0 -0
- package/prefabs/sit-spot/composite.json +45 -0
- package/prefabs/sit-spot/data.json +30 -0
- package/prefabs/sit-spot/scripts/seat.ts +191 -0
- package/prefabs/sit-spot/thumbnail.png +0 -0
- package/prefabs/sit-spot-edge/composite.json +42 -0
- package/prefabs/sit-spot-edge/data.json +32 -0
- package/prefabs/sit-spot-edge/emotes/Sit_Edge1_emote.glb +0 -0
- package/prefabs/sit-spot-edge/emotes/Sit_Edge2_emote.glb +0 -0
- package/prefabs/sit-spot-edge/scripts/seat.ts +191 -0
- package/prefabs/sit-spot-edge/thumbnail.png +0 -0
- package/prefabs/small-couch/TwoSeater_Couch.glb +0 -0
- package/prefabs/small-couch/composite.json +100 -0
- package/prefabs/small-couch/data.json +30 -0
- package/prefabs/small-couch/scripts/seat.ts +191 -0
- package/prefabs/small-couch/thumbnail.png +0 -0
- package/prefabs/spawner/ai.md +109 -0
- package/prefabs/spawner/composite.json +73 -0
- package/prefabs/spawner/data.json +22 -0
- package/prefabs/spawner/scripts/spawner.ts +380 -0
- package/prefabs/spawner/thumbnail.png +0 -0
- package/prefabs/steampunk-bench/Steampunk Bench.glb +0 -0
- package/prefabs/steampunk-bench/composite.json +100 -0
- package/prefabs/steampunk-bench/data.json +30 -0
- package/prefabs/steampunk-bench/scripts/seat.ts +191 -0
- package/prefabs/steampunk-bench/thumbnail.png +0 -0
- package/prefabs/steampunk-chair/Steampunk Chair.glb +0 -0
- package/prefabs/steampunk-chair/composite.json +74 -0
- package/prefabs/steampunk-chair/data.json +30 -0
- package/prefabs/steampunk-chair/scripts/seat.ts +191 -0
- package/prefabs/steampunk-chair/thumbnail.png +0 -0
- package/prefabs/tall-stool/Stool_D.glb +0 -0
- package/prefabs/tall-stool/composite.json +74 -0
- package/prefabs/tall-stool/data.json +30 -0
- package/prefabs/tall-stool/scripts/seat.ts +191 -0
- package/prefabs/tall-stool/thumbnail.png +0 -0
- package/prefabs/trigger-zone/ai.md +115 -0
- package/prefabs/trigger-zone/composite.json +52 -0
- package/prefabs/trigger-zone/data.json +31 -0
- package/prefabs/trigger-zone/scripts/trigger-zone.ts +89 -0
- package/prefabs/trigger-zone/thumbnail.png +0 -0
- package/prefabs/video-screen/composite.json +98 -0
- package/prefabs/video-screen/data.json +21 -0
- package/prefabs/video-screen/thumbnail.png +0 -0
- package/prefabs/video-screen/video_player.glb +0 -0
- package/prefabs/wooden-chair/Chair 1.glb +0 -0
- package/prefabs/wooden-chair/composite.json +74 -0
- package/prefabs/wooden-chair/data.json +30 -0
- package/prefabs/wooden-chair/scripts/seat.ts +191 -0
- package/prefabs/wooden-chair/thumbnail.png +0 -0
- package/prefabs/wooden-chair-armrests/Chair 3.glb +0 -0
- package/prefabs/wooden-chair-armrests/composite.json +74 -0
- package/prefabs/wooden-chair-armrests/data.json +30 -0
- package/prefabs/wooden-chair-armrests/scripts/seat.ts +191 -0
- package/prefabs/wooden-chair-armrests/thumbnail.png +0 -0
- package/scene/about +17 -0
- package/scene/bafkreias2w4xuqaic6sdcvblrisayi6xg7drfowzpt5ubjkmrk3mx6czc4 +1 -0
- package/scene/bafkreiby2gmthtauf7cckttphmhpecsueyypi2hzlke4lcodg7fm4cm2sq +23 -0
- package/scene/bafkreidhwqwajqk4nsuffef5fkdrgmp5ql6vnn2knpqsuqalyosv5o6lk4 +1 -0
- package/scene/bafkreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku +0 -0
- package/scene/bafybeihw3xlbff4sf5sc3kvmlmlwf2visnty5lfymawlunzt5vrkp5fd7u +90 -0
- package/web-build/esbuild.wasm +0 -0
- package/web-build/index.js +3 -0
- package/web-build/sdk-snapshot.json +1 -0
- package/web-build/worker.js +701 -0
- package/README.md +0 -3
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// Sit on this seat: click a sit spot -> the avatar is placed on it facing forward
|
|
2
|
+
// and a looping sitting emote plays; moving cancels the emote engine-side (the
|
|
3
|
+
// same mechanism the Creator Hub's seat smart items and Genesis Plaza use — no
|
|
4
|
+
// avatar attach). Spots are child entities named "Sit Spot…" carrying only a
|
|
5
|
+
// Transform; this script gives them their collider and pointer event at runtime.
|
|
6
|
+
// With no such children (the standalone Sit Spot prefab), the entity itself is
|
|
7
|
+
// the spot.
|
|
8
|
+
import {
|
|
9
|
+
ColliderLayer,
|
|
10
|
+
InputAction,
|
|
11
|
+
Material,
|
|
12
|
+
MeshCollider,
|
|
13
|
+
MeshRenderer,
|
|
14
|
+
Schemas,
|
|
15
|
+
Transform,
|
|
16
|
+
VisibilityComponent,
|
|
17
|
+
engine,
|
|
18
|
+
pointerEventsSystem,
|
|
19
|
+
type Entity,
|
|
20
|
+
type LastWriteWinElementSetComponentDefinition
|
|
21
|
+
} from '@dcl/sdk/ecs'
|
|
22
|
+
import { Quaternion, Vector3 } from '@dcl/sdk/math'
|
|
23
|
+
import { isServer } from '@dcl/sdk/network'
|
|
24
|
+
import { movePlayerTo, triggerEmote, triggerSceneEmote } from '~system/RestrictedActions'
|
|
25
|
+
|
|
26
|
+
const SPOT_NAME_PREFIX = 'sit spot'
|
|
27
|
+
const HOVER_TEXT = 'Sit Here'
|
|
28
|
+
const MAX_DISTANCE = 6
|
|
29
|
+
const EMOTE_DELAY_S = 0.25
|
|
30
|
+
const SITTING_EMOTES = ['sittingChair1', 'sittingChair2']
|
|
31
|
+
// emote .glb files bundled in this prefab's folder (Genesis Plaza-style custom
|
|
32
|
+
// poses, played via triggerSceneEmote); empty = the predefined chair emotes
|
|
33
|
+
const SCENE_EMOTES: string[] = []
|
|
34
|
+
|
|
35
|
+
// the Plaza-style "you can sit here" hint: a small glowing dot floating over
|
|
36
|
+
// each free spot, pulsing gently, shown only to nearby players
|
|
37
|
+
const DOT_VISIBLE_DIST = 10
|
|
38
|
+
const DOT_OCCUPIED_DIST = 0.9
|
|
39
|
+
const DOT_HEIGHT = 0.55
|
|
40
|
+
const DOT_SIZE = 0.09
|
|
41
|
+
const DOT_PULSE = 0.25
|
|
42
|
+
|
|
43
|
+
type NameValue = { value: string }
|
|
44
|
+
type NameComponent = LastWriteWinElementSetComponentDefinition<NameValue>
|
|
45
|
+
|
|
46
|
+
// Every placed seat prefab bundles its own copy of this script (prefab folders are
|
|
47
|
+
// self-contained), so the component may already be defined by a sibling copy.
|
|
48
|
+
function nameComponent(): NameComponent {
|
|
49
|
+
const existing = engine.getComponentOrNull('core-schema::Name')
|
|
50
|
+
if (existing !== null) return existing as NameComponent
|
|
51
|
+
return engine.defineComponent('core-schema::Name', { value: Schemas.String })
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isSpotName(name: unknown): boolean {
|
|
55
|
+
return typeof name === 'string' && name.toLowerCase().startsWith(SPOT_NAME_PREFIX)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function sceneRelativeTransform(entity: Entity): { position: Vector3; rotation: Quaternion } {
|
|
59
|
+
const chain: Entity[] = []
|
|
60
|
+
let cursor = entity
|
|
61
|
+
while (cursor !== engine.RootEntity && chain.length < 32) {
|
|
62
|
+
chain.push(cursor)
|
|
63
|
+
const t = Transform.getOrNull(cursor)
|
|
64
|
+
if (t === null) break
|
|
65
|
+
cursor = t.parent ?? engine.RootEntity
|
|
66
|
+
}
|
|
67
|
+
let position = Vector3.Zero()
|
|
68
|
+
let rotation = Quaternion.Identity()
|
|
69
|
+
let scale = Vector3.One()
|
|
70
|
+
for (const link of chain.reverse()) {
|
|
71
|
+
const t = Transform.getOrNull(link)
|
|
72
|
+
if (t === null) continue
|
|
73
|
+
const local = Vector3.create(
|
|
74
|
+
t.position.x * scale.x,
|
|
75
|
+
t.position.y * scale.y,
|
|
76
|
+
t.position.z * scale.z
|
|
77
|
+
)
|
|
78
|
+
position = Vector3.add(position, Vector3.rotate(local, rotation))
|
|
79
|
+
rotation = Quaternion.multiply(rotation, t.rotation)
|
|
80
|
+
scale = Vector3.create(scale.x * t.scale.x, scale.y * t.scale.y, scale.z * t.scale.z)
|
|
81
|
+
}
|
|
82
|
+
return { position, rotation }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export class Seat {
|
|
86
|
+
private emoteIn = 0
|
|
87
|
+
private time = 0
|
|
88
|
+
private dots: Array<{ spot: Entity; dot: Entity; visible: boolean }> = []
|
|
89
|
+
|
|
90
|
+
constructor(
|
|
91
|
+
public src: string,
|
|
92
|
+
public entity: Entity
|
|
93
|
+
) {}
|
|
94
|
+
|
|
95
|
+
start(): void {
|
|
96
|
+
if (isServer()) { return }
|
|
97
|
+
const Name = nameComponent()
|
|
98
|
+
const spots: Entity[] = []
|
|
99
|
+
for (const [child, transform] of engine.getEntitiesWith(Transform)) {
|
|
100
|
+
if (transform.parent !== this.entity) continue
|
|
101
|
+
const name = Name.getOrNull(child)
|
|
102
|
+
if (name !== null && isSpotName(name.value)) spots.push(child)
|
|
103
|
+
}
|
|
104
|
+
if (spots.length === 0) spots.push(this.entity)
|
|
105
|
+
|
|
106
|
+
for (const spot of spots) {
|
|
107
|
+
MeshCollider.setBox(spot, ColliderLayer.CL_POINTER)
|
|
108
|
+
pointerEventsSystem.onPointerDown(
|
|
109
|
+
{
|
|
110
|
+
entity: spot,
|
|
111
|
+
opts: { button: InputAction.IA_POINTER, hoverText: HOVER_TEXT, maxDistance: MAX_DISTANCE }
|
|
112
|
+
},
|
|
113
|
+
() => this.sit(spot)
|
|
114
|
+
)
|
|
115
|
+
// no collider on the dot: clicks pass through to the spot beneath it
|
|
116
|
+
const dot = engine.addEntity()
|
|
117
|
+
Transform.create(dot, {
|
|
118
|
+
parent: spot,
|
|
119
|
+
position: Vector3.create(0, DOT_HEIGHT, 0),
|
|
120
|
+
scale: Vector3.create(DOT_SIZE, DOT_SIZE, DOT_SIZE)
|
|
121
|
+
})
|
|
122
|
+
MeshRenderer.setSphere(dot)
|
|
123
|
+
Material.setPbrMaterial(dot, {
|
|
124
|
+
albedoColor: { r: 1, g: 1, b: 1, a: 0.85 },
|
|
125
|
+
emissiveColor: { r: 1, g: 1, b: 1 },
|
|
126
|
+
emissiveIntensity: 2,
|
|
127
|
+
roughness: 1
|
|
128
|
+
})
|
|
129
|
+
VisibilityComponent.create(dot, { visible: false })
|
|
130
|
+
this.dots.push({ spot, dot, visible: false })
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
update(dt: number): void {
|
|
135
|
+
// The hint walks a transform chain per spot per frame, and a seat is placed
|
|
136
|
+
// by the dozen: without this the Multiplayer Server pays for every one.
|
|
137
|
+
if (isServer()) { return }
|
|
138
|
+
this.time += dt
|
|
139
|
+
this.updateDots()
|
|
140
|
+
if (this.emoteIn <= 0) return
|
|
141
|
+
this.emoteIn -= dt
|
|
142
|
+
if (this.emoteIn > 0) return
|
|
143
|
+
if (SCENE_EMOTES.length > 0) {
|
|
144
|
+
// src is the script's directory (custom/<slug>/scripts) — the emotes live
|
|
145
|
+
// one level up, in the placed prefab's own folder
|
|
146
|
+
const root = this.src.replace(/\/scripts\/?$/, '')
|
|
147
|
+
const emote = SCENE_EMOTES[Math.floor(Math.random() * SCENE_EMOTES.length)]
|
|
148
|
+
void triggerSceneEmote({ src: `${root}/${emote}`, loop: true })
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
const emote = SITTING_EMOTES[Math.floor(Math.random() * SITTING_EMOTES.length)]
|
|
152
|
+
void triggerEmote({ predefinedEmote: emote })
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private updateDots(): void {
|
|
156
|
+
const player = Transform.getOrNull(engine.PlayerEntity)?.position
|
|
157
|
+
const pulse = 1 + DOT_PULSE * Math.sin(this.time * 3)
|
|
158
|
+
for (const entry of this.dots) {
|
|
159
|
+
let visible = false
|
|
160
|
+
if (player !== undefined) {
|
|
161
|
+
const world = sceneRelativeTransform(entry.spot).position
|
|
162
|
+
const dist = Vector3.distance(player, world)
|
|
163
|
+
// hidden while someone (you) is on the spot — a glow over a seated
|
|
164
|
+
// avatar's head reads as a bug, not a hint
|
|
165
|
+
visible = dist < DOT_VISIBLE_DIST && dist > DOT_OCCUPIED_DIST
|
|
166
|
+
}
|
|
167
|
+
if (entry.visible !== visible) {
|
|
168
|
+
entry.visible = visible
|
|
169
|
+
VisibilityComponent.getMutable(entry.dot).visible = visible
|
|
170
|
+
}
|
|
171
|
+
if (visible) {
|
|
172
|
+
Transform.getMutable(entry.dot).scale = Vector3.create(
|
|
173
|
+
DOT_SIZE * pulse,
|
|
174
|
+
DOT_SIZE * pulse,
|
|
175
|
+
DOT_SIZE * pulse
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private sit(spot: Entity): void {
|
|
182
|
+
const world = sceneRelativeTransform(spot)
|
|
183
|
+
const forward = Vector3.rotate(Vector3.Forward(), world.rotation)
|
|
184
|
+
void movePlayerTo({
|
|
185
|
+
newRelativePosition: world.position,
|
|
186
|
+
avatarTarget: Vector3.add(world.position, forward)
|
|
187
|
+
})
|
|
188
|
+
// let the teleport settle before the emote starts, or the walk cancels it
|
|
189
|
+
this.emoteIn = EMOTE_DELAY_S
|
|
190
|
+
}
|
|
191
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"components": [
|
|
4
|
+
{
|
|
5
|
+
"name": "core::GltfContainer",
|
|
6
|
+
"data": {
|
|
7
|
+
"0": {
|
|
8
|
+
"json": {
|
|
9
|
+
"src": "{assetPath}/ThreeSeater_Couch.glb",
|
|
10
|
+
"visibleMeshesCollisionMask": 0,
|
|
11
|
+
"invisibleMeshesCollisionMask": 3
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"name": "core-schema::Name",
|
|
18
|
+
"data": {
|
|
19
|
+
"0": {
|
|
20
|
+
"json": {
|
|
21
|
+
"value": "Large Couch"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"513": {
|
|
25
|
+
"json": {
|
|
26
|
+
"value": "Sit Spot 1"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"514": {
|
|
30
|
+
"json": {
|
|
31
|
+
"value": "Sit Spot 2"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "core::Transform",
|
|
38
|
+
"data": {
|
|
39
|
+
"513": {
|
|
40
|
+
"json": {
|
|
41
|
+
"position": {
|
|
42
|
+
"x": -0.6891,
|
|
43
|
+
"y": 0.0556,
|
|
44
|
+
"z": 0.0829
|
|
45
|
+
},
|
|
46
|
+
"rotation": {
|
|
47
|
+
"x": 0,
|
|
48
|
+
"y": 0.07845906168222427,
|
|
49
|
+
"z": 0,
|
|
50
|
+
"w": 0.9969173669815063
|
|
51
|
+
},
|
|
52
|
+
"scale": {
|
|
53
|
+
"x": 1,
|
|
54
|
+
"y": 1,
|
|
55
|
+
"z": 1
|
|
56
|
+
},
|
|
57
|
+
"parent": 0
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"514": {
|
|
61
|
+
"json": {
|
|
62
|
+
"position": {
|
|
63
|
+
"x": -2.24,
|
|
64
|
+
"y": 0.06,
|
|
65
|
+
"z": 0.14
|
|
66
|
+
},
|
|
67
|
+
"rotation": {
|
|
68
|
+
"x": 0,
|
|
69
|
+
"y": 0.07845909893512726,
|
|
70
|
+
"z": 0,
|
|
71
|
+
"w": 0.9969173073768616
|
|
72
|
+
},
|
|
73
|
+
"scale": {
|
|
74
|
+
"x": 1,
|
|
75
|
+
"y": 1,
|
|
76
|
+
"z": 1
|
|
77
|
+
},
|
|
78
|
+
"parent": 0
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"name": "asset-packs::Script",
|
|
85
|
+
"data": {
|
|
86
|
+
"0": {
|
|
87
|
+
"json": {
|
|
88
|
+
"value": [
|
|
89
|
+
{
|
|
90
|
+
"path": "{assetPath}/scripts/seat.ts",
|
|
91
|
+
"priority": 0,
|
|
92
|
+
"layout": "{\"params\":{},\"actions\":[]}"
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "ac233c16-699e-4f01-8e7c-910bb5a168af",
|
|
3
|
+
"name": "Large Couch",
|
|
4
|
+
"description": "A long couch, seating three. Players can sit along it.",
|
|
5
|
+
"category": "custom",
|
|
6
|
+
"tags": [
|
|
7
|
+
"seat",
|
|
8
|
+
"furniture",
|
|
9
|
+
"sit"
|
|
10
|
+
],
|
|
11
|
+
"version": "1.1.0",
|
|
12
|
+
"changelog": [
|
|
13
|
+
{
|
|
14
|
+
"version": "1.1.0",
|
|
15
|
+
"notes": "The seat no longer runs on the Multiplayer Server, where it was walking every sit spot's transform chain each frame for nobody to see."
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"version": "1.0.0",
|
|
19
|
+
"notes": "Initial release"
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"origin": {
|
|
23
|
+
"source": "builtin"
|
|
24
|
+
},
|
|
25
|
+
"requiredPermissions": [
|
|
26
|
+
"ALLOW_TO_MOVE_PLAYER_INSIDE_SCENE",
|
|
27
|
+
"ALLOW_TO_TRIGGER_AVATAR_EMOTE"
|
|
28
|
+
],
|
|
29
|
+
"group": "Seats"
|
|
30
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// Sit on this seat: click a sit spot -> the avatar is placed on it facing forward
|
|
2
|
+
// and a looping sitting emote plays; moving cancels the emote engine-side (the
|
|
3
|
+
// same mechanism the Creator Hub's seat smart items and Genesis Plaza use — no
|
|
4
|
+
// avatar attach). Spots are child entities named "Sit Spot…" carrying only a
|
|
5
|
+
// Transform; this script gives them their collider and pointer event at runtime.
|
|
6
|
+
// With no such children (the standalone Sit Spot prefab), the entity itself is
|
|
7
|
+
// the spot.
|
|
8
|
+
import {
|
|
9
|
+
ColliderLayer,
|
|
10
|
+
InputAction,
|
|
11
|
+
Material,
|
|
12
|
+
MeshCollider,
|
|
13
|
+
MeshRenderer,
|
|
14
|
+
Schemas,
|
|
15
|
+
Transform,
|
|
16
|
+
VisibilityComponent,
|
|
17
|
+
engine,
|
|
18
|
+
pointerEventsSystem,
|
|
19
|
+
type Entity,
|
|
20
|
+
type LastWriteWinElementSetComponentDefinition
|
|
21
|
+
} from '@dcl/sdk/ecs'
|
|
22
|
+
import { Quaternion, Vector3 } from '@dcl/sdk/math'
|
|
23
|
+
import { isServer } from '@dcl/sdk/network'
|
|
24
|
+
import { movePlayerTo, triggerEmote, triggerSceneEmote } from '~system/RestrictedActions'
|
|
25
|
+
|
|
26
|
+
const SPOT_NAME_PREFIX = 'sit spot'
|
|
27
|
+
const HOVER_TEXT = 'Sit Here'
|
|
28
|
+
const MAX_DISTANCE = 6
|
|
29
|
+
const EMOTE_DELAY_S = 0.25
|
|
30
|
+
const SITTING_EMOTES = ['sittingChair1', 'sittingChair2']
|
|
31
|
+
// emote .glb files bundled in this prefab's folder (Genesis Plaza-style custom
|
|
32
|
+
// poses, played via triggerSceneEmote); empty = the predefined chair emotes
|
|
33
|
+
const SCENE_EMOTES: string[] = []
|
|
34
|
+
|
|
35
|
+
// the Plaza-style "you can sit here" hint: a small glowing dot floating over
|
|
36
|
+
// each free spot, pulsing gently, shown only to nearby players
|
|
37
|
+
const DOT_VISIBLE_DIST = 10
|
|
38
|
+
const DOT_OCCUPIED_DIST = 0.9
|
|
39
|
+
const DOT_HEIGHT = 0.55
|
|
40
|
+
const DOT_SIZE = 0.09
|
|
41
|
+
const DOT_PULSE = 0.25
|
|
42
|
+
|
|
43
|
+
type NameValue = { value: string }
|
|
44
|
+
type NameComponent = LastWriteWinElementSetComponentDefinition<NameValue>
|
|
45
|
+
|
|
46
|
+
// Every placed seat prefab bundles its own copy of this script (prefab folders are
|
|
47
|
+
// self-contained), so the component may already be defined by a sibling copy.
|
|
48
|
+
function nameComponent(): NameComponent {
|
|
49
|
+
const existing = engine.getComponentOrNull('core-schema::Name')
|
|
50
|
+
if (existing !== null) return existing as NameComponent
|
|
51
|
+
return engine.defineComponent('core-schema::Name', { value: Schemas.String })
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isSpotName(name: unknown): boolean {
|
|
55
|
+
return typeof name === 'string' && name.toLowerCase().startsWith(SPOT_NAME_PREFIX)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function sceneRelativeTransform(entity: Entity): { position: Vector3; rotation: Quaternion } {
|
|
59
|
+
const chain: Entity[] = []
|
|
60
|
+
let cursor = entity
|
|
61
|
+
while (cursor !== engine.RootEntity && chain.length < 32) {
|
|
62
|
+
chain.push(cursor)
|
|
63
|
+
const t = Transform.getOrNull(cursor)
|
|
64
|
+
if (t === null) break
|
|
65
|
+
cursor = t.parent ?? engine.RootEntity
|
|
66
|
+
}
|
|
67
|
+
let position = Vector3.Zero()
|
|
68
|
+
let rotation = Quaternion.Identity()
|
|
69
|
+
let scale = Vector3.One()
|
|
70
|
+
for (const link of chain.reverse()) {
|
|
71
|
+
const t = Transform.getOrNull(link)
|
|
72
|
+
if (t === null) continue
|
|
73
|
+
const local = Vector3.create(
|
|
74
|
+
t.position.x * scale.x,
|
|
75
|
+
t.position.y * scale.y,
|
|
76
|
+
t.position.z * scale.z
|
|
77
|
+
)
|
|
78
|
+
position = Vector3.add(position, Vector3.rotate(local, rotation))
|
|
79
|
+
rotation = Quaternion.multiply(rotation, t.rotation)
|
|
80
|
+
scale = Vector3.create(scale.x * t.scale.x, scale.y * t.scale.y, scale.z * t.scale.z)
|
|
81
|
+
}
|
|
82
|
+
return { position, rotation }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export class Seat {
|
|
86
|
+
private emoteIn = 0
|
|
87
|
+
private time = 0
|
|
88
|
+
private dots: Array<{ spot: Entity; dot: Entity; visible: boolean }> = []
|
|
89
|
+
|
|
90
|
+
constructor(
|
|
91
|
+
public src: string,
|
|
92
|
+
public entity: Entity
|
|
93
|
+
) {}
|
|
94
|
+
|
|
95
|
+
start(): void {
|
|
96
|
+
if (isServer()) { return }
|
|
97
|
+
const Name = nameComponent()
|
|
98
|
+
const spots: Entity[] = []
|
|
99
|
+
for (const [child, transform] of engine.getEntitiesWith(Transform)) {
|
|
100
|
+
if (transform.parent !== this.entity) continue
|
|
101
|
+
const name = Name.getOrNull(child)
|
|
102
|
+
if (name !== null && isSpotName(name.value)) spots.push(child)
|
|
103
|
+
}
|
|
104
|
+
if (spots.length === 0) spots.push(this.entity)
|
|
105
|
+
|
|
106
|
+
for (const spot of spots) {
|
|
107
|
+
MeshCollider.setBox(spot, ColliderLayer.CL_POINTER)
|
|
108
|
+
pointerEventsSystem.onPointerDown(
|
|
109
|
+
{
|
|
110
|
+
entity: spot,
|
|
111
|
+
opts: { button: InputAction.IA_POINTER, hoverText: HOVER_TEXT, maxDistance: MAX_DISTANCE }
|
|
112
|
+
},
|
|
113
|
+
() => this.sit(spot)
|
|
114
|
+
)
|
|
115
|
+
// no collider on the dot: clicks pass through to the spot beneath it
|
|
116
|
+
const dot = engine.addEntity()
|
|
117
|
+
Transform.create(dot, {
|
|
118
|
+
parent: spot,
|
|
119
|
+
position: Vector3.create(0, DOT_HEIGHT, 0),
|
|
120
|
+
scale: Vector3.create(DOT_SIZE, DOT_SIZE, DOT_SIZE)
|
|
121
|
+
})
|
|
122
|
+
MeshRenderer.setSphere(dot)
|
|
123
|
+
Material.setPbrMaterial(dot, {
|
|
124
|
+
albedoColor: { r: 1, g: 1, b: 1, a: 0.85 },
|
|
125
|
+
emissiveColor: { r: 1, g: 1, b: 1 },
|
|
126
|
+
emissiveIntensity: 2,
|
|
127
|
+
roughness: 1
|
|
128
|
+
})
|
|
129
|
+
VisibilityComponent.create(dot, { visible: false })
|
|
130
|
+
this.dots.push({ spot, dot, visible: false })
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
update(dt: number): void {
|
|
135
|
+
// The hint walks a transform chain per spot per frame, and a seat is placed
|
|
136
|
+
// by the dozen: without this the Multiplayer Server pays for every one.
|
|
137
|
+
if (isServer()) { return }
|
|
138
|
+
this.time += dt
|
|
139
|
+
this.updateDots()
|
|
140
|
+
if (this.emoteIn <= 0) return
|
|
141
|
+
this.emoteIn -= dt
|
|
142
|
+
if (this.emoteIn > 0) return
|
|
143
|
+
if (SCENE_EMOTES.length > 0) {
|
|
144
|
+
// src is the script's directory (custom/<slug>/scripts) — the emotes live
|
|
145
|
+
// one level up, in the placed prefab's own folder
|
|
146
|
+
const root = this.src.replace(/\/scripts\/?$/, '')
|
|
147
|
+
const emote = SCENE_EMOTES[Math.floor(Math.random() * SCENE_EMOTES.length)]
|
|
148
|
+
void triggerSceneEmote({ src: `${root}/${emote}`, loop: true })
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
const emote = SITTING_EMOTES[Math.floor(Math.random() * SITTING_EMOTES.length)]
|
|
152
|
+
void triggerEmote({ predefinedEmote: emote })
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private updateDots(): void {
|
|
156
|
+
const player = Transform.getOrNull(engine.PlayerEntity)?.position
|
|
157
|
+
const pulse = 1 + DOT_PULSE * Math.sin(this.time * 3)
|
|
158
|
+
for (const entry of this.dots) {
|
|
159
|
+
let visible = false
|
|
160
|
+
if (player !== undefined) {
|
|
161
|
+
const world = sceneRelativeTransform(entry.spot).position
|
|
162
|
+
const dist = Vector3.distance(player, world)
|
|
163
|
+
// hidden while someone (you) is on the spot — a glow over a seated
|
|
164
|
+
// avatar's head reads as a bug, not a hint
|
|
165
|
+
visible = dist < DOT_VISIBLE_DIST && dist > DOT_OCCUPIED_DIST
|
|
166
|
+
}
|
|
167
|
+
if (entry.visible !== visible) {
|
|
168
|
+
entry.visible = visible
|
|
169
|
+
VisibilityComponent.getMutable(entry.dot).visible = visible
|
|
170
|
+
}
|
|
171
|
+
if (visible) {
|
|
172
|
+
Transform.getMutable(entry.dot).scale = Vector3.create(
|
|
173
|
+
DOT_SIZE * pulse,
|
|
174
|
+
DOT_SIZE * pulse,
|
|
175
|
+
DOT_SIZE * pulse
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private sit(spot: Entity): void {
|
|
182
|
+
const world = sceneRelativeTransform(spot)
|
|
183
|
+
const forward = Vector3.rotate(Vector3.Forward(), world.rotation)
|
|
184
|
+
void movePlayerTo({
|
|
185
|
+
newRelativePosition: world.position,
|
|
186
|
+
avatarTarget: Vector3.add(world.position, forward)
|
|
187
|
+
})
|
|
188
|
+
// let the teleport settle before the emote starts, or the walk cancels it
|
|
189
|
+
this.emoteIn = EMOTE_DELAY_S
|
|
190
|
+
}
|
|
191
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
prefab: leaderboard
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Leaderboard — AI guide
|
|
6
|
+
|
|
7
|
+
A panel that shows one board: it reads a `game.state` key and paints the places.
|
|
8
|
+
It never decides or stores a score.
|
|
9
|
+
|
|
10
|
+
The project copy is normally custom/leaderboard/ — check what is on disk, a
|
|
11
|
+
second copy is custom/leaderboard_2/.
|
|
12
|
+
|
|
13
|
+
## When to use
|
|
14
|
+
|
|
15
|
+
Whenever a game already keeps a ranked list and wants it on a wall. Place one per
|
|
16
|
+
board: round scores and all-time bests are two keys, so two panels.
|
|
17
|
+
|
|
18
|
+
Place NOTHING if no server code writes rows — the panel would just show its empty
|
|
19
|
+
line. Write the rows first, then place the board on that key.
|
|
20
|
+
|
|
21
|
+
## API
|
|
22
|
+
|
|
23
|
+
There is nothing to import. The contract is the state key:
|
|
24
|
+
|
|
25
|
+
game.setState({ leaderboard: [{ player, score }, …] })
|
|
26
|
+
|
|
27
|
+
- Rows are objects. The player field may be `player`, `p`, `address`, `wallet` or
|
|
28
|
+
`name`; the score field may be `score`, `points`, `pts`, `value`, `seconds`,
|
|
29
|
+
`time` or `best`. Anything else in a row is ignored, and a row missing either
|
|
30
|
+
half is skipped.
|
|
31
|
+
- WHICH SIDE: the `game.setState` that writes the rows goes on the SERVER, inside
|
|
32
|
+
the `if (isServer())` branch. The panel itself reads and paints on every
|
|
33
|
+
player's client — that half is this prefab's, and you write no client code.
|
|
34
|
+
- The panel sorts the rows itself, so the writer's order does not matter.
|
|
35
|
+
- Late joiners get the board from the snapshot. Write rows with `game.setState`,
|
|
36
|
+
never with a broadcast — a message is a moment and a board is a fact.
|
|
37
|
+
|
|
38
|
+
Params of the prefab's script — set them in the placePrefab request:
|
|
39
|
+
|
|
40
|
+
- `title`: the heading on the panel (default "Leaderboard").
|
|
41
|
+
- `boardKey`: the `game.state` key to read (default `'leaderboard'`). Game Flow
|
|
42
|
+
reads the same key for its winners line — point both at one key.
|
|
43
|
+
- `sort`: `'desc'` (highest wins — points) or `'asc'` (lowest wins — best times,
|
|
44
|
+
which also renders the score as m:ss).
|
|
45
|
+
- `rows`: how many places the panel lists (default 8, max 25).
|
|
46
|
+
|
|
47
|
+
## The all-time board idiom
|
|
48
|
+
|
|
49
|
+
`game.playerData` cannot be listed, so an all-time or season board is kept by
|
|
50
|
+
folding results into `game.saved` at the end of a round and copying the top N
|
|
51
|
+
into `game.state` — all of it in the server branch:
|
|
52
|
+
|
|
53
|
+
const board = fold(game.saved.get('bestTimes') ?? [], results)
|
|
54
|
+
game.saved.set('bestTimes', board)
|
|
55
|
+
game.setState({ bestTimes: board })
|
|
56
|
+
|
|
57
|
+
Then place a second Leaderboard with `boardKey: 'bestTimes'`.
|
|
58
|
+
|
|
59
|
+
## Do / Don't
|
|
60
|
+
|
|
61
|
+
- DON'T keep a score on a client and send it up. Count it in a server handler; a
|
|
62
|
+
number a client reports is a claim.
|
|
63
|
+
- DON'T write more than the visible places into the key. Fold to the top ten in
|
|
64
|
+
server code — every extra row rides the wire on every change.
|
|
65
|
+
- DO expect wallet addresses. There is no name lookup for a player who is not
|
|
66
|
+
connected, so offline players show as a shortened address.
|
|
67
|
+
|
|
68
|
+
## Example
|
|
69
|
+
|
|
70
|
+
"Show the fastest climbers": in a handler inside the server branch keep the list,
|
|
71
|
+
and place a board on that key.
|
|
72
|
+
|
|
73
|
+
game.setState({ leaderboard: runs.sort((a, b) => a.time - b.time).slice(0, 10) })
|
|
74
|
+
|
|
75
|
+
with `boardKey: 'leaderboard'`, `sort: 'asc'`, `title: 'Best Times'`.
|
|
Binary file
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"components": [
|
|
4
|
+
{
|
|
5
|
+
"name": "core-schema::Name",
|
|
6
|
+
"data": {
|
|
7
|
+
"512": {
|
|
8
|
+
"json": {
|
|
9
|
+
"value": "Leaderboard"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"513": {
|
|
13
|
+
"json": {
|
|
14
|
+
"value": "Board Text"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"name": "core::Transform",
|
|
21
|
+
"data": {
|
|
22
|
+
"512": {
|
|
23
|
+
"json": {
|
|
24
|
+
"position": { "x": 0, "y": 0, "z": 0 },
|
|
25
|
+
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
|
|
26
|
+
"scale": { "x": 2, "y": 2, "z": 2 }
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"513": {
|
|
30
|
+
"json": {
|
|
31
|
+
"position": { "x": 0, "y": 0.45, "z": 0.02 },
|
|
32
|
+
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
|
|
33
|
+
"scale": { "x": 1, "y": 1, "z": 1 },
|
|
34
|
+
"parent": 512
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"name": "core::GltfContainer",
|
|
41
|
+
"data": {
|
|
42
|
+
"512": {
|
|
43
|
+
"json": {
|
|
44
|
+
"src": "{assetPath}/board-panel.glb",
|
|
45
|
+
"visibleMeshesCollisionMask": 0,
|
|
46
|
+
"invisibleMeshesCollisionMask": 0
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"name": "core::TextShape",
|
|
53
|
+
"data": {
|
|
54
|
+
"513": {
|
|
55
|
+
"json": {
|
|
56
|
+
"text": "LEADERBOARD\n\nNothing to show yet — set the board key your game writes to.",
|
|
57
|
+
"fontSize": 0.8,
|
|
58
|
+
"textColor": { "r": 0.08, "g": 0.09, "b": 0.11, "a": 1 },
|
|
59
|
+
"outlineColor": { "r": 1, "g": 1, "b": 1 },
|
|
60
|
+
"outlineWidth": 0
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"name": "asset-packs::Script",
|
|
67
|
+
"data": {
|
|
68
|
+
"512": {
|
|
69
|
+
"json": {
|
|
70
|
+
"value": [
|
|
71
|
+
{
|
|
72
|
+
"path": "{assetPath}/scripts/leaderboard.ts",
|
|
73
|
+
"priority": 0,
|
|
74
|
+
"layout": "{\"params\":{},\"actions\":[]}"
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
}
|