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 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.
*
@ -17,7 +24,7 @@ export function loadProp(url: string, size: number): Promise<THREE.Group> {
const key = `${url}@${size}`;
let p = cache.get(key);
if (!p) {
p = loader.loadAsync(url).then((gltf) => {
p = loader.loadAsync(assetUrl(url)).then((gltf) => {
const root = gltf.scene;
const box = new THREE.Box3().setFromObject(root);
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 { TOOLS, type ToolId } from '../sim/cutlery';
import { clear, el, Panel } from '../ui/hud';
import { assetUrl } from './assets';
/**
* 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);
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.className = `judge-stamp grade-${verdict.grade}`;
// re-trigger the stamp animation

View File

@ -1,4 +1,5 @@
import * as THREE from 'three';
import { assetUrl } from './assets';
export function roundedRect(w: number, h: number, r: number): THREE.Shape {
const hw = w / 2;
@ -169,7 +170,7 @@ export function makeBench(): THREE.Group {
const g = new THREE.Group();
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.repeat.set(4, 2);
// 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);
// 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;
const wall = new THREE.Mesh(
new THREE.PlaneGeometry(22, 8),

View File

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