Deploy-base-aware asset URLs; live at partly.party/toastsim

Absolute '/assets/...' paths 404 under the /toastsim/ subpath (and the
root /assets/ URL is another game's — returns 200 with wrong content,
the classic partly.party trap). assetUrl() prefixes BASE_URL inside
loadProp + the four direct loads (bench/backdrop textures, title art,
judge portraits). Verified live: JS application/javascript, toaster.glb
4.5MB binary, title_art image/png, title screen renders.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-18 15:58:18 +10:00
parent d5ed20cacb
commit db31273fc9
4 changed files with 15 additions and 5 deletions

View File

@ -4,6 +4,13 @@ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const loader = new GLTFLoader(); const loader = new GLTFLoader();
const cache = new Map<string, Promise<THREE.Group>>(); const cache = new Map<string, Promise<THREE.Group>>();
/** Root-relative asset path → deploy-base-aware URL. Dev base is '/', the
* partly.party deploy serves from /toastsim/ absolute '/assets/...' paths
* silently 404 (or worse, hit another game's files) under a subpath. */
export function assetUrl(path: string): string {
return import.meta.env.BASE_URL.replace(/\/$/, '') + path;
}
/** /**
* Load a generated GLB and make it usable without touching the file. * Load a generated GLB and make it usable without touching the file.
* *
@ -17,7 +24,7 @@ export function loadProp(url: string, size: number): Promise<THREE.Group> {
const key = `${url}@${size}`; const key = `${url}@${size}`;
let p = cache.get(key); let p = cache.get(key);
if (!p) { if (!p) {
p = loader.loadAsync(url).then((gltf) => { p = loader.loadAsync(assetUrl(url)).then((gltf) => {
const root = gltf.scene; const root = gltf.scene;
const box = new THREE.Box3().setFromObject(root); const box = new THREE.Box3().setFromObject(root);
const dim = box.getSize(new THREE.Vector3()); const dim = box.getSize(new THREE.Vector3());

View File

@ -6,6 +6,7 @@ import type { Verdict } from '../game/judging';
import { judgeFace, verdictLines } from '../game/lines'; import { judgeFace, verdictLines } from '../game/lines';
import { TOOLS, type ToolId } from '../sim/cutlery'; import { TOOLS, type ToolId } from '../sim/cutlery';
import { clear, el, Panel } from '../ui/hud'; import { clear, el, Panel } from '../ui/hud';
import { assetUrl } from './assets';
/** /**
* The scorecard. This is the screen the game is actually about: every number * The scorecard. This is the screen the game is actually about: every number
@ -148,7 +149,7 @@ export class JudgeView implements View {
const lines = verdictLines(verdict, order, TOOLS[tool].name, rand); const lines = verdictLines(verdict, order, TOOLS[tool].name, rand);
for (const l of lines) el('p', undefined, this.linesEl, l); for (const l of lines) el('p', undefined, this.linesEl, l);
this.faceEl.src = `/assets/img/judge_${judgeFace(verdict.grade)}.png`; this.faceEl.src = assetUrl(`/assets/img/judge_${judgeFace(verdict.grade)}.png`);
this.stampEl.textContent = verdict.grade; this.stampEl.textContent = verdict.grade;
this.stampEl.className = `judge-stamp grade-${verdict.grade}`; this.stampEl.className = `judge-stamp grade-${verdict.grade}`;
// re-trigger the stamp animation // re-trigger the stamp animation

View File

@ -1,4 +1,5 @@
import * as THREE from 'three'; import * as THREE from 'three';
import { assetUrl } from './assets';
export function roundedRect(w: number, h: number, r: number): THREE.Shape { export function roundedRect(w: number, h: number, r: number): THREE.Shape {
const hw = w / 2; const hw = w / 2;
@ -169,7 +170,7 @@ export function makeBench(): THREE.Group {
const g = new THREE.Group(); const g = new THREE.Group();
const loader = new THREE.TextureLoader(); const loader = new THREE.TextureLoader();
const wood = loader.load('/assets/img/bench_wood.png'); const wood = loader.load(assetUrl('/assets/img/bench_wood.png'));
wood.wrapS = wood.wrapT = THREE.RepeatWrapping; wood.wrapS = wood.wrapT = THREE.RepeatWrapping;
wood.repeat.set(4, 2); wood.repeat.set(4, 2);
// The texture was generated as artwork, i.e. sRGB. Saying so is what keeps it // The texture was generated as artwork, i.e. sRGB. Saying so is what keeps it
@ -185,7 +186,7 @@ export function makeBench(): THREE.Group {
g.add(top); g.add(top);
// Splashback: a blurred kitchen so the scene has a horizon instead of fog. // Splashback: a blurred kitchen so the scene has a horizon instead of fog.
const back = loader.load('/assets/img/kitchen_backdrop.png'); const back = loader.load(assetUrl('/assets/img/kitchen_backdrop.png'));
back.colorSpace = THREE.SRGBColorSpace; back.colorSpace = THREE.SRGBColorSpace;
const wall = new THREE.Mesh( const wall = new THREE.Mesh(
new THREE.PlaneGeometry(22, 8), new THREE.PlaneGeometry(22, 8),

View File

@ -1,4 +1,5 @@
import { el, Panel } from './hud'; import { el, Panel } from './hud';
import { assetUrl } from '../scenes/assets';
/** The front door. One button, one piece of art, one joke. */ /** The front door. One button, one piece of art, one joke. */
export class Title { export class Title {
@ -8,7 +9,7 @@ export class Title {
this.panel = new Panel('title'); this.panel = new Panel('title');
const wrap = el('div', 'title-wrap', this.panel.root); const wrap = el('div', 'title-wrap', this.panel.root);
const art = el('img', 'title-art', wrap); const art = el('img', 'title-art', wrap);
art.src = '/assets/img/title_art.png'; art.src = assetUrl('/assets/img/title_art.png');
art.alt = ''; art.alt = '';
const txt = el('div', 'title-txt', wrap); const txt = el('div', 'title-txt', wrap);
el('h1', undefined, txt, 'TOASTSIM'); el('h1', undefined, txt, 'TOASTSIM');