Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,86 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>webstrafe</title>
<style>
/* Instant boot loader — inline so it paints before the JS bundle and
style.css load. Themed to match the main menu (orange on near-black)
instead of the old blue flash. Removed by GameApp once the menu is up. */
#boot-loader {
position: fixed;
inset: 0;
z-index: 9999;
display: grid;
place-items: center;
background:
radial-gradient(120% 80% at 80% 8%, rgba(255, 106, 43, 0.12), transparent 55%),
radial-gradient(90% 70% at 0% 100%, rgba(255, 106, 43, 0.05), transparent 52%),
linear-gradient(180deg, #0a0d12, #050607);
font-family: 'Rajdhani', 'Segoe UI', system-ui, sans-serif;
transition: opacity 320ms ease;
}
#boot-loader.is-hiding {
opacity: 0;
}
.boot-inner {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
.boot-word {
font-weight: 700;
font-size: clamp(2.4rem, 6vw, 3.6rem);
letter-spacing: 0.02em;
text-transform: uppercase;
color: #f2f4f8;
line-height: 1;
}
.boot-word span {
color: #ff6a2b;
}
.boot-bar {
position: relative;
width: min(220px, 44vw);
height: 3px;
background: rgba(255, 106, 43, 0.16);
overflow: hidden;
}
.boot-bar::after {
content: '';
position: absolute;
left: -40%;
top: 0;
height: 100%;
width: 40%;
background: linear-gradient(90deg, transparent, #ff6a2b, transparent);
animation: boot-sweep 1.1s ease-in-out infinite;
}
.boot-tag {
font-family: 'Space Mono', ui-monospace, monospace;
font-size: 0.66rem;
letter-spacing: 0.42em;
text-transform: uppercase;
color: #7d8794;
}
@keyframes boot-sweep {
0% {
left: -40%;
}
100% {
left: 100%;
}
}
</style>
</head>
<body>
<div id="app"></div>
<div id="boot-loader" aria-hidden="true">
<div class="boot-inner">
<div class="boot-word">WEB<span>STRAFE</span></div>
<div class="boot-bar"></div>
<div class="boot-tag">Loading</div>
</div>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
Binary file modified public/playermodels/counterterrorist.glb
Binary file not shown.
Binary file modified public/playermodels/terrorist.glb
Binary file not shown.
Binary file modified public/viewmodels/knife/knife.glb
Binary file not shown.
Binary file modified public/viewmodels/knife/knife_animated.glb
Binary file not shown.
26 changes: 22 additions & 4 deletions src/app/GameApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ export class GameApp {
private localPlayerName = loadPlayerName();
private multiplayerSendAccumulator = 0;
private resumeToggleInFlight = false;
private remotePlayersReady: Promise<void> = Promise.resolve();

private readonly tmpForward = new Vector3();
private readonly tmpDesiredCameraPos = new Vector3();
Expand Down Expand Up @@ -220,12 +221,15 @@ export class GameApp {
listCustomMaps(),
this.cosmeticsManager.loadManifest(),
]);
try {
await this.remotePlayers.load();
} catch (error) {
// The remote player models (~75 MB of GLBs) are only needed once a match
// starts — not for the menu or its character preview, which loads its own
// hero model. Kick the load off in the background so the menu paints
// immediately instead of blocking the first render on 75 MB. Awaited before
// a play session actually enters a map (see startPlaySession).
this.remotePlayersReady = this.remotePlayers.load().catch((error) => {
// eslint-disable-next-line no-console
console.warn('[Multiplayer] Failed to load remote player models:', error);
}
});
this.rebuildMapSources(builtinMaps, customRecords);
this.selectedMapId =
builtinMaps.find((map) => map.id === 'surf_skyworld_x')?.id
Expand Down Expand Up @@ -269,6 +273,7 @@ export class GameApp {
savePlayerName(this.localPlayerName);
this.menu.setVisible(true);
this.setCrosshairVisible(false);
this.dismissBootLoader();

// Pick the transport: Supabase Realtime when configured (serverless deploy),
// else the self-hosted WebSocket client (local dev / LAN).
Expand Down Expand Up @@ -462,6 +467,9 @@ export class GameApp {
if (!this.menu) {
return;
}
// The remote player models were loaded in the background during init; make
// sure they're ready before we drop into a map so other players render.
await this.remotePlayersReady;
this.selectedMapId = mapId;
if (await this.tryResumeLoadedMap(mapId, 'Could not lock cursor. Press Esc or click Play to resume.')) {
return;
Expand Down Expand Up @@ -1471,6 +1479,16 @@ export class GameApp {
return el;
}

/** Fades out and removes the instant boot loader painted from index.html. */
private dismissBootLoader(): void {
const boot = document.getElementById('boot-loader');
if (!boot) {
return;
}
boot.classList.add('is-hiding');
window.setTimeout(() => boot.remove(), 360);
}

private createLoadingOverlay(): {
root: HTMLDivElement;
title: HTMLDivElement;
Expand Down
14 changes: 7 additions & 7 deletions src/multiplayer/playerRig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,14 @@ const KNIFE_MODEL_PATH = '/viewmodels/knife/knife.glb';
* Knife child-offset (relative to the right weapon-hand bone) for the static
* menu hero pose only. The menu closes the fingers into a fist, so the knife
* seats deeper in the palm / finger-curl pocket than the in-game default set in
* {@link attachKnifeModel}. It also slides the knife along its handle so the
* fist grips the balance point just below the guard (rather than pinching the
* butt/pommel end), which leaves far less bare wooden handle exposed between the
* fist and the guard and tucks the pommel behind the fist — reading as a proper
* knife-fight grip. Applied by {@link applyKnifeIdlePose} so the in-game
* third-person hold is never affected.
* {@link attachKnifeModel}. It slides the knife up its handle so the fist grips
* right at the guard (leaving almost no bare wooden handle exposed between the
* fist and the guard) and seats it up into the palm so the pommel tucks under
* the fist rather than dangling below it — reading as a proper hammer-grip
* knife-fight hold from the third-person menu camera. Applied by
* {@link applyKnifeIdlePose} so the in-game third-person hold is never affected.
*/
const MENU_KNIFE_GRIP_POSITION = new Vector3(0.0409, 0.0063, 0.0289);
const MENU_KNIFE_GRIP_POSITION = new Vector3(0.0228, 0.0098, 0.0252);

const offsetQuat = new Quaternion();
const offsetEuler = new Euler(0, 0, 0, 'XYZ');
Expand Down
20 changes: 10 additions & 10 deletions src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ body,
margin: 0;
overflow: hidden;
background:
radial-gradient(1200px 800px at 15% 20%, rgba(89, 224, 195, 0.1), transparent 55%),
radial-gradient(900px 700px at 80% 15%, rgba(255, 194, 109, 0.1), transparent 55%),
linear-gradient(180deg, var(--bg-2), var(--bg-0));
radial-gradient(120% 80% at 80% 8%, rgba(255, 106, 43, 0.1), transparent 55%),
radial-gradient(90% 70% at 0% 100%, rgba(255, 106, 43, 0.05), transparent 52%),
linear-gradient(180deg, #0a0d12, #050607);
color: var(--ink-0);
font-family: 'Rajdhani', sans-serif;
}
Expand Down Expand Up @@ -700,7 +700,7 @@ canvas {
place-items: center;
padding: 20px;
pointer-events: none;
background: rgba(5, 11, 18, 0.7);
background: rgba(5, 6, 7, 0.72);
backdrop-filter: blur(8px);
}

Expand All @@ -709,31 +709,31 @@ canvas {
max-height: 86vh;
overflow: auto;
border-radius: 12px;
border: 1px solid rgba(160, 201, 223, 0.4);
background: rgba(10, 24, 34, 0.92);
box-shadow: 0 18px 36px rgba(0, 0, 0, 0.35);
border: 1px solid rgba(255, 106, 43, 0.32);
background: rgba(12, 10, 9, 0.94);
box-shadow: 0 18px 36px rgba(0, 0, 0, 0.45);
padding: 16px 18px;
font-family: 'Space Mono', monospace;
}

.loading-title {
font-size: 16px;
font-weight: 700;
color: #e9f6ff;
color: #f4f6fa;
}

.loading-progress {
margin-top: 6px;
margin-bottom: 10px;
color: var(--accent);
color: #ff6a2b;
font-size: 14px;
}

.loading-detail {
margin: 0;
white-space: pre-wrap;
line-height: 1.35;
color: #c5d9e8;
color: #b9a99f;
font-size: 12px;
}

Expand Down
Loading