@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.
Files changed (277) hide show
  1. package/editor.css +1 -0
  2. package/editor.js +33891 -0
  3. package/package.json +8 -4
  4. package/prefabs/admin-tools/composite.json +77 -0
  5. package/prefabs/admin-tools/data.json +28 -0
  6. package/prefabs/admin-tools/icons/admin-panel-control-button.png +0 -0
  7. package/prefabs/admin-tools/icons/admin-panel-moderation-control-button.png +0 -0
  8. package/prefabs/admin-tools/icons/admin-panel-rewards-control-button.png +0 -0
  9. package/prefabs/admin-tools/icons/admin-panel-smart-item-control-button.png +0 -0
  10. package/prefabs/admin-tools/icons/admin-panel-text-announcement-control-button.png +0 -0
  11. package/prefabs/admin-tools/icons/admin-panel-video-control-button.png +0 -0
  12. package/prefabs/admin-tools/icons/admin-tool-background.png +0 -0
  13. package/prefabs/admin-tools/icons/announcements/text-announcement-chat-message.png +0 -0
  14. package/prefabs/admin-tools/icons/announcements/text-announcement-check.png +0 -0
  15. package/prefabs/admin-tools/icons/announcements/text-announcement-close-button.png +0 -0
  16. package/prefabs/admin-tools/icons/moderation/admin-panel-verified-user.png +0 -0
  17. package/prefabs/admin-tools/icons/moderation/ban.png +0 -0
  18. package/prefabs/admin-tools/icons/moderation/chevron-back.png +0 -0
  19. package/prefabs/admin-tools/icons/moderation/chevron-forward.png +0 -0
  20. package/prefabs/admin-tools/icons/moderation/close.png +0 -0
  21. package/prefabs/admin-tools/icons/moderation/error.png +0 -0
  22. package/prefabs/admin-tools/icons/moderation/person-outline.png +0 -0
  23. package/prefabs/admin-tools/icons/moderation-control-icon.png +0 -0
  24. package/prefabs/admin-tools/icons/rewards/close.png +0 -0
  25. package/prefabs/admin-tools/icons/rewards/send.png +0 -0
  26. package/prefabs/admin-tools/icons/rewards-control.png +0 -0
  27. package/prefabs/admin-tools/icons/smart-item-control.png +0 -0
  28. package/prefabs/admin-tools/icons/text-announcement-control.png +0 -0
  29. package/prefabs/admin-tools/icons/video/chevron-down.png +0 -0
  30. package/prefabs/admin-tools/icons/video/chevron-up.png +0 -0
  31. package/prefabs/admin-tools/icons/video/copy-to-clipboard.png +0 -0
  32. package/prefabs/admin-tools/icons/video/error.png +0 -0
  33. package/prefabs/admin-tools/icons/video/eye-off.png +0 -0
  34. package/prefabs/admin-tools/icons/video/eye.png +0 -0
  35. package/prefabs/admin-tools/icons/video/help.png +0 -0
  36. package/prefabs/admin-tools/icons/video/info.png +0 -0
  37. package/prefabs/admin-tools/icons/video/person-outline.png +0 -0
  38. package/prefabs/admin-tools/icons/video/star.png +0 -0
  39. package/prefabs/admin-tools/icons/video/video-control-dcl-cast.png +0 -0
  40. package/prefabs/admin-tools/icons/video/video-control-live.png +0 -0
  41. package/prefabs/admin-tools/icons/video/video-control-loop.png +0 -0
  42. package/prefabs/admin-tools/icons/video/video-control-mute.png +0 -0
  43. package/prefabs/admin-tools/icons/video/video-control-pause-button.png +0 -0
  44. package/prefabs/admin-tools/icons/video/video-control-play-button.png +0 -0
  45. package/prefabs/admin-tools/icons/video/video-control-video-icon.png +0 -0
  46. package/prefabs/admin-tools/icons/video/video-control-volume-minus-button.png +0 -0
  47. package/prefabs/admin-tools/icons/video/video-control-volume-plus-button.png +0 -0
  48. package/prefabs/admin-tools/icons/video-control.png +0 -0
  49. package/prefabs/admin-tools/scripts/actions.ts +206 -0
  50. package/prefabs/admin-tools/scripts/admin.tsx +253 -0
  51. package/prefabs/admin-tools/scripts/api.ts +125 -0
  52. package/prefabs/admin-tools/scripts/components.ts +190 -0
  53. package/prefabs/admin-tools/scripts/icons.ts +53 -0
  54. package/prefabs/admin-tools/scripts/message-bus.ts +297 -0
  55. package/prefabs/admin-tools/scripts/state.ts +69 -0
  56. package/prefabs/admin-tools/scripts/tabs/Moderation.tsx +155 -0
  57. package/prefabs/admin-tools/scripts/tabs/Rewards.tsx +136 -0
  58. package/prefabs/admin-tools/scripts/tabs/SmartItemActions.tsx +167 -0
  59. package/prefabs/admin-tools/scripts/tabs/TextAnnouncements.tsx +195 -0
  60. package/prefabs/admin-tools/scripts/tabs/VideoControl.tsx +156 -0
  61. package/prefabs/admin-tools/scripts/tabs/announcements/Overlay.tsx +94 -0
  62. package/prefabs/admin-tools/scripts/tabs/announcements/icons.ts +15 -0
  63. package/prefabs/admin-tools/scripts/tabs/announcements/state.ts +71 -0
  64. package/prefabs/admin-tools/scripts/tabs/announcements/ui.tsx +37 -0
  65. package/prefabs/admin-tools/scripts/tabs/moderation/AddUserInput.tsx +184 -0
  66. package/prefabs/admin-tools/scripts/tabs/moderation/RemoveAdminConfirmation.tsx +93 -0
  67. package/prefabs/admin-tools/scripts/tabs/moderation/UsersList.tsx +228 -0
  68. package/prefabs/admin-tools/scripts/tabs/moderation/api.ts +35 -0
  69. package/prefabs/admin-tools/scripts/tabs/moderation/icons.ts +23 -0
  70. package/prefabs/admin-tools/scripts/tabs/moderation/kick.ts +95 -0
  71. package/prefabs/admin-tools/scripts/tabs/moderation/state.ts +69 -0
  72. package/prefabs/admin-tools/scripts/tabs/moderation/ui.tsx +172 -0
  73. package/prefabs/admin-tools/scripts/tabs/moderation/utils.ts +88 -0
  74. package/prefabs/admin-tools/scripts/tabs/rewards/Button.tsx +52 -0
  75. package/prefabs/admin-tools/scripts/tabs/rewards/Captcha.tsx +88 -0
  76. package/prefabs/admin-tools/scripts/tabs/rewards/api.ts +138 -0
  77. package/prefabs/admin-tools/scripts/tabs/rewards/claim.ts +99 -0
  78. package/prefabs/admin-tools/scripts/tabs/rewards/controller.ts +103 -0
  79. package/prefabs/admin-tools/scripts/tabs/rewards/icons.ts +13 -0
  80. package/prefabs/admin-tools/scripts/tabs/rewards/state.ts +32 -0
  81. package/prefabs/admin-tools/scripts/tabs/smart-items/selection.ts +90 -0
  82. package/prefabs/admin-tools/scripts/tabs/video/DclCast.tsx +226 -0
  83. package/prefabs/admin-tools/scripts/tabs/video/LiveStream.tsx +284 -0
  84. package/prefabs/admin-tools/scripts/tabs/video/SpeakerShowcase.tsx +112 -0
  85. package/prefabs/admin-tools/scripts/tabs/video/VideoUrl.tsx +107 -0
  86. package/prefabs/admin-tools/scripts/tabs/video/Volume.tsx +69 -0
  87. package/prefabs/admin-tools/scripts/tabs/video/api.ts +172 -0
  88. package/prefabs/admin-tools/scripts/tabs/video/controls.ts +108 -0
  89. package/prefabs/admin-tools/scripts/tabs/video/icons.ts +59 -0
  90. package/prefabs/admin-tools/scripts/tabs/video/speakers.ts +43 -0
  91. package/prefabs/admin-tools/scripts/tabs/video/state.ts +82 -0
  92. package/prefabs/admin-tools/scripts/tabs/video/ui.tsx +243 -0
  93. package/prefabs/admin-tools/scripts/types.ts +46 -0
  94. package/prefabs/admin-tools/scripts/ui.tsx +122 -0
  95. package/prefabs/admin-tools/thumbnail.png +0 -0
  96. package/prefabs/announcer/ai.md +76 -0
  97. package/prefabs/announcer/composite.json +31 -0
  98. package/prefabs/announcer/data.json +35 -0
  99. package/prefabs/announcer/scripts/announcer.tsx +79 -0
  100. package/prefabs/announcer/scripts/pure/toast.ts +30 -0
  101. package/prefabs/announcer/thumbnail.png +0 -0
  102. package/prefabs/black-chair/HighBackChair.glb +0 -0
  103. package/prefabs/black-chair/composite.json +74 -0
  104. package/prefabs/black-chair/data.json +30 -0
  105. package/prefabs/black-chair/scripts/seat.ts +191 -0
  106. package/prefabs/black-chair/thumbnail.png +0 -0
  107. package/prefabs/catalog.json +1 -0
  108. package/prefabs/classic-bench/Bench_01.glb +0 -0
  109. package/prefabs/classic-bench/composite.json +100 -0
  110. package/prefabs/classic-bench/data.json +34 -0
  111. package/prefabs/classic-bench/file1.png +0 -0
  112. package/prefabs/classic-bench/scripts/seat.ts +191 -0
  113. package/prefabs/classic-bench/thumbnail.png +0 -0
  114. package/prefabs/classic-bench-armrests/Bench 1.glb +0 -0
  115. package/prefabs/classic-bench-armrests/composite.json +100 -0
  116. package/prefabs/classic-bench-armrests/data.json +30 -0
  117. package/prefabs/classic-bench-armrests/scripts/seat.ts +191 -0
  118. package/prefabs/classic-bench-armrests/thumbnail.png +0 -0
  119. package/prefabs/classroom-chair/Chair_02.glb +0 -0
  120. package/prefabs/classroom-chair/composite.json +74 -0
  121. package/prefabs/classroom-chair/data.json +34 -0
  122. package/prefabs/classroom-chair/file1.png +0 -0
  123. package/prefabs/classroom-chair/scripts/seat.ts +191 -0
  124. package/prefabs/classroom-chair/thumbnail.png +0 -0
  125. package/prefabs/court-chair/Chair_Wood_01.glb +0 -0
  126. package/prefabs/court-chair/FanstasyPack_TX.png +0 -0
  127. package/prefabs/court-chair/composite.json +74 -0
  128. package/prefabs/court-chair/data.json +34 -0
  129. package/prefabs/court-chair/scripts/seat.ts +191 -0
  130. package/prefabs/court-chair/thumbnail.png +0 -0
  131. package/prefabs/curved-couch/CurvedCouch.glb +0 -0
  132. package/prefabs/curved-couch/composite.json +100 -0
  133. package/prefabs/curved-couch/data.json +30 -0
  134. package/prefabs/curved-couch/scripts/seat.ts +191 -0
  135. package/prefabs/curved-couch/thumbnail.png +0 -0
  136. package/prefabs/game-flow/ai.md +94 -0
  137. package/prefabs/game-flow/composite.json +45 -0
  138. package/prefabs/game-flow/data.json +27 -0
  139. package/prefabs/game-flow/scripts/game-flow.ts +168 -0
  140. package/prefabs/game-flow/scripts/pure/flow.ts +185 -0
  141. package/prefabs/game-flow/thumbnail.png +0 -0
  142. package/prefabs/health-respawn/ai.md +76 -0
  143. package/prefabs/health-respawn/composite.json +31 -0
  144. package/prefabs/health-respawn/data.json +38 -0
  145. package/prefabs/health-respawn/scripts/health-respawn.ts +98 -0
  146. package/prefabs/health-respawn/scripts/health.ts +22 -0
  147. package/prefabs/health-respawn/scripts/pure/health.ts +60 -0
  148. package/prefabs/health-respawn/thumbnail.png +0 -0
  149. package/prefabs/high-stool/HighStool.glb +0 -0
  150. package/prefabs/high-stool/composite.json +74 -0
  151. package/prefabs/high-stool/data.json +30 -0
  152. package/prefabs/high-stool/scripts/seat.ts +191 -0
  153. package/prefabs/high-stool/thumbnail.png +0 -0
  154. package/prefabs/large-couch/ThreeSeater_Couch.glb +0 -0
  155. package/prefabs/large-couch/composite.json +100 -0
  156. package/prefabs/large-couch/data.json +30 -0
  157. package/prefabs/large-couch/scripts/seat.ts +191 -0
  158. package/prefabs/large-couch/thumbnail.png +0 -0
  159. package/prefabs/leaderboard/ai.md +75 -0
  160. package/prefabs/leaderboard/board-panel.glb +0 -0
  161. package/prefabs/leaderboard/composite.json +82 -0
  162. package/prefabs/leaderboard/data.json +35 -0
  163. package/prefabs/leaderboard/scripts/leaderboard.ts +66 -0
  164. package/prefabs/leaderboard/scripts/pure/board.ts +71 -0
  165. package/prefabs/leaderboard/thumbnail.png +0 -0
  166. package/prefabs/loveseat/Couch_01.glb +0 -0
  167. package/prefabs/loveseat/composite.json +100 -0
  168. package/prefabs/loveseat/data.json +34 -0
  169. package/prefabs/loveseat/file1.png +0 -0
  170. package/prefabs/loveseat/scripts/seat.ts +191 -0
  171. package/prefabs/loveseat/thumbnail.png +0 -0
  172. package/prefabs/modern-sofa/ModernSofa.glb +0 -0
  173. package/prefabs/modern-sofa/composite.json +100 -0
  174. package/prefabs/modern-sofa/data.json +30 -0
  175. package/prefabs/modern-sofa/scripts/seat.ts +191 -0
  176. package/prefabs/modern-sofa/thumbnail.png +0 -0
  177. package/prefabs/outdoor-chair/Chair_07.glb +0 -0
  178. package/prefabs/outdoor-chair/composite.json +74 -0
  179. package/prefabs/outdoor-chair/data.json +34 -0
  180. package/prefabs/outdoor-chair/file1.png +0 -0
  181. package/prefabs/outdoor-chair/scripts/seat.ts +191 -0
  182. package/prefabs/outdoor-chair/thumbnail.png +0 -0
  183. package/prefabs/rustic-bench/Bench_01.glb +0 -0
  184. package/prefabs/rustic-bench/PiratesPack_TX.png.png +0 -0
  185. package/prefabs/rustic-bench/composite.json +100 -0
  186. package/prefabs/rustic-bench/data.json +34 -0
  187. package/prefabs/rustic-bench/scripts/seat.ts +191 -0
  188. package/prefabs/rustic-bench/thumbnail.png +0 -0
  189. package/prefabs/rustic-round-stool/Chairwood_01.glb +0 -0
  190. package/prefabs/rustic-round-stool/PiratesPack_TX.png.png +0 -0
  191. package/prefabs/rustic-round-stool/composite.json +74 -0
  192. package/prefabs/rustic-round-stool/data.json +34 -0
  193. package/prefabs/rustic-round-stool/scripts/seat.ts +191 -0
  194. package/prefabs/rustic-round-stool/thumbnail.png +0 -0
  195. package/prefabs/rustic-square-stool/Chairwood_02.glb +0 -0
  196. package/prefabs/rustic-square-stool/PiratesPack_TX.png.png +0 -0
  197. package/prefabs/rustic-square-stool/composite.json +74 -0
  198. package/prefabs/rustic-square-stool/data.json +34 -0
  199. package/prefabs/rustic-square-stool/scripts/seat.ts +191 -0
  200. package/prefabs/rustic-square-stool/thumbnail.png +0 -0
  201. package/prefabs/server-clock/ai.md +67 -0
  202. package/prefabs/server-clock/composite.json +45 -0
  203. package/prefabs/server-clock/data.json +24 -0
  204. package/prefabs/server-clock/scripts/clock-hud.tsx +55 -0
  205. package/prefabs/server-clock/scripts/server-clock.ts +71 -0
  206. package/prefabs/server-clock/thumbnail.png +0 -0
  207. package/prefabs/simple-chair/Chair_03.glb +0 -0
  208. package/prefabs/simple-chair/composite.json +74 -0
  209. package/prefabs/simple-chair/data.json +34 -0
  210. package/prefabs/simple-chair/file1.png +0 -0
  211. package/prefabs/simple-chair/scripts/seat.ts +191 -0
  212. package/prefabs/simple-chair/thumbnail.png +0 -0
  213. package/prefabs/sit-spot/composite.json +45 -0
  214. package/prefabs/sit-spot/data.json +30 -0
  215. package/prefabs/sit-spot/scripts/seat.ts +191 -0
  216. package/prefabs/sit-spot/thumbnail.png +0 -0
  217. package/prefabs/sit-spot-edge/composite.json +42 -0
  218. package/prefabs/sit-spot-edge/data.json +32 -0
  219. package/prefabs/sit-spot-edge/emotes/Sit_Edge1_emote.glb +0 -0
  220. package/prefabs/sit-spot-edge/emotes/Sit_Edge2_emote.glb +0 -0
  221. package/prefabs/sit-spot-edge/scripts/seat.ts +191 -0
  222. package/prefabs/sit-spot-edge/thumbnail.png +0 -0
  223. package/prefabs/small-couch/TwoSeater_Couch.glb +0 -0
  224. package/prefabs/small-couch/composite.json +100 -0
  225. package/prefabs/small-couch/data.json +30 -0
  226. package/prefabs/small-couch/scripts/seat.ts +191 -0
  227. package/prefabs/small-couch/thumbnail.png +0 -0
  228. package/prefabs/spawner/ai.md +109 -0
  229. package/prefabs/spawner/composite.json +73 -0
  230. package/prefabs/spawner/data.json +22 -0
  231. package/prefabs/spawner/scripts/spawner.ts +380 -0
  232. package/prefabs/spawner/thumbnail.png +0 -0
  233. package/prefabs/steampunk-bench/Steampunk Bench.glb +0 -0
  234. package/prefabs/steampunk-bench/composite.json +100 -0
  235. package/prefabs/steampunk-bench/data.json +30 -0
  236. package/prefabs/steampunk-bench/scripts/seat.ts +191 -0
  237. package/prefabs/steampunk-bench/thumbnail.png +0 -0
  238. package/prefabs/steampunk-chair/Steampunk Chair.glb +0 -0
  239. package/prefabs/steampunk-chair/composite.json +74 -0
  240. package/prefabs/steampunk-chair/data.json +30 -0
  241. package/prefabs/steampunk-chair/scripts/seat.ts +191 -0
  242. package/prefabs/steampunk-chair/thumbnail.png +0 -0
  243. package/prefabs/tall-stool/Stool_D.glb +0 -0
  244. package/prefabs/tall-stool/composite.json +74 -0
  245. package/prefabs/tall-stool/data.json +30 -0
  246. package/prefabs/tall-stool/scripts/seat.ts +191 -0
  247. package/prefabs/tall-stool/thumbnail.png +0 -0
  248. package/prefabs/trigger-zone/ai.md +115 -0
  249. package/prefabs/trigger-zone/composite.json +52 -0
  250. package/prefabs/trigger-zone/data.json +31 -0
  251. package/prefabs/trigger-zone/scripts/trigger-zone.ts +89 -0
  252. package/prefabs/trigger-zone/thumbnail.png +0 -0
  253. package/prefabs/video-screen/composite.json +98 -0
  254. package/prefabs/video-screen/data.json +21 -0
  255. package/prefabs/video-screen/thumbnail.png +0 -0
  256. package/prefabs/video-screen/video_player.glb +0 -0
  257. package/prefabs/wooden-chair/Chair 1.glb +0 -0
  258. package/prefabs/wooden-chair/composite.json +74 -0
  259. package/prefabs/wooden-chair/data.json +30 -0
  260. package/prefabs/wooden-chair/scripts/seat.ts +191 -0
  261. package/prefabs/wooden-chair/thumbnail.png +0 -0
  262. package/prefabs/wooden-chair-armrests/Chair 3.glb +0 -0
  263. package/prefabs/wooden-chair-armrests/composite.json +74 -0
  264. package/prefabs/wooden-chair-armrests/data.json +30 -0
  265. package/prefabs/wooden-chair-armrests/scripts/seat.ts +191 -0
  266. package/prefabs/wooden-chair-armrests/thumbnail.png +0 -0
  267. package/scene/about +17 -0
  268. package/scene/bafkreias2w4xuqaic6sdcvblrisayi6xg7drfowzpt5ubjkmrk3mx6czc4 +1 -0
  269. package/scene/bafkreiby2gmthtauf7cckttphmhpecsueyypi2hzlke4lcodg7fm4cm2sq +23 -0
  270. package/scene/bafkreidhwqwajqk4nsuffef5fkdrgmp5ql6vnn2knpqsuqalyosv5o6lk4 +1 -0
  271. package/scene/bafkreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku +0 -0
  272. package/scene/bafybeihw3xlbff4sf5sc3kvmlmlwf2visnty5lfymawlunzt5vrkp5fd7u +90 -0
  273. package/web-build/esbuild.wasm +0 -0
  274. package/web-build/index.js +3 -0
  275. package/web-build/sdk-snapshot.json +1 -0
  276. package/web-build/worker.js +701 -0
  277. 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
+ }
@@ -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
+ }