2026-06-26 21:24:08 -04:00
|
|
|
import type { CSSProperties } from "react";
|
2026-06-26 20:54:48 -04:00
|
|
|
import { useEffect, useState } from "react";
|
2026-06-26 21:36:41 -04:00
|
|
|
import { openWorldshaperStudioWindow } from "./worldshaperStudio/windowing";
|
2026-06-26 20:54:48 -04:00
|
|
|
import {
|
2026-06-26 21:36:41 -04:00
|
|
|
CHANGELOG_SECTIONS,
|
|
|
|
|
CHANGELOG_SPLASH_FOOTNOTE,
|
|
|
|
|
CHANGELOG_SPLASH_KICKER,
|
|
|
|
|
CHANGELOG_SPLASH_TITLE,
|
|
|
|
|
CHANGELOG_SPLASH_VERSION,
|
|
|
|
|
} from "./worldshaperStudio/changelogData";
|
|
|
|
|
import type { ChangelogItem } from "./worldshaperStudio/changelogData";
|
2026-06-26 21:24:08 -04:00
|
|
|
import launcherBackground from "../background.png";
|
2026-06-26 20:54:48 -04:00
|
|
|
|
|
|
|
|
type WorldDefaultPayload = {
|
|
|
|
|
worldId?: string;
|
|
|
|
|
world?: {
|
|
|
|
|
id?: string;
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-26 21:36:41 -04:00
|
|
|
type LaunchState = "ready" | "opening" | "opened" | "blocked" | "error";
|
2026-06-26 20:54:48 -04:00
|
|
|
|
|
|
|
|
const DEFAULT_EDITOR_WORLD_ID_FALLBACK = "overworld";
|
|
|
|
|
|
|
|
|
|
async function resolveDefaultWorldId(): Promise<string> {
|
|
|
|
|
const response = await fetch("/api/world-default");
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(`Failed to load default world (${response.status}).`);
|
|
|
|
|
}
|
|
|
|
|
const payload = await response.json() as WorldDefaultPayload;
|
|
|
|
|
const resolvedWorldId = String(payload.worldId || payload.world?.id || "").trim();
|
|
|
|
|
return resolvedWorldId || DEFAULT_EDITOR_WORLD_ID_FALLBACK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openStudioPopup(worldId: string): boolean {
|
|
|
|
|
const popup = openWorldshaperStudioWindow(worldId, window, { worldId });
|
|
|
|
|
return Boolean(popup);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-26 21:12:44 -04:00
|
|
|
function openRepo(): void {
|
|
|
|
|
window.location.assign("https://repo.andraxion.net/");
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-26 20:54:48 -04:00
|
|
|
function WorldshaperLauncher() {
|
2026-06-26 21:36:41 -04:00
|
|
|
const [launchState, setLaunchState] = useState<LaunchState>("ready");
|
|
|
|
|
const [status, setStatus] = useState("Launch Worldshaper Studio in its floating window.");
|
2026-06-26 20:54:48 -04:00
|
|
|
const [error, setError] = useState("");
|
2026-06-26 21:36:41 -04:00
|
|
|
const [worldId, setWorldId] = useState(DEFAULT_EDITOR_WORLD_ID_FALLBACK);
|
2026-06-26 20:54:48 -04:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
let cancelled = false;
|
2026-06-26 21:36:41 -04:00
|
|
|
void resolveDefaultWorldId()
|
|
|
|
|
.then((resolvedWorldId) => {
|
2026-06-26 20:54:48 -04:00
|
|
|
if (cancelled) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
setWorldId(resolvedWorldId);
|
2026-06-26 21:36:41 -04:00
|
|
|
})
|
|
|
|
|
.catch(() => {
|
2026-06-26 20:54:48 -04:00
|
|
|
if (cancelled) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-06-26 21:36:41 -04:00
|
|
|
setWorldId(DEFAULT_EDITOR_WORLD_ID_FALLBACK);
|
|
|
|
|
});
|
2026-06-26 20:54:48 -04:00
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
cancelled = true;
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
|
|
|
|
|
2026-06-26 21:36:41 -04:00
|
|
|
async function handleLaunch(): Promise<void> {
|
2026-06-26 20:54:48 -04:00
|
|
|
setError("");
|
2026-06-26 21:36:41 -04:00
|
|
|
const nextWorldId = worldId || DEFAULT_EDITOR_WORLD_ID_FALLBACK;
|
|
|
|
|
setLaunchState("opening");
|
|
|
|
|
setStatus(`Opening Worldshaper Studio for ${nextWorldId}...`);
|
2026-06-26 20:54:48 -04:00
|
|
|
try {
|
2026-06-26 21:36:41 -04:00
|
|
|
const resolvedWorldId = nextWorldId || await resolveDefaultWorldId().catch(() => DEFAULT_EDITOR_WORLD_ID_FALLBACK);
|
2026-06-26 20:54:48 -04:00
|
|
|
setWorldId(resolvedWorldId);
|
|
|
|
|
setStatus(`Opening Worldshaper Studio for ${resolvedWorldId}...`);
|
|
|
|
|
if (openStudioPopup(resolvedWorldId)) {
|
|
|
|
|
setLaunchState("opened");
|
|
|
|
|
setStatus("Worldshaper Studio opened in a separate window.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
setLaunchState("blocked");
|
2026-06-26 21:36:41 -04:00
|
|
|
setStatus("Your browser blocked the studio window. Use the launch button again after allowing popups.");
|
2026-06-26 20:54:48 -04:00
|
|
|
} catch (nextError: unknown) {
|
|
|
|
|
const nextErrorText = String(nextError || "Failed to prepare Worldshaper Studio.");
|
|
|
|
|
setLaunchState("error");
|
|
|
|
|
setError(nextErrorText);
|
|
|
|
|
setStatus("Worldshaper Studio unavailable.");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-26 21:36:41 -04:00
|
|
|
const isBusy = launchState === "opening";
|
2026-06-26 20:54:48 -04:00
|
|
|
|
|
|
|
|
return (
|
2026-06-26 21:24:08 -04:00
|
|
|
<main
|
|
|
|
|
className="launcher-shell"
|
|
|
|
|
style={{ "--launcher-background-image": `url(${launcherBackground})` } as CSSProperties}
|
|
|
|
|
>
|
2026-06-26 21:36:41 -04:00
|
|
|
<div className="launcher-stack">
|
2026-06-26 22:01:43 -04:00
|
|
|
<section className="launcher-hero-window" aria-labelledby="launcher-studio-title">
|
|
|
|
|
<div className="launcher-changelog-titlebar">
|
|
|
|
|
<div className="launcher-changelog-title">Worldshaper Studio</div>
|
|
|
|
|
<div className="launcher-changelog-hint">Floating editor launch</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="launcher-hero-body">
|
|
|
|
|
<div className="launcher-hero-card">
|
|
|
|
|
<div className="changelog-splash-hero launcher-hero-banner">
|
|
|
|
|
<p className="launcher-eyebrow">New RPG</p>
|
|
|
|
|
<h1 className="launcher-title" id="launcher-studio-title">Worldshaper Studio</h1>
|
|
|
|
|
<p className="launcher-status">{status}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="launcher-hero-support">
|
|
|
|
|
{launchState === "blocked" ? (
|
|
|
|
|
<p className="launcher-hint">
|
|
|
|
|
Allow the popup, then use the studio button again to launch the floating editor window.
|
|
|
|
|
</p>
|
|
|
|
|
) : null}
|
|
|
|
|
{launchState === "opened" ? (
|
|
|
|
|
<p className="launcher-hint">
|
|
|
|
|
The studio is open in its own slim window. This page stays behind as your release board and relaunch point.
|
|
|
|
|
</p>
|
|
|
|
|
) : null}
|
|
|
|
|
{launchState === "ready" ? (
|
|
|
|
|
<p className="launcher-hint">
|
|
|
|
|
The editor is designed to live in its own floating window, so the launcher keeps the first step clean.
|
|
|
|
|
</p>
|
|
|
|
|
) : null}
|
|
|
|
|
{error ? <p className="launcher-error">{error}</p> : null}
|
|
|
|
|
<div className="launcher-actions">
|
|
|
|
|
<button type="button" className="launcher-primary-btn" onClick={() => void handleLaunch()} disabled={isBusy}>
|
|
|
|
|
Open Floating Studio
|
|
|
|
|
</button>
|
|
|
|
|
<button type="button" className="launcher-secondary-btn" onClick={openRepo} disabled={isBusy}>
|
|
|
|
|
Open Repo
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-06-26 20:54:48 -04:00
|
|
|
</div>
|
2026-06-26 21:36:41 -04:00
|
|
|
</section>
|
|
|
|
|
<section className="launcher-changelog-window" aria-labelledby="launcher-whats-new-title">
|
|
|
|
|
<div className="launcher-changelog-titlebar">
|
|
|
|
|
<div className="launcher-changelog-title">What's New</div>
|
|
|
|
|
<div className="launcher-changelog-hint">Current release highlights</div>
|
2026-06-26 21:19:33 -04:00
|
|
|
</div>
|
2026-06-26 21:36:41 -04:00
|
|
|
<div className="launcher-changelog-body">
|
|
|
|
|
<div className="changelog-splash-card">
|
|
|
|
|
<div className="changelog-splash-hero">
|
|
|
|
|
<div className="changelog-splash-kicker">{CHANGELOG_SPLASH_KICKER}</div>
|
|
|
|
|
<div className="changelog-splash-title" id="launcher-whats-new-title">{CHANGELOG_SPLASH_TITLE}</div>
|
|
|
|
|
<div className="changelog-splash-meta">Release {CHANGELOG_SPLASH_VERSION}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="changelog-splash-list">
|
|
|
|
|
{CHANGELOG_SECTIONS.map((section) => (
|
|
|
|
|
<section key={section.title} className="changelog-splash-section">
|
|
|
|
|
<h3 className="changelog-splash-section-title">{section.title}</h3>
|
|
|
|
|
<ul className="changelog-splash-bullets">
|
|
|
|
|
{section.items.map((item, index) => {
|
|
|
|
|
const key = `${section.title}-${index}`;
|
|
|
|
|
const normalizedItem: ChangelogItem = item;
|
|
|
|
|
if (typeof normalizedItem === "string") {
|
|
|
|
|
return <li key={key}>{normalizedItem}</li>;
|
|
|
|
|
}
|
|
|
|
|
return (
|
|
|
|
|
<li key={key}>
|
|
|
|
|
<div>{normalizedItem.text}</div>
|
|
|
|
|
{normalizedItem.note ? <div className="changelog-splash-bullet-note">{normalizedItem.note}</div> : null}
|
|
|
|
|
</li>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</ul>
|
|
|
|
|
</section>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="changelog-splash-footer">
|
|
|
|
|
<div className="changelog-splash-footnote">{CHANGELOG_SPLASH_FOOTNOTE}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-06-26 21:19:33 -04:00
|
|
|
</div>
|
|
|
|
|
</section>
|
2026-06-26 21:36:41 -04:00
|
|
|
</div>
|
2026-06-26 20:54:48 -04:00
|
|
|
</main>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default WorldshaperLauncher;
|