@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
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
prefab: game-flow
|
|
3
|
+
claims-state: game.state.flow
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Game Flow — AI guide
|
|
7
|
+
|
|
8
|
+
Lobby, countdown, rounds, winners: the prefab that runs a game from start to end,
|
|
9
|
+
on top of `game.round`.
|
|
10
|
+
|
|
11
|
+
The project copy is normally custom/game_flow/ — check what is on disk, a second
|
|
12
|
+
copy is custom/game_flow_2/.
|
|
13
|
+
|
|
14
|
+
## When to use
|
|
15
|
+
|
|
16
|
+
Whenever the game is played in rounds: a race, a wave survival, a timed climb, a
|
|
17
|
+
scored match. Place ONE anywhere; it is a sign you can move or hide. A second
|
|
18
|
+
copy paints its own sign and drives nothing.
|
|
19
|
+
|
|
20
|
+
Do NOT write a phase machine, a lobby timer or a "round over" flag in a script —
|
|
21
|
+
this prefab owns all three, and two clocks writing one game never agree.
|
|
22
|
+
|
|
23
|
+
## API
|
|
24
|
+
|
|
25
|
+
There is nothing to import. Game Flow talks through the synced state and messages
|
|
26
|
+
every script already has:
|
|
27
|
+
|
|
28
|
+
- `game.state.flow` — `{ phase, endsAtMs, round, present }`. `phase` is
|
|
29
|
+
`'lobby' | 'round' | 'intermission'`; `endsAtMs` is a `game.now()` instant
|
|
30
|
+
(0 while the lobby is parked); `round` counts rounds PLAYED, from 1; `present`
|
|
31
|
+
is the head count. Read it, never write it.
|
|
32
|
+
- `game.round` / `game.onRoundStart` — the engine's round tuple, as always. Game
|
|
33
|
+
Flow starts rounds with `game.newRound()`, so `game.round.number` and
|
|
34
|
+
`game.state.flow.round` differ by the boot round. Key per-round validity on
|
|
35
|
+
`game.round.id`, never on the number: the number counts rounds inside one wake
|
|
36
|
+
of the Multiplayer Server and starts again at 1 after it sleeps, so a token
|
|
37
|
+
written down in one wake would read as current in the next.
|
|
38
|
+
- `game.broadcast('announce', { text })` — Game Flow tells every player who won at
|
|
39
|
+
the end of a round. If an Announcer is placed it shows the line; if not,
|
|
40
|
+
nothing happens. Read custom/announcer/ai.md if one is placed.
|
|
41
|
+
|
|
42
|
+
WHICH SIDE: Game Flow decides everything on the SERVER and every client paints
|
|
43
|
+
the sign from `game.state.flow`. Read the state on either side; write nothing,
|
|
44
|
+
and call `game.newRound()` only from inside an `if (isServer())` branch.
|
|
45
|
+
|
|
46
|
+
Params of the prefab's script — set them in the placePrefab request:
|
|
47
|
+
|
|
48
|
+
- `roundSeconds`: how long a round lasts (default 300). With `endsWhen: 'script'`
|
|
49
|
+
it is only a ceiling.
|
|
50
|
+
- `countdownSeconds`: how long the lobby counts down once enough players are in
|
|
51
|
+
(default 10).
|
|
52
|
+
- `intermissionSeconds`: how long the winners stay up between rounds (default 10).
|
|
53
|
+
- `minPlayers`: players needed before a round starts (default 1).
|
|
54
|
+
- `endsWhen`: `'timer'` (the clock ends the round) or `'script'` (your code ends
|
|
55
|
+
it with `game.newRound()`, and the sign hides a clock it does not own).
|
|
56
|
+
- `boardKey`: the `game.state` key the winners line is read from (default
|
|
57
|
+
`'leaderboard'`). Point a Leaderboard at the same key.
|
|
58
|
+
|
|
59
|
+
## Ending a round from a script
|
|
60
|
+
|
|
61
|
+
Set `endsWhen: 'script'` and call `game.newRound()` inside a server handler when
|
|
62
|
+
your condition hits. Game Flow follows: it announces the winners of the round
|
|
63
|
+
that just ended and starts the new one. Never call `game.newRound()` on a client
|
|
64
|
+
and never both end the round and run your own intermission — the ceiling is the
|
|
65
|
+
only other thing that can end it, and it exists so a forgotten call cannot wedge
|
|
66
|
+
the round.
|
|
67
|
+
|
|
68
|
+
## Do / Don't
|
|
69
|
+
|
|
70
|
+
- DON'T write `game.state.flow` from a script. Set the params instead.
|
|
71
|
+
- DON'T count players yourself for a lobby — `game.state.flow.present` is the
|
|
72
|
+
server's own count, and a client has no roster to count.
|
|
73
|
+
- DON'T reset scores on a timer. Reset them in `game.onRoundStart`, which runs on
|
|
74
|
+
the server for every round Game Flow or your script starts.
|
|
75
|
+
- DO write the board rows before the round ends, not after: the winners line is
|
|
76
|
+
read from `boardKey` at that instant. Rows are `{ player, score }`-ish objects
|
|
77
|
+
— `player`/`p`/`address` and `score`/`points`/`time` are all understood.
|
|
78
|
+
|
|
79
|
+
## Example
|
|
80
|
+
|
|
81
|
+
"End the round as soon as three players finish, and show the winners": place Game
|
|
82
|
+
Flow with `endsWhen: 'script'`, then in the server branch of your own script
|
|
83
|
+
|
|
84
|
+
start(): void {
|
|
85
|
+
if (isServer()) {
|
|
86
|
+
game.onRequest('finish', (_data, player) => {
|
|
87
|
+
const done = [...(game.state.finishers ?? []), player]
|
|
88
|
+
game.setState({ finishers: done, leaderboard: board(done) })
|
|
89
|
+
if (done.length >= 3) game.newRound()
|
|
90
|
+
return { ok: true }
|
|
91
|
+
})
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"components": [
|
|
4
|
+
{
|
|
5
|
+
"name": "core-schema::Name",
|
|
6
|
+
"data": {
|
|
7
|
+
"0": {
|
|
8
|
+
"json": {
|
|
9
|
+
"value": "Game Flow"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "core::TextShape",
|
|
16
|
+
"data": {
|
|
17
|
+
"0": {
|
|
18
|
+
"json": {
|
|
19
|
+
"text": "GAME FLOW\n--:--\nwaiting for the Multiplayer Server",
|
|
20
|
+
"fontSize": 3,
|
|
21
|
+
"textColor": { "r": 1, "g": 1, "b": 1, "a": 1 },
|
|
22
|
+
"outlineColor": { "r": 0, "g": 0, "b": 0 },
|
|
23
|
+
"outlineWidth": 0.1
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "asset-packs::Script",
|
|
30
|
+
"data": {
|
|
31
|
+
"0": {
|
|
32
|
+
"json": {
|
|
33
|
+
"value": [
|
|
34
|
+
{
|
|
35
|
+
"path": "{assetPath}/scripts/game-flow.ts",
|
|
36
|
+
"priority": 0,
|
|
37
|
+
"layout": "{\"params\":{},\"actions\":[]}"
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "44374100-12fb-481f-90ce-2a4d81949084",
|
|
3
|
+
"name": "Game Flow",
|
|
4
|
+
"description": "Lobby, countdown, rounds, winners — runs your game from start to end. Works with: Leaderboard, Announcer.",
|
|
5
|
+
"category": "custom",
|
|
6
|
+
"group": "Multiplayer Server",
|
|
7
|
+
"tags": [
|
|
8
|
+
"multiplayer",
|
|
9
|
+
"server",
|
|
10
|
+
"round",
|
|
11
|
+
"lobby",
|
|
12
|
+
"countdown",
|
|
13
|
+
"timer",
|
|
14
|
+
"winners"
|
|
15
|
+
],
|
|
16
|
+
"requiresSdk": "auth-server",
|
|
17
|
+
"version": "1.0.0",
|
|
18
|
+
"changelog": [
|
|
19
|
+
{
|
|
20
|
+
"version": "1.0.0",
|
|
21
|
+
"notes": "Initial release: lobby → round → winners on game.round, a countdown every client derives from one shared deadline, a winners line read from the board key a Leaderboard shows, and a \"script\" mode where your own game.newRound() ends the round and the round length is only a ceiling."
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"origin": {
|
|
25
|
+
"source": "builtin"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// Lobby, countdown, rounds, winners — the piece that runs a game from start to
|
|
2
|
+
// end. Everything it decides is decided once, on the server; the sign on the
|
|
3
|
+
// placed entity is what every player draws from those decisions.
|
|
4
|
+
//
|
|
5
|
+
// The flow rides one shared fact, `game.state.flow` — a phase, the instant that
|
|
6
|
+
// phase ends, and the number of rounds played. Nothing here is a timer, so a
|
|
7
|
+
// player who joins mid-round reads the same countdown as everybody else, and a
|
|
8
|
+
// restart lands everyone on the phase the scene is actually in.
|
|
9
|
+
//
|
|
10
|
+
// A script may end the round itself with game.newRound(); this piece follows it
|
|
11
|
+
// rather than fighting it, and in that mode the round length is only a ceiling —
|
|
12
|
+
// when the ceiling fires it goes out through game.newRound() too, so a round
|
|
13
|
+
// never ends down two different paths. See ai.md in this folder.
|
|
14
|
+
import { TextShape, type Entity } from '@dcl/sdk/ecs'
|
|
15
|
+
import { isServer } from '@dcl/sdk/network'
|
|
16
|
+
import { game } from '~runtime/game'
|
|
17
|
+
import {
|
|
18
|
+
advanceFlow,
|
|
19
|
+
asFlowFact,
|
|
20
|
+
configFromSeconds,
|
|
21
|
+
intermissionState,
|
|
22
|
+
lobbyState,
|
|
23
|
+
panelText,
|
|
24
|
+
podiumLine,
|
|
25
|
+
roundState,
|
|
26
|
+
type FlowConfig,
|
|
27
|
+
type FlowState
|
|
28
|
+
} from './pure/flow'
|
|
29
|
+
|
|
30
|
+
const FLOW_KEY = 'flow'
|
|
31
|
+
const ANNOUNCE = 'announce'
|
|
32
|
+
const TICK_S = 0.25
|
|
33
|
+
const PAINT_S = 0.2
|
|
34
|
+
const PODIUM_PLACES = 3
|
|
35
|
+
|
|
36
|
+
// One Game Flow runs the rounds. A second placed copy still paints its own sign,
|
|
37
|
+
// but never drives the phases: two writers on one fact is a fight with no
|
|
38
|
+
// winner, and a creator would only see the countdown stutter.
|
|
39
|
+
let claimed = false
|
|
40
|
+
|
|
41
|
+
export class GameFlow {
|
|
42
|
+
private primary = false
|
|
43
|
+
private present = new Set<string>()
|
|
44
|
+
private flow: FlowState = lobbyState()
|
|
45
|
+
private accum = 0
|
|
46
|
+
private painted = ''
|
|
47
|
+
private published = ''
|
|
48
|
+
private warnedCeiling = false
|
|
49
|
+
|
|
50
|
+
constructor(
|
|
51
|
+
public src: string,
|
|
52
|
+
public entity: Entity,
|
|
53
|
+
/** How long a round lasts. With "script" below this is only a ceiling. */
|
|
54
|
+
public roundSeconds: number = 300,
|
|
55
|
+
/** How long the lobby counts down once enough players are in. */
|
|
56
|
+
public countdownSeconds: number = 10,
|
|
57
|
+
/** How long the winners stay up between rounds. */
|
|
58
|
+
public intermissionSeconds: number = 10,
|
|
59
|
+
/** Players needed before a round starts. */
|
|
60
|
+
public minPlayers: number = 1,
|
|
61
|
+
/** Who ends a round: this clock, or your own script calling game.newRound(). */
|
|
62
|
+
public endsWhen: 'timer' | 'script' = 'timer',
|
|
63
|
+
/** The game.state key the winners come from — the key a Leaderboard shows. */
|
|
64
|
+
public boardKey: string = 'leaderboard'
|
|
65
|
+
) {}
|
|
66
|
+
|
|
67
|
+
private get config(): FlowConfig {
|
|
68
|
+
return configFromSeconds(this.roundSeconds, this.countdownSeconds, this.intermissionSeconds, this.minPlayers)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
start(): void {
|
|
72
|
+
if (isServer()) {
|
|
73
|
+
this.primary = !claimed
|
|
74
|
+
claimed = true
|
|
75
|
+
if (!this.primary) {
|
|
76
|
+
console.log('[gameFlow] a second Game Flow is placed — only the first runs the rounds')
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
game.onReady(() => {
|
|
80
|
+
this.flow = lobbyState()
|
|
81
|
+
this.publish()
|
|
82
|
+
})
|
|
83
|
+
game.onPlayerJoin((player) => {
|
|
84
|
+
this.present.add(player)
|
|
85
|
+
})
|
|
86
|
+
game.onPlayerLeave((player) => {
|
|
87
|
+
this.present.delete(player)
|
|
88
|
+
})
|
|
89
|
+
// Every round start comes through here — this piece's own and any a script
|
|
90
|
+
// starts — so the two can never both end one round. A round a script ended
|
|
91
|
+
// gets no podium from here: the script that closed it writes its own, and
|
|
92
|
+
// this one would be read from a board the script has not filled yet.
|
|
93
|
+
game.onRoundStart((round) => {
|
|
94
|
+
// Round 1 is the round every scene boots into; the lobby sits in it.
|
|
95
|
+
if (round.number <= 1) return
|
|
96
|
+
this.flow = roundState(this.flow, this.config, game.now())
|
|
97
|
+
this.publish()
|
|
98
|
+
})
|
|
99
|
+
game.every(TICK_S, () => this.tick())
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
update(dt: number): void {
|
|
104
|
+
if (isServer()) { return }
|
|
105
|
+
this.accum += dt
|
|
106
|
+
if (this.accum < PAINT_S) return
|
|
107
|
+
this.accum = 0
|
|
108
|
+
this.paint()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private tick(): void {
|
|
112
|
+
const config = this.config
|
|
113
|
+
const { state, action } = advanceFlow(this.flow, config, this.present.size, game.now())
|
|
114
|
+
this.flow = state
|
|
115
|
+
if (action === 'startRound') {
|
|
116
|
+
game.newRound()
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
if (action === 'endRound') {
|
|
120
|
+
// In script mode the deadline is only a ceiling, and ending the round here
|
|
121
|
+
// would skip the script's own close — its payout, its podium, its teleport
|
|
122
|
+
// home — with nothing printed. Go out the same door a script goes out.
|
|
123
|
+
if (this.endsWhen === 'script') {
|
|
124
|
+
this.warnCeiling()
|
|
125
|
+
game.newRound()
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
this.announcePodium()
|
|
129
|
+
this.flow = intermissionState(this.flow, config, game.now())
|
|
130
|
+
}
|
|
131
|
+
this.publish()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private warnCeiling(): void {
|
|
135
|
+
if (this.warnedCeiling) return
|
|
136
|
+
this.warnedCeiling = true
|
|
137
|
+
console.log(
|
|
138
|
+
'[server] Game Flow: The round hit its time ceiling — call game.newRound() from your script, or raise the ceiling.'
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private announcePodium(): void {
|
|
143
|
+
game.broadcast(ANNOUNCE, { text: podiumLine(game.state[this.boardKey], PODIUM_PLACES) })
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private publish(): void {
|
|
147
|
+
const fact = { ...this.flow, present: this.present.size }
|
|
148
|
+
const json = JSON.stringify(fact)
|
|
149
|
+
// 4 Hz of an unchanged fact is 4 Hz of wire traffic for nothing.
|
|
150
|
+
if (json === this.published) return
|
|
151
|
+
this.published = json
|
|
152
|
+
game.setState({ [FLOW_KEY]: fact })
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private paint(): void {
|
|
156
|
+
const fact = asFlowFact(game.state[FLOW_KEY])
|
|
157
|
+
const text = panelText(fact, game.now(), this.endsWhen === 'timer', this.config.minPlayers)
|
|
158
|
+
if (text === this.painted) return
|
|
159
|
+
this.painted = text
|
|
160
|
+
TextShape.createOrReplace(this.entity, {
|
|
161
|
+
text,
|
|
162
|
+
fontSize: 3,
|
|
163
|
+
textColor: { r: 1, g: 1, b: 1, a: 1 },
|
|
164
|
+
outlineColor: { r: 0, g: 0, b: 0 },
|
|
165
|
+
outlineWidth: 0.1
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
}
|