@netforge/template-racer 0.0.0-stage → 0.2.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/AGENTS.md +65 -0
- package/CLAUDE.md +11 -0
- package/LICENSE +21 -0
- package/NOTICE +33 -0
- package/README.md +74 -2
- package/client/audio.ts +26 -0
- package/client/decor.ts +27 -0
- package/client/host.ts +96 -0
- package/client/hud.ts +257 -0
- package/client/index.html +189 -0
- package/client/input.ts +105 -0
- package/client/main.ts +464 -0
- package/client/names.ts +42 -0
- package/client/scene.ts +646 -0
- package/client/stake.ts +143 -0
- package/llms-full.txt +236 -0
- package/llms.txt +125 -0
- package/package.json +80 -3
- package/src/game.ts +96 -0
- package/src/manifest.ts +44 -0
- package/src/store.ts +109 -0
|
@@ -0,0 +1,189 @@
|
|
|
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>Torquebay</title>
|
|
7
|
+
<link rel="icon" href="data:," />
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
color-scheme: dark;
|
|
11
|
+
--ink: #fff8ec;
|
|
12
|
+
--dim: #c9bfae;
|
|
13
|
+
--sun: #ff8a3d;
|
|
14
|
+
--leaf: #7ee08a;
|
|
15
|
+
--sky: #8fd3ff;
|
|
16
|
+
--coral: #ff7a59;
|
|
17
|
+
--panel: rgba(28, 22, 40, 0.8);
|
|
18
|
+
--line: rgba(255, 210, 63, 0.28);
|
|
19
|
+
font-family: "Avenir Next", "Segoe UI", system-ui, -apple-system, sans-serif;
|
|
20
|
+
}
|
|
21
|
+
html, body { margin: 0; height: 100%; background: #1d3a52; color: var(--ink); overflow: hidden; touch-action: none; -webkit-user-select: none; user-select: none; }
|
|
22
|
+
#game { position: fixed; inset: 0; }
|
|
23
|
+
#game canvas { display: block; width: 100%; height: 100%; }
|
|
24
|
+
.hud { position: fixed; z-index: 5; pointer-events: none; font-variant-numeric: tabular-nums; }
|
|
25
|
+
.hud button { pointer-events: auto; }
|
|
26
|
+
button { font: inherit; color: var(--ink); background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 7px 11px; cursor: pointer; }
|
|
27
|
+
button:disabled { opacity: 0.45; cursor: default; }
|
|
28
|
+
button:active { transform: scale(0.97); }
|
|
29
|
+
|
|
30
|
+
#top-left { top: max(12px, env(safe-area-inset-top)); left: 12px; width: min(320px, 44vw); }
|
|
31
|
+
#title { font-weight: 900; letter-spacing: 0.22em; font-size: 13px; color: var(--sun); text-shadow: 0 2px 0 rgba(0, 0, 0, 0.35); }
|
|
32
|
+
#course { margin-top: 2px; font-size: 12px; color: var(--dim); letter-spacing: 0.06em; }
|
|
33
|
+
#lap { margin-top: 6px; font-size: 22px; font-weight: 900; letter-spacing: 0.06em; text-shadow: 0 2px 0 rgba(0, 0, 0, 0.35); }
|
|
34
|
+
#map { position: fixed; z-index: 5; right: 12px; bottom: max(14px, env(safe-area-inset-bottom)); width: 170px; height: 170px; border-radius: 14px; background: rgba(28, 22, 40, 0.55); pointer-events: none; }
|
|
35
|
+
#speed { right: 196px; bottom: max(18px, env(safe-area-inset-bottom)); font-size: 34px; font-weight: 900; text-align: right; text-shadow: 0 2px 0 rgba(0, 0, 0, 0.4); }
|
|
36
|
+
#speed small { display: block; font-size: 10px; letter-spacing: 0.2em; color: var(--dim); }
|
|
37
|
+
#drift { left: 50%; bottom: 90px; transform: translateX(-50%); width: 160px; height: 8px; border-radius: 5px; background: rgba(0, 0, 0, 0.35); overflow: hidden; display: none; }
|
|
38
|
+
#drift.on { display: block; }
|
|
39
|
+
#drift > i { display: block; height: 100%; width: 0; background: var(--c, #5fc8ff); }
|
|
40
|
+
#coins { margin-top: 8px; display: flex; gap: 8px; align-items: center; font-weight: 800; font-size: 15px; }
|
|
41
|
+
#coins .dot { width: 14px; height: 14px; border-radius: 50%; background: radial-gradient(circle at 35% 35%, #fff4b0, #ffd23f 55%, #c98a10); box-shadow: 0 0 6px rgba(255, 210, 63, 0.7); }
|
|
42
|
+
#coins small { color: var(--dim); font-weight: 600; }
|
|
43
|
+
#buffs { margin-top: 4px; font-size: 10px; letter-spacing: 0.16em; color: var(--leaf); text-transform: uppercase; min-height: 12px; }
|
|
44
|
+
|
|
45
|
+
#top-center { top: max(8px, env(safe-area-inset-top)); left: 50%; transform: translateX(-50%); text-align: center; }
|
|
46
|
+
#timer { font-size: 30px; font-weight: 900; letter-spacing: 0.04em; text-shadow: 0 2px 0 rgba(0, 0, 0, 0.35); }
|
|
47
|
+
#timer.low { color: var(--coral); }
|
|
48
|
+
#place { font-size: 28px; letter-spacing: 0.06em; color: var(--ink); font-weight: 900; }
|
|
49
|
+
|
|
50
|
+
#top-right { top: max(10px, env(safe-area-inset-top)); right: 12px; display: flex; flex-direction: column; align-items: flex-end; gap: 6px; width: min(240px, 46vw); }
|
|
51
|
+
#top-right .row { display: flex; gap: 6px; }
|
|
52
|
+
#coin-box { display: flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: 10px; background: rgba(28, 22, 40, 0.9); border: 1px solid var(--line); font-weight: 700; white-space: nowrap; }
|
|
53
|
+
#coin-box.guest #coin-balance { color: var(--dim); font-weight: 600; }
|
|
54
|
+
#coin-box.play { display: block; text-align: right; }
|
|
55
|
+
#coin-box.play #coin-logo { vertical-align: middle; margin-right: 6px; }
|
|
56
|
+
#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); }
|
|
57
|
+
#coin-logo { border-radius: 50%; }
|
|
58
|
+
#board { list-style: none; padding: 6px 8px; margin: 0; min-width: 150px; box-sizing: border-box; background: var(--panel); border-radius: 10px; font-size: 12px; }
|
|
59
|
+
#board:empty { display: none; }
|
|
60
|
+
#board li { display: flex; align-items: center; gap: 6px; padding: 2px 0; }
|
|
61
|
+
#board li.me { color: var(--sun); font-weight: 800; }
|
|
62
|
+
#board em { font-style: normal; color: var(--dim); width: 14px; }
|
|
63
|
+
#board .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--c, transparent); }
|
|
64
|
+
#board b { margin-left: auto; min-width: 22px; text-align: right; }
|
|
65
|
+
|
|
66
|
+
#count { inset: 0; display: none; place-items: center; }
|
|
67
|
+
#count.show { display: grid; }
|
|
68
|
+
#count b { font-size: 120px; font-weight: 900; color: var(--sun); text-shadow: 0 6px 0 rgba(0, 0, 0, 0.3); }
|
|
69
|
+
#count small { display: block; text-align: center; font-size: 14px; letter-spacing: 0.24em; color: var(--ink); }
|
|
70
|
+
|
|
71
|
+
#end { inset: 0; display: none; place-items: center; background: rgba(20, 14, 30, 0.6); }
|
|
72
|
+
#end.show { display: grid; }
|
|
73
|
+
#end .box { width: min(440px, 90vw); padding: 22px; border-radius: 16px; background: rgba(32, 24, 48, 0.97); border: 1px solid var(--line); text-align: center; }
|
|
74
|
+
#end-title { font-size: 24px; font-weight: 900; letter-spacing: 0.12em; color: var(--sun); }
|
|
75
|
+
#end-board { list-style: none; padding: 0; margin: 14px 0; text-align: left; font-size: 14px; }
|
|
76
|
+
#end-board li { display: flex; gap: 8px; padding: 4px 0; border-top: 1px solid rgba(255, 255, 255, 0.08); }
|
|
77
|
+
#end-board li.me { color: var(--sun); font-weight: 800; }
|
|
78
|
+
#end-board em { font-style: normal; color: var(--dim); width: 18px; }
|
|
79
|
+
#end-board b { margin-left: auto; }
|
|
80
|
+
#end-earned { list-style: none; padding: 0; margin: 0 0 12px; text-align: left; font-size: 14px; }
|
|
81
|
+
#end-earned li { display: flex; justify-content: space-between; padding: 6px 0; border-top: 1px solid var(--line); }
|
|
82
|
+
#end-earned li.guest { color: var(--dim); }
|
|
83
|
+
#end-earned b { color: var(--sun); }
|
|
84
|
+
#end-next { color: var(--dim); font-size: 12px; letter-spacing: 0.12em; }
|
|
85
|
+
#ready { pointer-events: auto; margin-top: 10px; }
|
|
86
|
+
|
|
87
|
+
#store { top: 0; right: 0; bottom: 0; width: min(330px, 88vw); background: rgba(28, 22, 40, 0.98); border-left: 1px solid var(--line); padding: 18px; box-sizing: border-box; transform: translateX(100%); transition: transform 220ms ease; pointer-events: auto; overflow-y: auto; z-index: 8; }
|
|
88
|
+
#store.show { transform: none; }
|
|
89
|
+
#store h2 { margin: 0 0 2px; font-size: 15px; letter-spacing: 0.2em; }
|
|
90
|
+
#store h3 { margin: 16px 0 0; font-size: 10px; letter-spacing: 0.24em; color: var(--dim); font-weight: 600; }
|
|
91
|
+
#store .item { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 11px 0; border-top: 1px solid rgba(255, 255, 255, 0.08); }
|
|
92
|
+
#store .item > div { display: flex; align-items: center; gap: 10px; }
|
|
93
|
+
#store .swatch { width: 26px; height: 26px; border-radius: 7px; flex: none; }
|
|
94
|
+
#store small { color: var(--dim); }
|
|
95
|
+
#store button { min-width: 88px; }
|
|
96
|
+
#store-close { margin-top: 16px; }
|
|
97
|
+
|
|
98
|
+
#toast { left: 50%; top: 84px; transform: translate(-50%, -10px); padding: 8px 14px; border-radius: 999px; background: rgba(28, 22, 40, 0.94); border: 1px solid var(--sun); font-weight: 700; opacity: 0; transition: opacity 200ms, transform 200ms; white-space: nowrap; }
|
|
99
|
+
#toast.show { opacity: 1; transform: translate(-50%, 0); }
|
|
100
|
+
#flash { inset: 0; background: radial-gradient(circle, transparent 45%, rgba(255, 90, 60, 0.45)); opacity: 0; }
|
|
101
|
+
#stats { left: 12px; bottom: 90px; font: 11px/1.4 ui-monospace, monospace; color: var(--dim); white-space: pre; display: none; }
|
|
102
|
+
#stats.show { display: block; }
|
|
103
|
+
#help { left: 12px; bottom: max(14px, env(safe-area-inset-bottom)); font-size: 10px; letter-spacing: 0.08em; color: var(--dim); text-shadow: 0 1px 2px #000; }
|
|
104
|
+
#banner { left: 50%; top: 50%; transform: translate(-50%, -50%); text-align: center; font-size: 14px; color: var(--dim); letter-spacing: 0.12em; }
|
|
105
|
+
#banner b { display: block; font-size: 44px; letter-spacing: 0.28em; color: var(--sun); margin-bottom: 8px; }
|
|
106
|
+
|
|
107
|
+
.touch { position: fixed; z-index: 6; bottom: max(18px, env(safe-area-inset-bottom)); width: 74px; height: 74px; border-radius: 50%; font-weight: 900; font-size: 13px; letter-spacing: 0.06em; display: none; touch-action: none; }
|
|
108
|
+
.touch.left { left: 18px; }
|
|
109
|
+
.touch.right { left: 104px; }
|
|
110
|
+
.touch.jump { right: 18px; width: 92px; height: 92px; border: 2px solid var(--sun); background: rgba(255, 210, 63, 0.16); }
|
|
111
|
+
.touch.dash { right: 122px; border: 2px solid var(--sky); background: rgba(143, 211, 255, 0.14); }
|
|
112
|
+
html[data-host="netnet"] #invite { display: none; }
|
|
113
|
+
@media (pointer: coarse) {
|
|
114
|
+
.touch { display: block; }
|
|
115
|
+
#help { display: none; }
|
|
116
|
+
#map { bottom: 120px; width: 120px; height: 120px; }
|
|
117
|
+
#speed { display: none; }
|
|
118
|
+
}
|
|
119
|
+
@media (max-width: 560px) {
|
|
120
|
+
#board li:nth-child(n + 5) { display: none; }
|
|
121
|
+
#timer { font-size: 22px; }
|
|
122
|
+
#top-left { width: 40vw; }
|
|
123
|
+
#top-right { top: max(52px, calc(env(safe-area-inset-top) + 42px)); width: 56vw; }
|
|
124
|
+
#top-right button { padding: 5px 8px; font-size: 12px; white-space: nowrap; }
|
|
125
|
+
}
|
|
126
|
+
</style>
|
|
127
|
+
</head>
|
|
128
|
+
<body>
|
|
129
|
+
<div id="game"></div>
|
|
130
|
+
<div id="flash" class="hud"></div>
|
|
131
|
+
|
|
132
|
+
<div id="top-left" class="hud">
|
|
133
|
+
<div id="title">TORQUEBAY</div>
|
|
134
|
+
<div id="course"></div>
|
|
135
|
+
<div id="lap">LAP 1/3</div>
|
|
136
|
+
<div id="coins"><span class="dot"></span><span id="coin-count">0</span><small id="coin-total"></small></div>
|
|
137
|
+
<div id="buffs"></div>
|
|
138
|
+
</div>
|
|
139
|
+
<div id="top-center" class="hud">
|
|
140
|
+
<div id="timer">3:00</div>
|
|
141
|
+
<div id="place"></div>
|
|
142
|
+
</div>
|
|
143
|
+
<div id="top-right" class="hud">
|
|
144
|
+
<div id="coin-box"><img id="coin-logo" alt="" width="18" height="18" hidden /><span id="coin-balance"></span></div>
|
|
145
|
+
<div class="row"><button id="invite">Invite</button><button id="store-btn">Store</button><button id="sound">Sound off</button></div>
|
|
146
|
+
<ul id="board"></ul>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<canvas id="map" width="340" height="340"></canvas>
|
|
150
|
+
<div id="speed" class="hud"><span id="kmh">0</span><small>KM/H</small></div>
|
|
151
|
+
<div id="drift" class="hud"><i></i></div>
|
|
152
|
+
<div id="count" class="hud"><div><b id="count-n">3</b><small id="count-sub"></small></div></div>
|
|
153
|
+
|
|
154
|
+
<div id="end" class="hud">
|
|
155
|
+
<div class="box">
|
|
156
|
+
<div id="end-title"></div>
|
|
157
|
+
<ul id="end-board"></ul>
|
|
158
|
+
<ul id="end-earned"></ul>
|
|
159
|
+
<div id="end-next"></div>
|
|
160
|
+
<button id="ready">Next race</button>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<aside id="store" class="hud">
|
|
165
|
+
<h2>STORE</h2>
|
|
166
|
+
<small id="coin-name"></small>
|
|
167
|
+
<h3>THIS ROUND</h3>
|
|
168
|
+
<div id="store-upgrades"></div>
|
|
169
|
+
<h3>PAINT</h3>
|
|
170
|
+
<div id="store-finishes"></div>
|
|
171
|
+
<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>
|
|
172
|
+
<div class="item" id="pot-row" hidden><div style="display: block">Round pot<br /><small id="pot-line"></small></div><button id="pot-go" style="white-space: nowrap"></button></div>
|
|
173
|
+
<div class="item" id="stake-row"><div style="display: block">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>
|
|
174
|
+
<small id="store-note"></small>
|
|
175
|
+
<br /><button id="store-close">Close</button>
|
|
176
|
+
</aside>
|
|
177
|
+
|
|
178
|
+
<button class="touch left" id="t-left" aria-label="Left">◀</button>
|
|
179
|
+
<button class="touch right" id="t-right" aria-label="Right">▶</button>
|
|
180
|
+
<button class="touch dash" id="t-brake">BRAKE</button>
|
|
181
|
+
<button class="touch jump" id="t-drift">DRIFT</button>
|
|
182
|
+
|
|
183
|
+
<div id="toast" class="hud"></div>
|
|
184
|
+
<div id="stats" class="hud"></div>
|
|
185
|
+
<div id="help" class="hud">W or Up accelerate · S or Down brake · A D or arrows steer · hold Space or Shift through a bend to drift, release for a boost</div>
|
|
186
|
+
<div id="banner" class="hud"><b>TORQUEBAY</b><span id="banner-text">Warming up the tyres</span></div>
|
|
187
|
+
<script type="module" src="./main.ts"></script>
|
|
188
|
+
</body>
|
|
189
|
+
</html>
|
package/client/input.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TORQUEBAY input: throttle, brake, steer, drift.
|
|
3
|
+
*
|
|
4
|
+
* - Keyboard: W or Up accelerates, S or Down brakes and reverses, A/D or
|
|
5
|
+
* Left/Right steer, Space or Shift drifts (hold through a bend, release
|
|
6
|
+
* for the mini-turbo); R or Enter readies the next race.
|
|
7
|
+
* - Gamepad: left stick steers, RT (or A) accelerates, LT (or B) brakes,
|
|
8
|
+
* RB or X drifts.
|
|
9
|
+
* - Touch: the kart accelerates by itself; left and right buttons steer,
|
|
10
|
+
* DRIFT and BRAKE buttons.
|
|
11
|
+
*/
|
|
12
|
+
import { Button } from "@netforge/engine-sim";
|
|
13
|
+
import type { InputCommand } from "@netforge/engine-sim";
|
|
14
|
+
|
|
15
|
+
export class KartInput {
|
|
16
|
+
private readonly keys = new Set<string>();
|
|
17
|
+
private readonly touchHeld = new Set<string>();
|
|
18
|
+
private readyLatch = 0;
|
|
19
|
+
private driftLatch = 0;
|
|
20
|
+
readonly touch: boolean;
|
|
21
|
+
private pad: Gamepad | null = null;
|
|
22
|
+
|
|
23
|
+
constructor(root: HTMLElement) {
|
|
24
|
+
this.touch = globalThis.matchMedia?.("(pointer: coarse)").matches || "ontouchstart" in globalThis;
|
|
25
|
+
addEventListener("keydown", (e) => {
|
|
26
|
+
if (e.code === "Space" || e.code.startsWith("Arrow")) e.preventDefault();
|
|
27
|
+
if (e.repeat) return;
|
|
28
|
+
this.keys.add(e.code);
|
|
29
|
+
if (e.code === "Space" || e.code.startsWith("Shift")) this.driftLatch = 2;
|
|
30
|
+
if (e.code === "KeyR" || e.code === "Enter") this.readyLatch = 6;
|
|
31
|
+
});
|
|
32
|
+
addEventListener("keyup", (e) => this.keys.delete(e.code));
|
|
33
|
+
addEventListener("blur", () => {
|
|
34
|
+
this.keys.clear();
|
|
35
|
+
this.touchHeld.clear();
|
|
36
|
+
});
|
|
37
|
+
const bind = (id: string, name: string) => {
|
|
38
|
+
const el = root.ownerDocument.getElementById(id);
|
|
39
|
+
if (!el) return;
|
|
40
|
+
el.addEventListener("pointerdown", (e) => {
|
|
41
|
+
e.preventDefault();
|
|
42
|
+
this.touchHeld.add(name);
|
|
43
|
+
if (name === "drift") this.driftLatch = 2;
|
|
44
|
+
el.setPointerCapture?.(e.pointerId);
|
|
45
|
+
});
|
|
46
|
+
const off = () => this.touchHeld.delete(name);
|
|
47
|
+
el.addEventListener("pointerup", off);
|
|
48
|
+
el.addEventListener("pointercancel", off);
|
|
49
|
+
el.addEventListener("lostpointercapture", off);
|
|
50
|
+
};
|
|
51
|
+
bind("t-left", "left");
|
|
52
|
+
bind("t-right", "right");
|
|
53
|
+
bind("t-drift", "drift");
|
|
54
|
+
bind("t-brake", "brake");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
ready(): void {
|
|
58
|
+
this.readyLatch = 6;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private has(...codes: string[]): boolean {
|
|
62
|
+
return codes.some((c) => this.keys.has(c));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
sample(tick: number): InputCommand {
|
|
66
|
+
this.pad = (globalThis.navigator?.getGamepads?.() ?? []).find((p) => p) ?? null;
|
|
67
|
+
let steer = 0;
|
|
68
|
+
let throttle = 0;
|
|
69
|
+
let drift = false;
|
|
70
|
+
if (this.has("KeyA", "ArrowLeft") || this.touchHeld.has("left")) steer -= 1;
|
|
71
|
+
if (this.has("KeyD", "ArrowRight") || this.touchHeld.has("right")) steer += 1;
|
|
72
|
+
if (this.has("KeyW", "ArrowUp")) throttle = 1;
|
|
73
|
+
if (this.has("KeyS", "ArrowDown") || this.touchHeld.has("brake")) throttle = -1;
|
|
74
|
+
// Touch drives itself: the kart accelerates unless the brake is held.
|
|
75
|
+
if (this.touch && this.touchHeld.size > 0 && !this.touchHeld.has("brake")) throttle = 1;
|
|
76
|
+
if (this.touch && this.touchHeld.size === 0 && this.keys.size === 0) throttle = 1;
|
|
77
|
+
if (this.has("Space", "ShiftLeft", "ShiftRight") || this.touchHeld.has("drift")) drift = true;
|
|
78
|
+
const pad = this.pad;
|
|
79
|
+
if (pad) {
|
|
80
|
+
const ax = pad.axes[0] ?? 0;
|
|
81
|
+
if (Math.abs(ax) > 0.15) steer = Math.max(-1, Math.min(1, ax));
|
|
82
|
+
const rt = pad.buttons[7]?.value ?? 0;
|
|
83
|
+
const lt = pad.buttons[6]?.value ?? 0;
|
|
84
|
+
if (rt > 0.1 || pad.buttons[0]?.pressed) throttle = Math.max(throttle, rt > 0.1 ? rt : 1);
|
|
85
|
+
if (lt > 0.1 || pad.buttons[1]?.pressed) throttle = -Math.max(lt, 0.8);
|
|
86
|
+
if (pad.buttons[5]?.pressed || pad.buttons[2]?.pressed) drift = true;
|
|
87
|
+
if (pad.buttons[9]?.pressed) this.readyLatch = 6;
|
|
88
|
+
}
|
|
89
|
+
if (this.driftLatch > 0) {
|
|
90
|
+
drift = true;
|
|
91
|
+
this.driftLatch--;
|
|
92
|
+
}
|
|
93
|
+
let buttons = drift ? Button.Sprint : 0;
|
|
94
|
+
if (this.readyLatch > 0) {
|
|
95
|
+
buttons |= Button.Ready;
|
|
96
|
+
this.readyLatch--;
|
|
97
|
+
}
|
|
98
|
+
return { seq: 0, tick, moveX: steer, moveZ: throttle, yaw: 0, buttons };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
rumble(strength: number, ms: number): void {
|
|
102
|
+
const act = (this.pad as (Gamepad & { vibrationActuator?: { playEffect?: (t: string, o: object) => Promise<unknown> } }) | null)?.vibrationActuator;
|
|
103
|
+
void act?.playEffect?.("dual-rumble", { duration: ms, strongMagnitude: strength, weakMagnitude: strength * 0.6 }).catch(() => undefined);
|
|
104
|
+
}
|
|
105
|
+
}
|