@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,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
|
|
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
|
+
}
|