@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,185 @@
1
+ // Game Flow's phase arithmetic, with no SDK and no I/O.
2
+ //
3
+ // The loop is lobby → round → intermission → round → …, and every phase is a
4
+ // deadline rather than a timer: the server writes when a phase ENDS, and each
5
+ // client derives its countdown from that one number against the shared clock.
6
+ // A player who joins mid-round therefore lands on the same countdown as
7
+ // everyone else without a single extra message.
8
+ //
9
+ // The lobby parks (endsAtMs 0) while the scene is short of players, so a round
10
+ // never burns down in an empty world and the next arrival gets a full lobby.
11
+
12
+ export type FlowPhase = 'lobby' | 'round' | 'intermission'
13
+
14
+ /** What the flow wants the caller to do at this tick. */
15
+ export type FlowAction = 'none' | 'startRound' | 'endRound'
16
+
17
+ export interface FlowConfig {
18
+ roundMs: number
19
+ countdownMs: number
20
+ intermissionMs: number
21
+ minPlayers: number
22
+ }
23
+
24
+ /** The machine's own state. `round` counts rounds PLAYED, from 1. */
25
+ export interface FlowState {
26
+ phase: FlowPhase
27
+ endsAtMs: number
28
+ round: number
29
+ }
30
+
31
+ /**
32
+ * What the server publishes. The head count only exists there — a client has no
33
+ * roster — so it rides the fact rather than being counted twice.
34
+ */
35
+ export interface FlowFact extends FlowState {
36
+ present: number
37
+ }
38
+
39
+ export const MIN_SECONDS = 1
40
+ export const MAX_SECONDS = 3600
41
+
42
+ /** A creator can type anything into a number field; a 0-second round would spin. */
43
+ export function clampSeconds(value: number): number {
44
+ if (!Number.isFinite(value)) return MIN_SECONDS
45
+ return Math.min(MAX_SECONDS, Math.max(MIN_SECONDS, Math.round(value)))
46
+ }
47
+
48
+ export function configFromSeconds(
49
+ round: number,
50
+ countdown: number,
51
+ intermission: number,
52
+ minPlayers: number
53
+ ): FlowConfig {
54
+ return {
55
+ roundMs: clampSeconds(round) * 1000,
56
+ countdownMs: clampSeconds(countdown) * 1000,
57
+ intermissionMs: clampSeconds(intermission) * 1000,
58
+ minPlayers: Number.isFinite(minPlayers) ? Math.max(1, Math.round(minPlayers)) : 1
59
+ }
60
+ }
61
+
62
+ export function lobbyState(): FlowState {
63
+ return { phase: 'lobby', endsAtMs: 0, round: 0 }
64
+ }
65
+
66
+ /** Defensive read: the fact crossed the wire and a script can clobber the key. */
67
+ export function asFlowFact(value: unknown): FlowFact | null {
68
+ if (typeof value !== 'object' || value === null) return null
69
+ const record = value as Record<string, unknown>
70
+ const phase = record.phase
71
+ if (phase !== 'lobby' && phase !== 'round' && phase !== 'intermission') return null
72
+ const num = (key: string): number => {
73
+ const raw = record[key]
74
+ return typeof raw === 'number' && Number.isFinite(raw) ? Math.max(0, Math.floor(raw)) : 0
75
+ }
76
+ return { phase, endsAtMs: num('endsAtMs'), round: num('round'), present: num('present') }
77
+ }
78
+
79
+ /**
80
+ * One tick of the machine. Pure: the caller owns the clock, the head count, and
81
+ * the two side effects (`startRound` calls game.newRound, `endRound` closes the
82
+ * round out). Rounds never end here in script mode by accident — the caller
83
+ * decides whether an expired round deadline is the end or only a ceiling.
84
+ */
85
+ export function advanceFlow(
86
+ state: FlowState,
87
+ config: FlowConfig,
88
+ present: number,
89
+ nowMs: number
90
+ ): { state: FlowState; action: FlowAction } {
91
+ if (state.phase === 'round') {
92
+ if (state.endsAtMs > 0 && nowMs >= state.endsAtMs) return { state, action: 'endRound' }
93
+ return { state, action: 'none' }
94
+ }
95
+ const ready = present >= config.minPlayers
96
+ if (state.phase === 'intermission') {
97
+ if (nowMs < state.endsAtMs) return { state, action: 'none' }
98
+ if (!ready) return { state: { phase: 'lobby', endsAtMs: 0, round: state.round }, action: 'none' }
99
+ return { state, action: 'startRound' }
100
+ }
101
+ if (!ready) {
102
+ if (state.endsAtMs === 0) return { state, action: 'none' }
103
+ return { state: { ...state, endsAtMs: 0 }, action: 'none' }
104
+ }
105
+ if (state.endsAtMs === 0) {
106
+ return { state: { ...state, endsAtMs: nowMs + config.countdownMs }, action: 'none' }
107
+ }
108
+ if (nowMs >= state.endsAtMs) return { state, action: 'startRound' }
109
+ return { state, action: 'none' }
110
+ }
111
+
112
+ // A round always carries a deadline, even where a script owns the end: it is the
113
+ // ceiling that keeps a forgotten game.newRound() from wedging the loop forever.
114
+ // Only the panel differs — in script mode it hides a clock it does not own.
115
+ export function roundState(state: FlowState, config: FlowConfig, nowMs: number): FlowState {
116
+ return { phase: 'round', endsAtMs: nowMs + config.roundMs, round: state.round + 1 }
117
+ }
118
+
119
+ export function intermissionState(state: FlowState, config: FlowConfig, nowMs: number): FlowState {
120
+ return { phase: 'intermission', endsAtMs: nowMs + config.intermissionMs, round: state.round }
121
+ }
122
+
123
+ /** Whole seconds left, the way a countdown reads them. */
124
+ export function secondsLeft(state: { endsAtMs: number }, nowMs: number): number {
125
+ if (state.endsAtMs === 0) return 0
126
+ return Math.max(0, Math.ceil((state.endsAtMs - nowMs) / 1000))
127
+ }
128
+
129
+ export function formatCountdown(seconds: number): string {
130
+ const total = Math.max(0, Math.floor(seconds))
131
+ const rest = total % 60
132
+ return `${Math.floor(total / 60)}:${rest < 10 ? '0' : ''}${rest}`
133
+ }
134
+
135
+ /** One row of whatever a board key holds — the shapes real games write vary. */
136
+ export interface Standing {
137
+ player: string
138
+ score: number
139
+ }
140
+
141
+ const PLAYER_FIELDS = ['player', 'p', 'address', 'wallet', 'name']
142
+ const SCORE_FIELDS = ['score', 'points', 'pts', 'value', 'seconds', 'time', 'best']
143
+
144
+ /**
145
+ * Read a board out of a `game.state` key. Creators write these rows themselves,
146
+ * so the field names differ game to game — anything with a player-ish and a
147
+ * number-ish field is a standing, and anything else is skipped rather than
148
+ * rendered as `[object Object]`.
149
+ */
150
+ export function standingsOf(value: unknown): Standing[] {
151
+ if (!Array.isArray(value)) return []
152
+ const out: Standing[] = []
153
+ for (const item of value) {
154
+ if (typeof item !== 'object' || item === null) continue
155
+ const record = item as Record<string, unknown>
156
+ const playerKey = PLAYER_FIELDS.find((key) => typeof record[key] === 'string' && record[key] !== '')
157
+ const scoreKey = SCORE_FIELDS.find((key) => typeof record[key] === 'number' && Number.isFinite(record[key]))
158
+ if (playerKey === undefined || scoreKey === undefined) continue
159
+ out.push({ player: String(record[playerKey]), score: Number(record[scoreKey]) })
160
+ }
161
+ return out
162
+ }
163
+
164
+ /** Wallets are long; a podium line reads better with the tail. */
165
+ export function shortName(player: string): string {
166
+ return player.startsWith('0x') && player.length > 10 ? `${player.slice(0, 6)}…${player.slice(-4)}` : player
167
+ }
168
+
169
+ /** The winners line broadcast to every player at the end of a round. */
170
+ export function podiumLine(value: unknown, places: number): string {
171
+ const top = standingsOf(value).slice(0, Math.max(1, places))
172
+ if (top.length === 0) return 'Round over — nobody scored.'
173
+ return `Round over — ${top.map((row, i) => `${i + 1}. ${shortName(row.player)}`).join(' ')}`
174
+ }
175
+
176
+ export function panelText(fact: FlowFact | null, nowMs: number, showClock: boolean, needed: number): string {
177
+ if (fact === null) return 'GAME FLOW\n--:--\nwaiting for the Multiplayer Server'
178
+ if (fact.phase === 'lobby') {
179
+ if (fact.endsAtMs === 0) return `LOBBY\n--:--\n${fact.present}/${needed} players`
180
+ return `LOBBY\n${formatCountdown(secondsLeft(fact, nowMs))}\nstarting`
181
+ }
182
+ if (fact.phase === 'intermission') return `ROUND ${fact.round} OVER\n${formatCountdown(secondsLeft(fact, nowMs))}`
183
+ if (!showClock) return `ROUND ${fact.round}`
184
+ return `ROUND ${fact.round}\n${formatCountdown(secondsLeft(fact, nowMs))}`
185
+ }
Binary file
@@ -0,0 +1,76 @@
1
+ ---
2
+ prefab: health-respawn
3
+ claims-state: game.state.health
4
+ claims-messages: respawn
5
+ ---
6
+
7
+ # Health & Respawn — AI guide
8
+
9
+ Hit points the server owns, and a respawn point players come back to.
10
+
11
+ The project copy is normally custom/health_respawn/ — check what is on disk, a
12
+ second copy is custom/health_respawn_2/.
13
+
14
+ ## When to use
15
+
16
+ Any scene where players can die: a fall, a hazard, a fight. Place ONE anywhere;
17
+ it is invisible. A second copy is a no-op (the first one's params win).
18
+
19
+ Do NOT write your own health map, death check or teleport-home code — this prefab
20
+ owns all three, and health a client can edit is health nobody can trust.
21
+
22
+ ## API
23
+
24
+ import { damage, healthOf } from '../../custom/health_respawn/scripts/health'
25
+
26
+ - `damage(player, amount)` — WHICH SIDE: the SERVER, inside the `if (isServer())`
27
+ branch, from a `game.onRequest`, `game.onEnterArea`, `game.every` or
28
+ `game.onRoundStart` handler. Takes the player to a minimum of 0; at 0 this
29
+ prefab respawns them within half a second and refills them. A player who is not
30
+ in the scene is ignored.
31
+ - `healthOf(player)` — reads hit points on either side, the client included.
32
+ - `game.state.health` — the whole `{ wallet: points }` map, readable on every
33
+ client and by late joiners. Read it to draw bars or count who is alive; never
34
+ write it.
35
+
36
+ Params of the prefab's script — set them in the placePrefab request:
37
+
38
+ - `respawnAt`: the entity players come back to. Pick a pad or marker that sits at
39
+ the scene root — the position is read straight off its Transform. Until it is
40
+ picked, nothing respawns and the console says so.
41
+ - `maxHealth`: hit points a player starts every life with (default 100). Also the
42
+ refill at a new round.
43
+ - `dieBelowHeight`: players below this height die (default 0 = off). This is the
44
+ death plane for a climbing or platforming scene; set it under the lowest floor
45
+ a player is allowed to stand on.
46
+
47
+ ## Hazards
48
+
49
+ A hurting area is a Trigger Area plus one handler in the server branch — this
50
+ prefab deliberately has no area list, because damage belongs to it and detection
51
+ belongs to the area. `game.onEnterArea` is a server verb:
52
+
53
+ start(): void {
54
+ if (isServer()) {
55
+ game.onEnterArea('Moat', (player) => damage(player, 100))
56
+ return
57
+ }
58
+ }
59
+
60
+ ## Do / Don't
61
+
62
+ - DON'T call `damage` on the client. Only the server changes synced state, so the
63
+ call does nothing there.
64
+ - DON'T teleport a dead player yourself. The server broadcasts `respawn` to that
65
+ one player and their own client moves them; a `movePlayerTo` in your code moves
66
+ the wrong avatar or none.
67
+ - DON'T handle `respawn` yourself with `game.onBroadcast('respawn', …)`. This
68
+ prefab already listens on it, on the client side.
69
+ - DO reset scores in `game.onRoundStart` if you also want a fresh scoreboard —
70
+ this prefab only refills health.
71
+
72
+ ## Example
73
+
74
+ "Kill anyone who falls off the tower and send them back to the base": place Health
75
+ & Respawn, pick the BaseSpawn pad as `respawnAt`, and set `dieBelowHeight` to the
76
+ height just under the tower's first floor. No script at all.
@@ -0,0 +1,31 @@
1
+ {
2
+ "version": 1,
3
+ "components": [
4
+ {
5
+ "name": "core-schema::Name",
6
+ "data": {
7
+ "0": {
8
+ "json": {
9
+ "value": "Health & Respawn"
10
+ }
11
+ }
12
+ }
13
+ },
14
+ {
15
+ "name": "asset-packs::Script",
16
+ "data": {
17
+ "0": {
18
+ "json": {
19
+ "value": [
20
+ {
21
+ "path": "{assetPath}/scripts/health-respawn.ts",
22
+ "priority": 0,
23
+ "layout": "{\"params\":{},\"actions\":[]}"
24
+ }
25
+ ]
26
+ }
27
+ }
28
+ }
29
+ }
30
+ ]
31
+ }
@@ -0,0 +1,38 @@
1
+ {
2
+ "id": "5cc3b4d7-2a87-4825-9a53-e2b007bcff24",
3
+ "name": "Health & Respawn",
4
+ "description": "Players can take damage and respawn. Works with: Game Flow, Trigger Area.",
5
+ "category": "custom",
6
+ "group": "Multiplayer Server",
7
+ "tags": [
8
+ "player",
9
+ "health",
10
+ "damage",
11
+ "respawn",
12
+ "multiplayer",
13
+ "server"
14
+ ],
15
+ "requiresSdk": "auth-server",
16
+ "requiredPermissions": [
17
+ "ALLOW_TO_MOVE_PLAYER_INSIDE_SCENE"
18
+ ],
19
+ "drivenBy": {
20
+ "rule": "Nothing takes hit points by itself — the server calls damage() wherever the harm happens, and this item does the dying and the respawn.",
21
+ "code": "game.onEnterArea('Spikes', (player) => damage(player, 25))",
22
+ "next": "Press New script below, write that inside the isServer() block it comes with, and import damage from the health.ts beside this item's own script."
23
+ },
24
+ "version": "1.1.0",
25
+ "changelog": [
26
+ {
27
+ "version": "1.0.0",
28
+ "notes": "Initial release: hit points the server owns in game.state.health, a respawn point picked in the inspector, a \"die below height\" death plane, and a damage() other server code calls."
29
+ },
30
+ {
31
+ "version": "1.1.0",
32
+ "notes": "The placed item's Script card now shows the damage() call that takes hit points, under its respawn settings."
33
+ }
34
+ ],
35
+ "origin": {
36
+ "source": "builtin"
37
+ }
38
+ }
@@ -0,0 +1,98 @@
1
+ // Players can take damage and respawn. The server owns everyone's hit points and
2
+ // decides who died; a client only moves its own player when the server says so,
3
+ // because moving an avatar is the one thing only that player's client can do.
4
+ //
5
+ // Hit points live in `game.state.health` — one wallet → points map every player
6
+ // can read, which is what makes a health bar or a "who is still alive" check a
7
+ // plain read instead of a message. Other server code hurts a player through
8
+ // damage() below; the sweep here notices the zero and respawns them.
9
+ import { Transform, type Entity } from '@dcl/sdk/ecs'
10
+ import { isServer } from '@dcl/sdk/network'
11
+ import { movePlayerTo } from '~system/RestrictedActions'
12
+ import { game, type Player } from '~runtime/game'
13
+ import { asHealthMap, clampMax, deadPlayers } from './pure/health'
14
+ import { HEALTH_KEY } from './health'
15
+
16
+ const RESPAWN = 'respawn'
17
+ const SWEEP_S = 0.5
18
+ const NO_POINT = "Players won't respawn anywhere until you pick a respawn point."
19
+
20
+ // One Health & Respawn owns the roster. A second copy would revive everyone the
21
+ // first one just killed, half a second apart.
22
+ let claimed = false
23
+
24
+ export class HealthRespawn {
25
+ private primary = false
26
+
27
+ constructor(
28
+ public src: string,
29
+ public entity: Entity,
30
+ /** Where players come back. Pick the pad or marker they should land on. */
31
+ public respawnAt: Entity = 0 as Entity,
32
+ /** Hit points a player starts every life with. */
33
+ public maxHealth: number = 100,
34
+ /** Players who fall below this height die. Leave at 0 to switch it off. */
35
+ public dieBelowHeight: number = 0
36
+ ) {}
37
+
38
+ start(): void {
39
+ // Claimed before the branch: each side keeps its own `claimed`, and the copy
40
+ // that answers a respawn must be the same one whose params drive the sweep.
41
+ this.primary = !claimed
42
+ claimed = true
43
+ if (!this.primary) {
44
+ console.log('[healthRespawn] a second Health & Respawn is placed — only the first tracks health')
45
+ return
46
+ }
47
+ if (this.respawnAt === (0 as Entity)) console.log(`[healthRespawn] ${NO_POINT}`)
48
+ if (isServer()) {
49
+ game.onReady(() => game.setState({ [HEALTH_KEY]: {} }))
50
+ game.onPlayerJoin((player) => this.revive(player))
51
+ game.onPlayerLeave((player) => {
52
+ const map = { ...asHealthMap(game.state[HEALTH_KEY]) }
53
+ delete map[player]
54
+ game.setState({ [HEALTH_KEY]: map })
55
+ })
56
+ game.onRoundStart(() => {
57
+ const map = asHealthMap(game.state[HEALTH_KEY])
58
+ const full: Record<string, number> = {}
59
+ for (const player of Object.keys(map)) full[player] = clampMax(this.maxHealth)
60
+ game.setState({ [HEALTH_KEY]: full })
61
+ })
62
+ game.every(SWEEP_S, () => this.sweep())
63
+ return
64
+ }
65
+ game.onBroadcast(RESPAWN, () => this.goHome())
66
+ }
67
+
68
+ private sweep(): void {
69
+ const map = asHealthMap(game.state[HEALTH_KEY])
70
+ const dead = deadPlayers(map, (player) => game.positionOf(player), this.deathPlane)
71
+ if (dead.length === 0) return
72
+ const next = { ...map }
73
+ for (const player of dead) {
74
+ next[player] = clampMax(this.maxHealth)
75
+ game.broadcast(RESPAWN, {}, player)
76
+ }
77
+ game.setState({ [HEALTH_KEY]: next })
78
+ }
79
+
80
+ private get deathPlane(): number {
81
+ return Number.isFinite(this.dieBelowHeight) ? this.dieBelowHeight : 0
82
+ }
83
+
84
+ private revive(player: Player): void {
85
+ game.setState({
86
+ [HEALTH_KEY]: { ...asHealthMap(game.state[HEALTH_KEY]), [player]: clampMax(this.maxHealth) }
87
+ })
88
+ }
89
+
90
+ private goHome(): void {
91
+ const point = this.respawnAt === (0 as Entity) ? null : Transform.getOrNull(this.respawnAt)
92
+ if (point === null) {
93
+ console.log(`[healthRespawn] ${NO_POINT}`)
94
+ return
95
+ }
96
+ void movePlayerTo({ newRelativePosition: { ...point.position } })
97
+ }
98
+ }
@@ -0,0 +1,22 @@
1
+ // What other scripts call to read and change a player's hit points. It lives
2
+ // beside health-respawn.ts rather than in it because a prefab's script file
3
+ // exports exactly one class — the runtime constructs the first function-valued
4
+ // export, so a helper sitting next to the class could be built instead of it.
5
+ import { game, type Player } from '~runtime/game'
6
+ import { afterDamage, asHealthMap } from './pure/health'
7
+
8
+ export const HEALTH_KEY = 'health'
9
+
10
+ /** Hit points a player has right now. 0 means they are on their way back. */
11
+ export function healthOf(player: Player): number {
12
+ return asHealthMap(game.state[HEALTH_KEY])[player] ?? 0
13
+ }
14
+
15
+ /**
16
+ * Hurt a player. Call it from server code — a client cannot change anyone's
17
+ * health. At zero the placed Health & Respawn sends them back to its respawn
18
+ * point on their next sweep.
19
+ */
20
+ export function damage(player: Player, amount: number): void {
21
+ game.setState({ [HEALTH_KEY]: afterDamage(asHealthMap(game.state[HEALTH_KEY]), player, amount) })
22
+ }
@@ -0,0 +1,60 @@
1
+ // Health & Respawn's bookkeeping, with no SDK and no I/O.
2
+ //
3
+ // Health is one shared fact — a wallet → hit points map under `game.state.health`
4
+ // — so every player can draw a bar for anyone without asking, and a late joiner
5
+ // reads the whole roster from the snapshot. The map is the ONLY place hit points
6
+ // live: the server's sweep reacts to a zero, whoever wrote it.
7
+
8
+ export type HealthMap = Record<string, number>
9
+
10
+ export const MIN_HEALTH = 1
11
+ export const MAX_HEALTH = 100000
12
+
13
+ export function clampMax(value: number): number {
14
+ if (!Number.isFinite(value)) return 100
15
+ return Math.min(MAX_HEALTH, Math.max(MIN_HEALTH, Math.round(value)))
16
+ }
17
+
18
+ /** Defensive read: the map crossed the wire and a script can clobber the key. */
19
+ export function asHealthMap(value: unknown): HealthMap {
20
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return {}
21
+ const out: HealthMap = {}
22
+ for (const [player, points] of Object.entries(value as Record<string, unknown>)) {
23
+ if (typeof points === 'number' && Number.isFinite(points)) out[player] = Math.max(0, Math.floor(points))
24
+ }
25
+ return out
26
+ }
27
+
28
+ /**
29
+ * The map after `amount` damage. A player with no entry is not on the roster —
30
+ * hurting them would invent a roster row that never gets cleaned up.
31
+ */
32
+ export function afterDamage(map: HealthMap, player: string, amount: number): HealthMap {
33
+ const before = map[player]
34
+ if (before === undefined) return map
35
+ const hurt = Number.isFinite(amount) ? Math.max(0, Math.round(amount)) : 0
36
+ return { ...map, [player]: Math.max(0, before - hurt) }
37
+ }
38
+
39
+ /**
40
+ * The players to respawn right now: at or below zero, or fallen
41
+ * past the death plane. `dieBelowY` of 0 switches the plane off — a scene whose
42
+ * ground sits at y 0 would otherwise kill everyone standing on it.
43
+ */
44
+ export function deadPlayers(
45
+ map: HealthMap,
46
+ feetOf: (player: string) => { y: number } | null,
47
+ dieBelowY: number
48
+ ): string[] {
49
+ const out: string[] = []
50
+ for (const [player, points] of Object.entries(map)) {
51
+ if (points <= 0) {
52
+ out.push(player)
53
+ continue
54
+ }
55
+ if (dieBelowY === 0) continue
56
+ const feet = feetOf(player)
57
+ if (feet !== null && feet.y < dieBelowY) out.push(player)
58
+ }
59
+ return out
60
+ }
Binary file
@@ -0,0 +1,74 @@
1
+ {
2
+ "version": 1,
3
+ "components": [
4
+ {
5
+ "name": "core::GltfContainer",
6
+ "data": {
7
+ "0": {
8
+ "json": {
9
+ "src": "{assetPath}/HighStool.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": "High Stool"
22
+ }
23
+ },
24
+ "513": {
25
+ "json": {
26
+ "value": "Sit Spot"
27
+ }
28
+ }
29
+ }
30
+ },
31
+ {
32
+ "name": "core::Transform",
33
+ "data": {
34
+ "513": {
35
+ "json": {
36
+ "position": {
37
+ "x": 0.0194,
38
+ "y": 0.4707,
39
+ "z": 0.5924
40
+ },
41
+ "rotation": {
42
+ "x": 0,
43
+ "y": 0,
44
+ "z": 0,
45
+ "w": 1
46
+ },
47
+ "scale": {
48
+ "x": 1,
49
+ "y": 1,
50
+ "z": 1
51
+ },
52
+ "parent": 0
53
+ }
54
+ }
55
+ }
56
+ },
57
+ {
58
+ "name": "asset-packs::Script",
59
+ "data": {
60
+ "0": {
61
+ "json": {
62
+ "value": [
63
+ {
64
+ "path": "{assetPath}/scripts/seat.ts",
65
+ "priority": 0,
66
+ "layout": "{\"params\":{},\"actions\":[]}"
67
+ }
68
+ ]
69
+ }
70
+ }
71
+ }
72
+ }
73
+ ]
74
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "id": "f8ee0d2a-19f5-45a9-9029-cd46d7a8f834",
3
+ "name": "High Stool",
4
+ "description": "A tall bar stool. Players can sit on 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
+ }