@netforge/template-party 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/LICENSE +21 -0
- package/NOTICE +32 -0
- package/README.md +50 -2
- package/client/debris.ts +267 -0
- package/client/decor.ts +13 -0
- package/client/host.ts +96 -0
- package/client/hud.ts +576 -0
- package/client/icons.ts +42 -0
- package/client/index.html +277 -0
- package/client/input.ts +172 -0
- package/client/library.ts +250 -0
- package/client/main.ts +607 -0
- package/client/names.ts +47 -0
- package/client/palette.ts +7 -0
- package/client/scene.ts +941 -0
- package/client/stage.ts +266 -0
- package/client/stake.ts +101 -0
- package/client/threats.ts +157 -0
- package/llms.txt +375 -0
- package/package.json +57 -3
- package/src/game.ts +326 -0
- package/src/manifest.ts +38 -0
- package/src/store.ts +107 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, user-scalable=no" />
|
|
6
|
+
<title>Romphouse</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
color-scheme: dark;
|
|
10
|
+
--ink: #f4ead8;
|
|
11
|
+
--dim: #a79f91;
|
|
12
|
+
--ember: #ff7ab8;
|
|
13
|
+
--panel: rgba(12, 14, 20, 0.72);
|
|
14
|
+
--line: rgba(255, 200, 140, 0.18);
|
|
15
|
+
--bad: #ff5c5c;
|
|
16
|
+
--xp: #7ad7ff;
|
|
17
|
+
font-family: ui-rounded, "SF Pro Rounded", system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
18
|
+
}
|
|
19
|
+
html, body { margin: 0; height: 100%; background: #0c1018; color: var(--ink); overflow: hidden; touch-action: none; -webkit-user-select: none; user-select: none; }
|
|
20
|
+
#game { position: fixed; inset: 0; }
|
|
21
|
+
#game canvas { display: block; width: 100%; height: 100%; }
|
|
22
|
+
.hud { position: fixed; z-index: 5; pointer-events: none; font-variant-numeric: tabular-nums; }
|
|
23
|
+
.hud button, .hud .card { pointer-events: auto; }
|
|
24
|
+
button { font: inherit; color: var(--ink); background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 8px 12px; cursor: pointer; }
|
|
25
|
+
button:disabled { opacity: 0.45; cursor: default; }
|
|
26
|
+
button:active { transform: scale(0.97); }
|
|
27
|
+
|
|
28
|
+
#top-left { top: max(12px, env(safe-area-inset-top)); left: 12px; width: min(300px, 34vw); }
|
|
29
|
+
.bar { position: relative; height: 10px; border-radius: 6px; background: rgba(255, 255, 255, 0.08); overflow: hidden; margin-top: 6px; }
|
|
30
|
+
.bar > div { position: absolute; inset: 0; transform-origin: left; transition: transform 120ms linear; }
|
|
31
|
+
#xp-fill { background: var(--xp); }
|
|
32
|
+
#hp-fill { background: linear-gradient(90deg, #ff6b4a, #ffb347); }
|
|
33
|
+
#level { font-weight: 800; letter-spacing: 0.06em; font-size: 15px; }
|
|
34
|
+
#hp-text { position: absolute; right: 6px; top: -1px; font-size: 9px; color: #fff; text-shadow: 0 1px 2px #000; }
|
|
35
|
+
#team { list-style: none; padding: 0; margin: 10px 0 0; font-size: 12px; color: var(--dim); }
|
|
36
|
+
#team li { display: flex; align-items: center; gap: 6px; padding: 2px 0; }
|
|
37
|
+
#team .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--c); }
|
|
38
|
+
#team em { font-style: normal; color: var(--ink); margin-left: auto; }
|
|
39
|
+
#team small { min-width: 44px; text-align: right; }
|
|
40
|
+
|
|
41
|
+
#top-center { top: max(10px, env(safe-area-inset-top)); left: 50%; transform: translateX(-50%); text-align: center; }
|
|
42
|
+
#timer { font-size: 28px; font-weight: 800; letter-spacing: 0.04em; text-shadow: 0 2px 8px #000; }
|
|
43
|
+
#wave { font-size: 11px; letter-spacing: 0.2em; color: var(--ember); }
|
|
44
|
+
#boss { display: none; width: min(420px, 70vw); margin-top: 6px; }
|
|
45
|
+
#boss.show { display: block; }
|
|
46
|
+
#boss .boss-row + .boss-row { margin-top: 6px; }
|
|
47
|
+
#boss .boss-name { font-size: 11px; letter-spacing: 0.18em; color: #ffd9a0; }
|
|
48
|
+
#boss .bar { height: 8px; }
|
|
49
|
+
#boss .boss-fill { background: linear-gradient(90deg, #b08d57, #ffe1a6); }
|
|
50
|
+
|
|
51
|
+
#top-right { top: max(10px, env(safe-area-inset-top)); right: 12px; display: flex; flex-direction: column; align-items: flex-end; gap: 6px; }
|
|
52
|
+
#top-right .row { display: flex; gap: 6px; }
|
|
53
|
+
#kills-box { font-size: 12px; color: var(--dim); }
|
|
54
|
+
#kills { color: var(--ink); font-weight: 700; font-size: 16px; }
|
|
55
|
+
|
|
56
|
+
#cards { left: 50%; bottom: max(24px, env(safe-area-inset-bottom)); transform: translate(-50%, 140%); display: flex; gap: 10px; opacity: 0; transition: transform 260ms cubic-bezier(0.2, 1.4, 0.4, 1), opacity 160ms; }
|
|
57
|
+
#cards.show { transform: translate(-50%, 0); opacity: 1; }
|
|
58
|
+
.card { width: min(170px, 28vw); padding: 12px 12px 10px; border-radius: 14px; background: rgba(20, 16, 10, 0.88); border: 1px solid rgba(255, 179, 71, 0.45); text-align: left; display: grid; gap: 3px; box-shadow: 0 8px 28px rgba(0, 0, 0, 0.5); }
|
|
59
|
+
.card:hover { border-color: var(--ember); transform: translateY(-3px); }
|
|
60
|
+
.card kbd { font: 700 10px ui-monospace, monospace; color: var(--ember); }
|
|
61
|
+
.card b { font-size: 15px; }
|
|
62
|
+
.card span { font-size: 12px; color: var(--dim); }
|
|
63
|
+
.card i { font-style: normal; font-size: 10px; letter-spacing: 0.14em; color: var(--xp); text-transform: uppercase; }
|
|
64
|
+
.card em { font-style: normal; font-size: 11px; color: #ffd9a0; }
|
|
65
|
+
.card.evolve { border-color: #ffd36b; box-shadow: 0 0 22px rgba(255, 211, 107, 0.45); }
|
|
66
|
+
.card-actions { display: grid; gap: 6px; align-content: end; }
|
|
67
|
+
.card-actions button { font-size: 11px; padding: 6px 10px; border-radius: 10px; background: rgba(20, 16, 10, 0.8); border: 1px solid rgba(255, 179, 71, 0.3); color: var(--dim); }
|
|
68
|
+
.card-actions button:disabled { opacity: 0.4; }
|
|
69
|
+
#pending { position: absolute; right: -8px; top: -10px; font-size: 12px; color: var(--ember); }
|
|
70
|
+
|
|
71
|
+
#downed { inset: 0; display: none; place-items: end center; padding-bottom: 22vh; background: radial-gradient(circle, transparent 45%, rgba(80, 0, 0, 0.35)); }
|
|
72
|
+
#downed.show { display: grid; }
|
|
73
|
+
#downed .box { text-align: center; }
|
|
74
|
+
#downed-title { font-size: 34px; font-weight: 900; letter-spacing: 0.12em; color: var(--bad); }
|
|
75
|
+
#downed-sub { color: var(--dim); margin: 4px 0 14px; }
|
|
76
|
+
#revive-btn, #getup-btn { pointer-events: auto; border-color: var(--ember); }
|
|
77
|
+
#getup-btn { background: rgba(255, 179, 71, 0.18); }
|
|
78
|
+
#getup-btn kbd { font: inherit; font-size: 11px; opacity: 0.7; margin-left: 6px; }
|
|
79
|
+
.downed-actions { display: flex; gap: 8px; justify-content: center; }
|
|
80
|
+
|
|
81
|
+
#end { inset: 0; display: none; place-items: center; background: rgba(6, 8, 12, 0.72); }
|
|
82
|
+
#end.show { display: grid; }
|
|
83
|
+
#end .box { width: min(600px, 92vw); max-height: 88vh; overflow-y: auto; box-sizing: border-box; padding: 22px; border-radius: 18px; background: rgba(18, 16, 14, 0.94); border: 1px solid var(--line); text-align: center; pointer-events: auto; }
|
|
84
|
+
#end-players { display: grid; gap: 10px; margin: 0 0 14px; text-align: left; }
|
|
85
|
+
.end-player { padding: 10px 12px; border-radius: 12px; border: 1px solid var(--line); background: rgba(255, 255, 255, 0.03); }
|
|
86
|
+
.end-player header { display: flex; align-items: baseline; gap: 8px; font-weight: 800; }
|
|
87
|
+
.end-player header .dot { width: 9px; height: 9px; border-radius: 50%; background: var(--c); align-self: center; }
|
|
88
|
+
.end-player header small { color: var(--dim); font-weight: 400; }
|
|
89
|
+
.end-player .titles { margin-left: auto; color: #ffd36b; font-size: 12px; letter-spacing: 0.06em; }
|
|
90
|
+
.end-player ul { list-style: none; padding: 0; margin: 6px 0 0; font-size: 12px; }
|
|
91
|
+
.end-player li { display: grid; grid-template-columns: 1fr auto 64px; gap: 8px; align-items: center; padding: 2px 0; }
|
|
92
|
+
.end-player li .dmgbar { height: 5px; border-radius: 3px; background: var(--ember); transform-origin: left; }
|
|
93
|
+
.end-player .line { font-size: 12px; color: var(--dim); margin-top: 6px; }
|
|
94
|
+
#end-actions { display: flex; gap: 10px; justify-content: center; align-items: center; margin-top: 12px; }
|
|
95
|
+
#end-again { border-color: var(--ember); font-weight: 800; padding: 10px 18px; }
|
|
96
|
+
#end-again.voted { background: rgba(255, 179, 71, 0.22); }
|
|
97
|
+
|
|
98
|
+
#keepers { left: 50%; bottom: max(24px, env(safe-area-inset-bottom)); transform: translateX(-50%); display: none; gap: 8px; }
|
|
99
|
+
#keepers.show { display: flex; }
|
|
100
|
+
#end #keepers { position: static; transform: none; justify-content: center; margin-top: 14px; }
|
|
101
|
+
.keeper { width: 128px; padding: 8px 10px; border-radius: 12px; text-align: left; display: grid; gap: 2px; border: 1px solid var(--line); }
|
|
102
|
+
.keeper b { font-size: 13px; }
|
|
103
|
+
.keeper small { font-size: 11px; color: var(--dim); }
|
|
104
|
+
.keeper.on { border-color: var(--k, var(--ember)); box-shadow: 0 0 14px rgba(255, 179, 71, 0.35); }
|
|
105
|
+
.keeper.locked { opacity: 0.5; cursor: default; }
|
|
106
|
+
|
|
107
|
+
#inv { left: 12px; bottom: max(36px, calc(env(safe-area-inset-bottom) + 24px)); display: flex; gap: 6px; align-items: center; flex-wrap: wrap; max-width: min(300px, 24vw); transition: opacity 160ms; }
|
|
108
|
+
body:has(#cards.show) #inv { opacity: 0.25; }
|
|
109
|
+
body:has(#stats.show) #inv { bottom: 96px; }
|
|
110
|
+
#inv .slot { min-width: 30px; padding: 3px 6px; border-radius: 8px; background: var(--panel); border: 1px solid var(--line); font-size: 11px; text-align: center; }
|
|
111
|
+
#inv .slot { display: grid; justify-items: center; gap: 1px; color: #ffd9a0; }
|
|
112
|
+
#inv .slot.passive { color: #9fdcff; }
|
|
113
|
+
#inv .slot svg { display: block; }
|
|
114
|
+
#inv .slot span { font-size: 10px; color: var(--dim); font-variant-numeric: tabular-nums; }
|
|
115
|
+
#inv .slot.evolved { border-color: #ffd36b; }
|
|
116
|
+
#inv .slot.passive { border-color: rgba(122, 215, 255, 0.35); }
|
|
117
|
+
#dash-ring { width: 30px; height: 30px; border-radius: 50%; background: conic-gradient(var(--ember) calc(var(--p) * 360deg), rgba(255, 255, 255, 0.1) 0); display: grid; place-items: center; }
|
|
118
|
+
#dash-ring::after { content: "DASH"; font-size: 7px; font-weight: 800; letter-spacing: 0.06em; width: 22px; height: 22px; border-radius: 50%; background: rgba(12, 14, 20, 0.9); display: grid; place-items: center; }
|
|
119
|
+
#next { font-size: 11px; letter-spacing: 0.16em; color: #ffd9a0; margin-top: 2px; }
|
|
120
|
+
#goals { list-style: none; padding: 0; margin: 10px 0 0; font-size: 11px; color: var(--dim); }
|
|
121
|
+
#goals li { display: flex; gap: 6px; padding: 1px 0; }
|
|
122
|
+
#goals li.done { color: #ffd36b; }
|
|
123
|
+
#goals li em { font-style: normal; margin-left: auto; }
|
|
124
|
+
.dmg.crit { font-size: 22px; color: #ffd23f; }
|
|
125
|
+
#end-title { font-size: 26px; font-weight: 900; letter-spacing: 0.1em; color: var(--ember); }
|
|
126
|
+
#end-stats { display: flex; justify-content: space-around; margin: 16px 0; }
|
|
127
|
+
#end-stats small { display: block; color: var(--dim); font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase; }
|
|
128
|
+
#end-stats b { font-size: 22px; }
|
|
129
|
+
#end-earned { list-style: none; padding: 0; margin: 0 0 12px; text-align: left; font-size: 14px; }
|
|
130
|
+
#end-earned li { display: flex; justify-content: space-between; padding: 6px 0; border-top: 1px solid var(--line); }
|
|
131
|
+
#end-earned b { color: var(--ember); }
|
|
132
|
+
#end-next { color: var(--dim); font-size: 12px; }
|
|
133
|
+
|
|
134
|
+
#store { top: 0; right: 0; bottom: 0; width: min(320px, 86vw); background: rgba(14, 12, 10, 0.96); border-left: 1px solid var(--line); padding: 18px; box-sizing: border-box; transform: translateX(100%); transition: transform 220ms ease; pointer-events: auto; }
|
|
135
|
+
#store.show { transform: none; }
|
|
136
|
+
#store h2 { margin: 0 0 4px; font-size: 16px; letter-spacing: 0.12em; }
|
|
137
|
+
#store .item { display: flex; align-items: center; justify-content: space-between; padding: 12px 0; border-top: 1px solid var(--line); }
|
|
138
|
+
#store .swatch { width: 26px; height: 26px; border-radius: 8px; background: linear-gradient(135deg, #3cf2c4, #a77bff); margin-right: 10px; }
|
|
139
|
+
#store small { color: var(--dim); }
|
|
140
|
+
#store-close { margin-top: 14px; }
|
|
141
|
+
/* The coin sits on its own dark plate, so nothing in the scene runs through it. */
|
|
142
|
+
#coin-box { display: flex; align-items: center; justify-content: flex-end; gap: 6px; padding: 3px 10px; border-radius: 10px; background: rgba(12, 10, 8, 0.86); border: 1px solid var(--line); font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
143
|
+
#coin-box.guest b { color: var(--dim); font-weight: 600; }
|
|
144
|
+
/* Play money: a second line inside the plate, so the plate (and the Store row under it) makes room for it. */
|
|
145
|
+
#coin-box.play { display: block; text-align: right; }
|
|
146
|
+
#coin-box.play #coin-logo { vertical-align: middle; margin-right: 6px; }
|
|
147
|
+
#coin-box.play::after { content: "play money"; display: block; margin-top: 1px; font-size: 10px; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; color: var(--dim); }
|
|
148
|
+
#end-earned li.guest { color: var(--dim); }
|
|
149
|
+
#coin-logo { border-radius: 50%; }
|
|
150
|
+
|
|
151
|
+
#markers .marker { position: absolute; left: 0; top: 0; width: 34px; height: 34px; border-radius: 50%; border: 3px solid var(--c); box-sizing: border-box; background: conic-gradient(var(--c) calc(var(--p) * 360deg), rgba(0, 0, 0, 0.45) 0); opacity: 0.9; }
|
|
152
|
+
#markers .marker.off:not(.down) { width: 14px; height: 14px; border-width: 2px; background: var(--c); }
|
|
153
|
+
#markers .marker.down { animation: pulse 0.9s ease-in-out infinite; }
|
|
154
|
+
@keyframes pulse { 50% { box-shadow: 0 0 0 8px rgba(255, 80, 80, 0.25); } }
|
|
155
|
+
|
|
156
|
+
.dmg { position: fixed; left: 0; top: 0; z-index: 4; pointer-events: none; font: 800 14px ui-rounded, system-ui, sans-serif; color: #fff; text-shadow: 0 1px 0 #000, 0 0 6px rgba(0, 0, 0, 0.7); opacity: 0; will-change: transform, opacity; }
|
|
157
|
+
.dmg.big { font-size: 20px; color: #ffd36b; }
|
|
158
|
+
|
|
159
|
+
#toast { left: 50%; top: 92px; transform: translate(-50%, -10px); padding: 8px 14px; border-radius: 999px; background: rgba(20, 16, 10, 0.9); border: 1px solid var(--ember); font-weight: 700; opacity: 0; transition: opacity 200ms, transform 200ms; }
|
|
160
|
+
#toast.show { opacity: 1; transform: translate(-50%, 0); }
|
|
161
|
+
#announce { left: 50%; top: 28%; transform: translate(-50%, -50%) scale(0.92); text-align: center; opacity: 0; transition: opacity 220ms, transform 260ms cubic-bezier(0.2, 1.4, 0.4, 1); pointer-events: none; }
|
|
162
|
+
#announce.show { opacity: 1; transform: translate(-50%, -50%) scale(1); }
|
|
163
|
+
#announce-text { font-size: clamp(22px, 4.2vw, 44px); font-weight: 900; letter-spacing: 0.12em; color: #fff3dc; text-shadow: 0 2px 18px rgba(0, 0, 0, 0.8), 0 0 28px rgba(255, 160, 64, 0.55); }
|
|
164
|
+
#announce.danger #announce-text { color: #ffd2c4; text-shadow: 0 2px 18px rgba(0, 0, 0, 0.8), 0 0 30px rgba(255, 70, 40, 0.75); }
|
|
165
|
+
#announce.reward #announce-text { color: #fff1b0; text-shadow: 0 2px 18px rgba(0, 0, 0, 0.8), 0 0 30px rgba(255, 211, 107, 0.8); }
|
|
166
|
+
#arrows .arrow { position: absolute; left: 0; top: 0; width: 0; height: 0; border-left: 11px solid transparent; border-right: 11px solid transparent; border-bottom: 22px solid #ffb347; filter: drop-shadow(0 0 6px rgba(255, 140, 40, 0.9)); }
|
|
167
|
+
#hurt { inset: 0; background: radial-gradient(circle, transparent 45%, rgba(255, 40, 40, 0.5)); opacity: 0; }
|
|
168
|
+
#stats { left: 12px; bottom: max(12px, env(safe-area-inset-bottom)); font: 11px/1.4 ui-monospace, monospace; color: var(--dim); white-space: pre; display: none; }
|
|
169
|
+
#stats.show { display: block; }
|
|
170
|
+
#help { right: 12px; bottom: max(12px, env(safe-area-inset-bottom)); font-size: 11px; color: var(--dim); text-align: right; }
|
|
171
|
+
/* The first 5 s of a run: the controls, centred under your keeper. */
|
|
172
|
+
#intro { left: 50%; top: 64%; transform: translate(-50%, 0); padding: 10px 18px; border-radius: 999px; background: rgba(12, 10, 8, 0.72); border: 1px solid var(--line); font-size: 18px; font-weight: 700; letter-spacing: 0.04em; opacity: 0; transition: opacity 400ms; }
|
|
173
|
+
#intro.show { opacity: 1; }
|
|
174
|
+
/* Your own level-up: a warm flash from the edges and a word, for you only. */
|
|
175
|
+
#lvlflash { inset: 0; display: grid; place-items: center; opacity: 0; background: radial-gradient(circle, transparent 35%, rgba(255, 196, 90, 0.38)); }
|
|
176
|
+
#lvlflash b { font-size: 44px; font-weight: 900; letter-spacing: 0.2em; color: #ffe2a0; text-shadow: 0 0 24px rgba(255, 170, 60, 0.9); transform: translateY(-14vh) scale(0.9); }
|
|
177
|
+
#lvlflash.go { animation: lvlflash 700ms ease-out; }
|
|
178
|
+
#lvlflash.go b { animation: lvlword 700ms cubic-bezier(0.2, 1.4, 0.4, 1); }
|
|
179
|
+
@keyframes lvlflash { 0% { opacity: 0; } 12% { opacity: 1; } 100% { opacity: 0; } }
|
|
180
|
+
@keyframes lvlword { 0% { transform: translateY(-14vh) scale(0.6); } 30% { transform: translateY(-14vh) scale(1.08); } 100% { transform: translateY(-15vh) scale(1); } }
|
|
181
|
+
@media (prefers-reduced-motion: reduce) { #lvlflash.go, #lvlflash.go b { animation-duration: 1ms; } }
|
|
182
|
+
#banner { left: 50%; top: 50%; transform: translate(-50%, -50%); text-align: center; font-size: 15px; color: var(--dim); }
|
|
183
|
+
#banner b { display: block; font-size: 42px; letter-spacing: 0.18em; color: var(--ember); margin-bottom: 6px; }
|
|
184
|
+
|
|
185
|
+
.stick-zone { position: fixed; left: 0; top: 70px; bottom: 0; width: 62%; z-index: 3; touch-action: none; }
|
|
186
|
+
.dash-button { position: fixed; right: 22px; bottom: max(28px, env(safe-area-inset-bottom)); z-index: 6; width: 82px; height: 82px; border-radius: 50%; border: 2px solid var(--ember); background: rgba(255, 179, 71, 0.16); font-weight: 800; letter-spacing: 0.08em; }
|
|
187
|
+
/* Framed by Net Forge: the host's own Invite and Connect replace these. */
|
|
188
|
+
html[data-host="netnet"] #invite, html[data-host="netnet"] #signin { display: none; }
|
|
189
|
+
@media (pointer: coarse) {
|
|
190
|
+
#help, #intro { display: none; }
|
|
191
|
+
#cards { bottom: 128px; }
|
|
192
|
+
.card { width: 30vw; padding: 10px; }
|
|
193
|
+
}
|
|
194
|
+
@media (max-width: 560px) {
|
|
195
|
+
#team { display: none; }
|
|
196
|
+
#timer { font-size: 22px; }
|
|
197
|
+
/* Phones: the boss bar keeps to the centre column, clear of the HP bar, and the
|
|
198
|
+
three picks fit the screen with Reroll and Skip in a row under them. */
|
|
199
|
+
#boss { width: 26vw; }
|
|
200
|
+
#next { max-width: 26vw; margin: 2px auto 0; font-size: 10px; letter-spacing: 0.06em; }
|
|
201
|
+
#boss .boss-name { font-size: 10px; letter-spacing: 0.04em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
202
|
+
#cards { width: calc(100vw - 16px); flex-wrap: wrap; justify-content: center; gap: 6px; }
|
|
203
|
+
.card { width: calc((100vw - 32px) / 3); padding: 8px; }
|
|
204
|
+
.card-actions { grid-auto-flow: column; justify-content: center; width: 100%; }
|
|
205
|
+
}
|
|
206
|
+
</style>
|
|
207
|
+
</head>
|
|
208
|
+
<body>
|
|
209
|
+
<div id="game"></div>
|
|
210
|
+
<div id="hurt" class="hud"></div>
|
|
211
|
+
<div id="markers" class="hud" style="inset: 0"></div>
|
|
212
|
+
|
|
213
|
+
<div id="top-left" class="hud">
|
|
214
|
+
<div id="level">LV 1</div>
|
|
215
|
+
<div class="bar"><div id="xp-fill"></div></div>
|
|
216
|
+
<div class="bar" style="height: 12px"><div id="hp-fill"></div><span id="hp-text"></span></div>
|
|
217
|
+
<ul id="team"></ul>
|
|
218
|
+
<ul id="goals"></ul>
|
|
219
|
+
</div>
|
|
220
|
+
<div id="top-center" class="hud">
|
|
221
|
+
<div id="timer">0:00</div>
|
|
222
|
+
<div id="wave"></div>
|
|
223
|
+
<div id="next"></div>
|
|
224
|
+
<div id="boss"></div>
|
|
225
|
+
</div>
|
|
226
|
+
<div id="top-right" class="hud">
|
|
227
|
+
<div id="kills-box">Kills <span id="kills">0</span></div>
|
|
228
|
+
<div id="coin-box"><img id="coin-logo" alt="" width="18" height="18" hidden /><b id="coin-balance"></b></div>
|
|
229
|
+
<div class="row"><button id="invite">Invite</button><button id="store-btn">Store</button></div>
|
|
230
|
+
<div class="row"><button id="sound">Sound on</button><button id="signin">Sign in to save</button></div>
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
<div id="cards" class="hud">
|
|
234
|
+
<button class="card" id="card0"></button>
|
|
235
|
+
<button class="card" id="card1"></button>
|
|
236
|
+
<button class="card" id="card2"></button>
|
|
237
|
+
<span id="pending"></span>
|
|
238
|
+
<div class="card-actions"><button id="reroll">Reroll</button><button id="skip">Skip</button></div>
|
|
239
|
+
</div>
|
|
240
|
+
|
|
241
|
+
<div id="downed" class="hud"><div class="box"><div id="downed-title">DOWNED</div><div id="downed-sub"></div><div class="downed-actions"><button id="getup-btn">Get up <kbd>Space</kbd></button><button id="revive-btn">Revive</button></div></div></div>
|
|
242
|
+
|
|
243
|
+
<div id="end" class="hud">
|
|
244
|
+
<div class="box">
|
|
245
|
+
<div id="end-title"></div>
|
|
246
|
+
<div id="end-stats"></div>
|
|
247
|
+
<div id="end-players"></div>
|
|
248
|
+
<ul id="end-earned"></ul>
|
|
249
|
+
<div id="end-keeper-slot"></div>
|
|
250
|
+
<div id="end-actions"><button id="end-again">Play again</button><div id="end-next"></div></div>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
|
|
254
|
+
<aside id="store" class="hud">
|
|
255
|
+
<h2>STORE</h2>
|
|
256
|
+
<small id="coin-name"></small>
|
|
257
|
+
<div class="item"><div style="display: flex; align-items: center"><span class="swatch"></span><div>Disco palette<br /><small>Cosmetic</small></div></div><button id="buy-disco"></button></div>
|
|
258
|
+
<div class="item"><div>Revive<br /><small>Spend while downed</small></div><small id="revive-price"></small></div>
|
|
259
|
+
<div class="item" id="gear-row"><div style="display: block"><span id="gear-name"></span><br /><small id="gear-line"></small></div><button id="gear-buy" style="white-space: nowrap"></button></div>
|
|
260
|
+
<div class="item" id="stake-row"><div>Stake a run<br /><small id="stake-line"></small></div><span style="white-space: nowrap"><input id="stake-amount" type="number" aria-label="Stake" style="width: 56px" /> <button id="stake-go" style="min-width: 0">Stake</button></span></div>
|
|
261
|
+
<small id="store-note"></small>
|
|
262
|
+
<br /><button id="store-close">Close</button>
|
|
263
|
+
</aside>
|
|
264
|
+
|
|
265
|
+
<div id="keepers" class="hud"></div>
|
|
266
|
+
<div id="inv" class="hud"><div id="dash-ring" style="--p: 1"></div></div>
|
|
267
|
+
<div id="toast" class="hud"></div>
|
|
268
|
+
<div id="announce" class="hud"><div id="announce-text"></div></div>
|
|
269
|
+
<div id="arrows" class="hud" style="inset: 0"></div>
|
|
270
|
+
<div id="stats" class="hud"></div>
|
|
271
|
+
<div id="help" class="hud">WASD move · Space dash · 1 2 3 pick</div>
|
|
272
|
+
<div id="intro" class="hud">WASD move · Space dash</div>
|
|
273
|
+
<div id="lvlflash" class="hud"><b>LEVEL UP</b></div>
|
|
274
|
+
<div id="banner" class="hud"><b>ROMPHOUSE</b><span id="banner-text">Setting up the party</span></div>
|
|
275
|
+
<script type="module" src="./main.ts"></script>
|
|
276
|
+
</body>
|
|
277
|
+
</html>
|
package/client/input.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Top-down input for ROMPHOUSE: keyboard (WASD/arrows, Space or Shift to
|
|
3
|
+
* dash, 1/2/3 to pick an upgrade), a gamepad (left stick, A dash, X/Y/B
|
|
4
|
+
* picks, rumble on hits) and touch (a nipplejs stick anywhere on the left
|
|
5
|
+
* of the screen, a dash button; upgrade cards are tapped in the HUD).
|
|
6
|
+
*
|
|
7
|
+
* The sim's input frame: yaw 0, so moveX is +x (screen right) and moveZ is
|
|
8
|
+
* -z (screen up). Upgrade picks are held for two ticks so the sim sees the
|
|
9
|
+
* press edge even if a tick is skipped.
|
|
10
|
+
*/
|
|
11
|
+
import { Button, keeperButtons } from "@netforge/engine-sim";
|
|
12
|
+
import type { InputCommand } from "@netforge/engine-sim";
|
|
13
|
+
|
|
14
|
+
export class PartyInput {
|
|
15
|
+
private readonly keys = new Set<string>();
|
|
16
|
+
private stickX = 0;
|
|
17
|
+
private stickY = 0;
|
|
18
|
+
private dashHeld = false;
|
|
19
|
+
private choice = -1;
|
|
20
|
+
private choiceTicks = 0;
|
|
21
|
+
/** Reroll or skip pressed (Button.Reroll / Button.Skip), held for two ticks like a pick. */
|
|
22
|
+
private extra = 0;
|
|
23
|
+
private extraTicks = 0;
|
|
24
|
+
private readonly cleanup: (() => void)[] = [];
|
|
25
|
+
readonly touch: boolean;
|
|
26
|
+
private pad: Gamepad | null = null;
|
|
27
|
+
|
|
28
|
+
constructor(private readonly root: HTMLElement) {
|
|
29
|
+
this.touch = globalThis.matchMedia?.("(pointer: coarse)").matches || "ontouchstart" in globalThis;
|
|
30
|
+
const down = (e: KeyboardEvent) => {
|
|
31
|
+
if (e.code === "Space" || e.code.startsWith("Arrow")) e.preventDefault();
|
|
32
|
+
this.keys.add(e.code);
|
|
33
|
+
if (e.code === "Digit1" || e.code === "Numpad1") this.pick(0);
|
|
34
|
+
if (e.code === "Digit2" || e.code === "Numpad2") this.pick(1);
|
|
35
|
+
if (e.code === "Digit3" || e.code === "Numpad3") this.pick(2);
|
|
36
|
+
if (e.code === "KeyR") this.reroll();
|
|
37
|
+
if (e.code === "KeyX") this.skip();
|
|
38
|
+
};
|
|
39
|
+
const up = (e: KeyboardEvent) => this.keys.delete(e.code);
|
|
40
|
+
const blur = () => this.keys.clear();
|
|
41
|
+
addEventListener("keydown", down);
|
|
42
|
+
addEventListener("keyup", up);
|
|
43
|
+
addEventListener("blur", blur);
|
|
44
|
+
this.cleanup.push(() => {
|
|
45
|
+
removeEventListener("keydown", down);
|
|
46
|
+
removeEventListener("keyup", up);
|
|
47
|
+
removeEventListener("blur", blur);
|
|
48
|
+
});
|
|
49
|
+
if (this.touch) void this.bindTouch();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Take upgrade offer k (0..2) on the next ticks (keys, card taps, gamepad). */
|
|
53
|
+
pick(k: number): void {
|
|
54
|
+
this.choice = k;
|
|
55
|
+
this.choiceTicks = 2;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Press dash for the next ticks (the downed screen's Get up button). */
|
|
59
|
+
getUp(): void {
|
|
60
|
+
this.extra = Button.Sprint;
|
|
61
|
+
this.extraTicks = 2;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Reroll the open cards (one per run). */
|
|
65
|
+
reroll(): void {
|
|
66
|
+
this.extra = Button.Reroll;
|
|
67
|
+
this.extraTicks = 2;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The keeper to play (held on every tick; the sim takes it at the start of a run). -1: none. */
|
|
71
|
+
keeper = -1;
|
|
72
|
+
/** The end screen's "Play again" vote, held until the next run starts. */
|
|
73
|
+
ready = false;
|
|
74
|
+
|
|
75
|
+
/** Skip the open cards for a small heal. */
|
|
76
|
+
skip(): void {
|
|
77
|
+
this.extra = Button.Skip;
|
|
78
|
+
this.extraTicks = 2;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private async bindTouch(): Promise<void> {
|
|
82
|
+
const nipple = await import("nipplejs");
|
|
83
|
+
const zone = document.createElement("div");
|
|
84
|
+
zone.className = "stick-zone";
|
|
85
|
+
this.root.append(zone);
|
|
86
|
+
const sticks = nipple.create({ zone, mode: "dynamic", color: "rgba(255,200,120,0.8)", size: 120 });
|
|
87
|
+
sticks.on("move", (evt) => {
|
|
88
|
+
this.stickX = evt.data.vector.x;
|
|
89
|
+
this.stickY = evt.data.vector.y;
|
|
90
|
+
});
|
|
91
|
+
sticks.on("end", () => {
|
|
92
|
+
this.stickX = 0;
|
|
93
|
+
this.stickY = 0;
|
|
94
|
+
});
|
|
95
|
+
const dash = document.createElement("button");
|
|
96
|
+
dash.className = "dash-button";
|
|
97
|
+
dash.textContent = "DASH";
|
|
98
|
+
dash.addEventListener("pointerdown", (e) => {
|
|
99
|
+
e.preventDefault();
|
|
100
|
+
this.dashHeld = true;
|
|
101
|
+
});
|
|
102
|
+
const release = () => {
|
|
103
|
+
this.dashHeld = false;
|
|
104
|
+
};
|
|
105
|
+
dash.addEventListener("pointerup", release);
|
|
106
|
+
dash.addEventListener("pointercancel", release);
|
|
107
|
+
dash.addEventListener("pointerleave", release);
|
|
108
|
+
this.root.append(dash);
|
|
109
|
+
this.cleanup.push(() => {
|
|
110
|
+
sticks.destroy();
|
|
111
|
+
zone.remove();
|
|
112
|
+
dash.remove();
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private readPad(): { x: number; y: number; buttons: number } | null {
|
|
117
|
+
const pads = navigator.getGamepads?.() ?? [];
|
|
118
|
+
this.pad = null;
|
|
119
|
+
for (const p of pads) if (p && p.connected) this.pad = p;
|
|
120
|
+
const p = this.pad;
|
|
121
|
+
if (!p) return null;
|
|
122
|
+
const dz = (v: number) => (Math.abs(v) < 0.18 ? 0 : v);
|
|
123
|
+
let buttons = 0;
|
|
124
|
+
if (p.buttons[0]?.pressed || p.buttons[5]?.pressed) buttons |= Button.Sprint;
|
|
125
|
+
if (p.buttons[2]?.pressed) this.pick(0);
|
|
126
|
+
if (p.buttons[3]?.pressed) this.pick(1);
|
|
127
|
+
if (p.buttons[1]?.pressed) this.pick(2);
|
|
128
|
+
return { x: dz(p.axes[0] ?? 0), y: -dz(p.axes[1] ?? 0), buttons };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Controller rumble (Gamepad API), when a pad that supports it is connected. */
|
|
132
|
+
rumble(strength: number, ms: number): void {
|
|
133
|
+
const act = (this.pad as (Gamepad & { vibrationActuator?: { playEffect?: (t: string, p: object) => Promise<unknown> } }) | null)?.vibrationActuator;
|
|
134
|
+
void act?.playEffect?.("dual-rumble", { duration: ms, strongMagnitude: strength, weakMagnitude: strength * 0.6 }).catch(() => undefined);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** One command for the next fixed tick. */
|
|
138
|
+
sample(tick: number): InputCommand {
|
|
139
|
+
const k = this.keys;
|
|
140
|
+
let mx = (k.has("KeyD") || k.has("ArrowRight") ? 1 : 0) - (k.has("KeyA") || k.has("ArrowLeft") ? 1 : 0);
|
|
141
|
+
let my = (k.has("KeyW") || k.has("ArrowUp") ? 1 : 0) - (k.has("KeyS") || k.has("ArrowDown") ? 1 : 0);
|
|
142
|
+
let buttons = 0;
|
|
143
|
+
if (k.has("Space") || k.has("ShiftLeft") || k.has("ShiftRight") || this.dashHeld) buttons |= Button.Sprint;
|
|
144
|
+
const pad = this.readPad();
|
|
145
|
+
if (pad) {
|
|
146
|
+
if (pad.x !== 0 || pad.y !== 0) {
|
|
147
|
+
mx = pad.x;
|
|
148
|
+
my = pad.y;
|
|
149
|
+
}
|
|
150
|
+
buttons |= pad.buttons;
|
|
151
|
+
}
|
|
152
|
+
if (this.stickX !== 0 || this.stickY !== 0) {
|
|
153
|
+
mx = this.stickX;
|
|
154
|
+
my = this.stickY;
|
|
155
|
+
}
|
|
156
|
+
if (this.extraTicks > 0) {
|
|
157
|
+
buttons |= this.extra;
|
|
158
|
+
this.extraTicks--;
|
|
159
|
+
}
|
|
160
|
+
if (this.choiceTicks > 0) {
|
|
161
|
+
buttons |= Button.Choice1 << this.choice;
|
|
162
|
+
this.choiceTicks--;
|
|
163
|
+
}
|
|
164
|
+
if (this.keeper >= 0) buttons |= keeperButtons(this.keeper);
|
|
165
|
+
if (this.ready) buttons |= Button.Ready;
|
|
166
|
+
return { seq: 0, tick, moveX: mx, moveZ: my, yaw: 0, buttons };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
dispose(): void {
|
|
170
|
+
for (const f of this.cleanup.splice(0)) f();
|
|
171
|
+
}
|
|
172
|
+
}
|