diff --git a/frontend/index.html b/frontend/index.html index c1e5d20..1c458e5 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -13,7 +13,7 @@ type="font/woff2" crossorigin /> - hyprverse — step into the metaverse + hyprverse
diff --git a/frontend/src/App.css b/frontend/src/App.css index ce446fe..bbc50d8 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -411,13 +411,6 @@ html, body, #root { .console-submit:disabled { opacity: 0.6; cursor: default; } .console-submit-arrow { transition: transform 0.18s ease; } .console-submit:hover:not(:disabled) .console-submit-arrow { transform: translateX(3px); } -.console-foot { - margin: 14px 0 0; - font-size: 12px; - font-weight: 600; - text-align: center; - color: var(--hv-muted); -} /* Shared keyboard-focus ring for every interactive landing control. */ .landing button:focus-visible, diff --git a/frontend/src/game/boardTable.test.ts b/frontend/src/game/boardTable.test.ts index f4234eb..f4e1548 100644 --- a/frontend/src/game/boardTable.test.ts +++ b/frontend/src/game/boardTable.test.ts @@ -239,7 +239,7 @@ describe("boardTableView — endings", () => { seats: [null, { ...bob, accepted: true }], state: { board: board9(), turn: 1, result: { status: "in_progress" } }, }); - expect(boardTableView(forfeit, "b").status).toBe("Opponent left — you win!"); + expect(boardTableView(forfeit, "b").status).toBe("Opponent left. You win!"); // The forfeiter's seat is emptied, so they (and any spectator) see the same. expect(boardTableView(forfeit, "a").status).toBe("Bob wins by forfeit"); expect(boardTableView(forfeit, "spectator").status).toBe("Bob wins by forfeit"); diff --git a/frontend/src/game/boardTable.ts b/frontend/src/game/boardTable.ts index 88fde14..63a79b4 100644 --- a/frontend/src/game/boardTable.ts +++ b/frontend/src/game/boardTable.ts @@ -160,7 +160,7 @@ function statusFor(snapshot: BoardUpdatePayload, mySeat: 0 | 1 | null): string { const result = snapshot.state?.result; if (snapshot.reason === "forfeit") { // Forfeit empties the leaver's seat, so anyone still seated is the winner. - if (mySeat !== null) return "Opponent left — you win!"; + if (mySeat !== null) return "Opponent left. You win!"; const winnerSeat = snapshot.seats[0] ? 0 : snapshot.seats[1] ? 1 : null; return `${winnerSeat !== null ? names[winnerSeat] ?? "Player" : "A player"} wins by forfeit`; } diff --git a/frontend/src/game/chars.ts b/frontend/src/game/chars.ts index 3bd7198..c869f62 100644 --- a/frontend/src/game/chars.ts +++ b/frontend/src/game/chars.ts @@ -5,21 +5,17 @@ * * Add a new entry here + drop the PNG in public/assets/characters/ to * introduce a new avatar — no other code changes needed. + * + * char5/char7/char9 were removed as visual duplicates of char3/char4/char1; + * PNGs remain on disk so old localStorage values still resolve via aliases. */ export const CHARS = [ - // Original four (Penzilla-style interior characters) "char1", "char2", "char3", "char4", - // Expanded roster: Pipoya FREE RPG Character Sprites 32x32 - // Female 01, 02, 05, 07 - "char5", "char6", - "char7", "char8", - // Male 01, 02, 06, 08 - "char9", "char10", "char11", "char12", @@ -27,10 +23,26 @@ export const CHARS = [ export type CharKey = (typeof CHARS)[number]; +/** Legacy picker keys that were visual duplicates of a keeper. */ +const CHAR_ALIASES: Readonly> = { + char5: "char3", + char7: "char4", + char9: "char1", +}; + export function isCharKey(v: string): v is CharKey { return (CHARS as readonly string[]).includes(v); } +/** + * Map a stored or override avatar key onto a selectable sprite. Returns null + * for unknown keys so callers can fall back (e.g. charForPlayer). + */ +export function resolveCharKey(v: string): CharKey | null { + if (isCharKey(v)) return v; + return CHAR_ALIASES[v] ?? null; +} + function hash(s: string): number { let h = 0; for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0; diff --git a/frontend/src/game/chatCommands.ts b/frontend/src/game/chatCommands.ts index 38a8a0f..87501d1 100644 --- a/frontend/src/game/chatCommands.ts +++ b/frontend/src/game/chatCommands.ts @@ -31,6 +31,6 @@ export function commandInsertion(command: ChatCommand): string { export const CHAT_HELP = [ "Commands:", - ...CHAT_COMMANDS.map((command) => `${command.usage} — ${command.description}`), + ...CHAT_COMMANDS.map((command) => `${command.usage}: ${command.description}`), "Enter or T focuses chat · Esc returns to the game", ] as const; diff --git a/frontend/src/game/chatCooldown.test.ts b/frontend/src/game/chatCooldown.test.ts index 4d6f264..0f20815 100644 --- a/frontend/src/game/chatCooldown.test.ts +++ b/frontend/src/game/chatCooldown.test.ts @@ -18,8 +18,8 @@ describe("cooldownRetrySeconds", () => { describe("chatCooldownNotice", () => { it("names the whole-second wait derived from retryAfterMs", () => { - expect(chatCooldownNotice(4000)).toBe("You're sending messages too fast — wait 4s."); - expect(chatCooldownNotice(3200)).toBe("You're sending messages too fast — wait 4s."); - expect(chatCooldownNotice(0)).toBe("You're sending messages too fast — wait 1s."); + expect(chatCooldownNotice(4000)).toBe("You're sending messages too fast. Wait 4s."); + expect(chatCooldownNotice(3200)).toBe("You're sending messages too fast. Wait 4s."); + expect(chatCooldownNotice(0)).toBe("You're sending messages too fast. Wait 1s."); }); }); diff --git a/frontend/src/game/chatCooldown.ts b/frontend/src/game/chatCooldown.ts index 6f85655..7025a81 100644 --- a/frontend/src/game/chatCooldown.ts +++ b/frontend/src/game/chatCooldown.ts @@ -18,5 +18,5 @@ export function cooldownRetrySeconds(retryAfterMs: number): number { /** The system line shown in a chat transcript when a send is rate-limited. */ export function chatCooldownNotice(retryAfterMs: number): string { - return `You're sending messages too fast — wait ${cooldownRetrySeconds(retryAfterMs)}s.`; + return `You're sending messages too fast. Wait ${cooldownRetrySeconds(retryAfterMs)}s.`; } diff --git a/frontend/src/game/controlBar.test.ts b/frontend/src/game/controlBar.test.ts index d2ed7d5..e184776 100644 --- a/frontend/src/game/controlBar.test.ts +++ b/frontend/src/game/controlBar.test.ts @@ -3,10 +3,10 @@ import { mediaFailureText } from "./controlBar"; describe("bounded media-failure copy (PRD 25.7)", () => { it.each([ - ["mic", "denied", "Microphone blocked — allow access in your browser"], + ["mic", "denied", "Microphone blocked: allow access in your browser"], ["mic", "unavailable", "No microphone found"], ["mic", "failed", "Couldn't turn on the microphone"], - ["cam", "denied", "Camera blocked — allow access in your browser"], + ["cam", "denied", "Camera blocked: allow access in your browser"], ["cam", "unavailable", "No camera found"], ["cam", "failed", "Couldn't turn on the camera"], ] as const)("%s %s -> %s", (device, failure, text) => { diff --git a/frontend/src/game/controlBar.ts b/frontend/src/game/controlBar.ts index d0e633f..60bed09 100644 --- a/frontend/src/game/controlBar.ts +++ b/frontend/src/game/controlBar.ts @@ -11,7 +11,7 @@ export function mediaFailureText( const noun = device === "mic" ? "Microphone" : "Camera"; switch (failure) { case "denied": - return `${noun} blocked — allow access in your browser`; + return `${noun} blocked: allow access in your browser`; case "unavailable": return device === "mic" ? "No microphone found" : "No camera found"; case "failed": diff --git a/frontend/src/game/modPanel.ts b/frontend/src/game/modPanel.ts index 95df5c9..020adb6 100644 --- a/frontend/src/game/modPanel.ts +++ b/frontend/src/game/modPanel.ts @@ -160,15 +160,15 @@ export type ModErrorCode = export function moderationErrorText(code: ModErrorCode): string { switch (code) { case "validation": - return "That request was rejected — check the details and try again."; + return "That request was rejected. Check the details and try again."; case "invalid-until": return "Pick a suspension end time in the future."; case "target-not-found": return "That user no longer exists."; case "not-found": - return "That report is no longer available — refresh the list."; + return "That report is no longer available. Refresh the list."; case "rate-limited": - return "Too many moderation actions — wait a moment and try again."; + return "Too many moderation actions. Wait a moment and try again."; case "unauthorized": return "Your session is no longer authorized for moderation."; case "network": diff --git a/frontend/src/game/report.ts b/frontend/src/game/report.ts index ec91bc7..1195fe4 100644 --- a/frontend/src/game/report.ts +++ b/frontend/src/game/report.ts @@ -30,11 +30,11 @@ function errorNotice(code: ReportErrorCode): string { case "message-not-found": return "That message is too old to report."; case "rate-limited": - return "You're reporting too fast — wait a moment and try again."; + return "You're reporting too fast. Wait a moment and try again."; case "unauthorized": return "Please sign in again to report."; case "network": - return "Couldn't reach the server — check your connection and try again."; + return "Couldn't reach the server. Check your connection and try again."; case "unknown": default: return "Couldn't send your report. Please try again."; @@ -45,6 +45,6 @@ function errorNotice(code: ReportErrorCode): string { export function reportResultNotice(result: ReportResult): string { if (!result.ok) return errorNotice(result.code); return result.status === "duplicate" - ? "You've already reported that message — moderators have it." - : "Thanks — your report was sent to the moderators."; + ? "You've already reported that message. Moderators have it." + : "Thanks. Your report was sent to the moderators."; } diff --git a/frontend/src/game/roomAccess.ts b/frontend/src/game/roomAccess.ts index 75c191f..858fa1e 100644 --- a/frontend/src/game/roomAccess.ts +++ b/frontend/src/game/roomAccess.ts @@ -83,7 +83,7 @@ export const CAPACITY_MESSAGE = "This room is at max capacity."; export function knockResultMessage(result: "denied" | "timeout" | "too-far"): string { switch (result) { case "timeout": - return "No answer — nobody let you in."; + return "No answer. Nobody let you in."; case "too-far": return "Step up to the door to knock."; default: diff --git a/frontend/src/game/scenes/WorldScene.ts b/frontend/src/game/scenes/WorldScene.ts index f8c5132..d5cc67e 100644 --- a/frontend/src/game/scenes/WorldScene.ts +++ b/frontend/src/game/scenes/WorldScene.ts @@ -11,7 +11,7 @@ import type { Net } from "../../net/net"; import { authToken } from "../../net/auth"; import { bus } from "../eventBus"; import { createCharAnims, idleFrame, walkAnim } from "../avatar"; -import { CHARS, charForPlayer, isCharKey } from "../chars"; +import { CHARS, charForPlayer, resolveCharKey } from "../chars"; import { activeMap } from "../maps"; import { parseInteractables, findNear, arcadeOpenPayload, type InteractableDef } from "../interactables"; import { movementIntent, BASE_SPEED } from "../movement"; @@ -227,7 +227,7 @@ export default class WorldScene extends Phaser.Scene { }); const saved = localStorage.getItem("avatar"); - this.avatar = saved && isCharKey(saved) ? saved : "char1"; + this.avatar = saved ? (resolveCharKey(saved) ?? "char1") : "char1"; const mapKey = activeMap().key; const map = this.make.tilemap({ key: mapKey }); diff --git a/frontend/src/net/auth.ts b/frontend/src/net/auth.ts index 0346389..0d14125 100644 --- a/frontend/src/net/auth.ts +++ b/frontend/src/net/auth.ts @@ -50,9 +50,9 @@ function failureMessage(failure: AuthFailureResponse | null): string { case "validation": return "Check the username and password requirements, then try again."; case "username-taken": - return "That username is taken — try signing in instead."; + return "That username is taken. Try signing in instead."; case "invalid-credentials": - return "Sign in failed — check your username and password."; + return "Sign in failed. Check your username and password."; case "rate-limited": return `Too many attempts. Try again in ${failure.retryAfterSeconds} seconds.`; case "server-error": diff --git a/frontend/src/net/config.ts b/frontend/src/net/config.ts index 98308fd..21414aa 100644 --- a/frontend/src/net/config.ts +++ b/frontend/src/net/config.ts @@ -25,7 +25,7 @@ export const MISCONFIGURED = !USE_MOCK && !SERVER_URL; export function assertServerUrl(): string { if (!SERVER_URL) throw new Error( - "VITE_SERVER_URL is required — production builds must point at the backend " + + "VITE_SERVER_URL is required: production builds must point at the backend " + "(mock mode is development-only)." ); return SERVER_URL; diff --git a/frontend/src/ui/ArrivalPanel.tsx b/frontend/src/ui/ArrivalPanel.tsx index d1315f3..f63dd8b 100644 --- a/frontend/src/ui/ArrivalPanel.tsx +++ b/frontend/src/ui/ArrivalPanel.tsx @@ -77,7 +77,7 @@ export default function ArrivalPanel() { className="arrival-head" onClick={() => setOpen((o) => !o)} aria-expanded={open} - aria-label={headerCount === undefined ? "Who's around" : `Who's around — ${headerCount} online`} + aria-label={headerCount === undefined ? "Who's around" : `Who's around: ${headerCount} online`} >