Refresh launcher release presentation

This commit is contained in:
Andraxion 2026-06-26 21:36:41 -04:00
parent b632e98ad1
commit c063041efc
4 changed files with 267 additions and 285 deletions

View file

@ -1,10 +1,14 @@
import type { CSSProperties } from "react";
import { useEffect, useState } from "react";
import { openWorldshaperStudioWindow } from "./worldshaperStudio/windowing";
import {
buildWorldshaperStudioUrl,
openWorldshaperStudioWindow,
} from "./worldshaperStudio/windowing";
import { CHANGELOG_SECTIONS, CHANGELOG_SPLASH_VERSION } from "./worldshaperStudio/changelogData";
CHANGELOG_SECTIONS,
CHANGELOG_SPLASH_FOOTNOTE,
CHANGELOG_SPLASH_KICKER,
CHANGELOG_SPLASH_TITLE,
CHANGELOG_SPLASH_VERSION,
} from "./worldshaperStudio/changelogData";
import type { ChangelogItem } from "./worldshaperStudio/changelogData";
import launcherBackground from "../background.png";
type WorldDefaultPayload = {
@ -14,16 +18,10 @@ type WorldDefaultPayload = {
};
};
type LaunchState = "resolving" | "opening" | "opened" | "blocked" | "error";
type LaunchState = "ready" | "opening" | "opened" | "blocked" | "error";
const DEFAULT_EDITOR_WORLD_ID_FALLBACK = "overworld";
let didAttemptInitialLaunch = false;
let rememberedLaunchState: LaunchState | "" = "";
let rememberedStatus = "";
let rememberedError = "";
let rememberedWorldId = "";
async function resolveDefaultWorldId(): Promise<string> {
const response = await fetch("/api/world-default");
if (!response.ok) {
@ -39,216 +37,125 @@ function openStudioPopup(worldId: string): boolean {
return Boolean(popup);
}
function openStudioInCurrentTab(worldId: string): void {
window.location.assign(buildWorldshaperStudioUrl(worldId, window, { worldId }));
}
function openRepo(): void {
window.location.assign("https://repo.andraxion.net/");
}
function WorldshaperLauncher() {
const [launchState, setLaunchState] = useState<LaunchState>("resolving");
const [status, setStatus] = useState("Preparing Worldshaper Studio...");
const [launchState, setLaunchState] = useState<LaunchState>("ready");
const [status, setStatus] = useState("Launch Worldshaper Studio in its floating window.");
const [error, setError] = useState("");
const [worldId, setWorldId] = useState("");
const [worldId, setWorldId] = useState(DEFAULT_EDITOR_WORLD_ID_FALLBACK);
useEffect(() => {
let cancelled = false;
async function attemptInitialLaunch() {
if (didAttemptInitialLaunch) {
setWorldId(rememberedWorldId);
setLaunchState(rememberedLaunchState || "blocked");
setStatus(rememberedStatus || "Open Worldshaper Studio.");
setError(rememberedError);
return;
}
didAttemptInitialLaunch = true;
setLaunchState("resolving");
setError("");
setStatus("Preparing Worldshaper Studio...");
try {
const resolvedWorldId = await resolveDefaultWorldId().catch(() => DEFAULT_EDITOR_WORLD_ID_FALLBACK);
void resolveDefaultWorldId()
.then((resolvedWorldId) => {
if (cancelled) {
return;
}
setWorldId(resolvedWorldId);
rememberedWorldId = resolvedWorldId;
setLaunchState("opening");
setStatus(`Opening Worldshaper Studio for ${resolvedWorldId}...`);
if (openStudioPopup(resolvedWorldId)) {
setLaunchState("opened");
setStatus("Worldshaper Studio opened in a separate window.");
rememberedLaunchState = "opened";
rememberedStatus = "Worldshaper Studio opened in a separate window.";
rememberedError = "";
return;
}
setLaunchState("blocked");
setStatus("Your browser blocked the studio window.");
rememberedLaunchState = "blocked";
rememberedStatus = "Your browser blocked the studio window.";
rememberedError = "";
} catch (nextError: unknown) {
})
.catch(() => {
if (cancelled) {
return;
}
const nextErrorText = String(nextError || "Failed to prepare Worldshaper Studio.");
setLaunchState("error");
setError(nextErrorText);
setStatus("Worldshaper Studio unavailable.");
rememberedLaunchState = "error";
rememberedStatus = "Worldshaper Studio unavailable.";
rememberedError = nextErrorText;
}
}
void attemptInitialLaunch();
setWorldId(DEFAULT_EDITOR_WORLD_ID_FALLBACK);
});
return () => {
cancelled = true;
};
}, []);
useEffect(() => {
if (launchState !== "blocked" || !worldId) {
return;
}
function retryLaunch() {
if (!worldId) {
return;
}
setLaunchState("opening");
setStatus(`Opening Worldshaper Studio for ${worldId}...`);
if (openStudioPopup(worldId)) {
setLaunchState("opened");
setStatus("Worldshaper Studio opened in a separate window.");
rememberedLaunchState = "opened";
rememberedStatus = "Worldshaper Studio opened in a separate window.";
rememberedError = "";
return;
}
setLaunchState("blocked");
setStatus("Your browser blocked the studio window.");
rememberedLaunchState = "blocked";
rememberedStatus = "Your browser blocked the studio window.";
rememberedError = "";
}
function handlePointerDown() {
retryLaunch();
}
function handleKeyDown() {
retryLaunch();
}
window.addEventListener("pointerdown", handlePointerDown, { once: true });
window.addEventListener("keydown", handleKeyDown, { once: true });
return () => {
window.removeEventListener("pointerdown", handlePointerDown);
window.removeEventListener("keydown", handleKeyDown);
};
}, [launchState, worldId]);
async function handleRetry(): Promise<void> {
async function handleLaunch(): Promise<void> {
setError("");
setLaunchState("resolving");
setStatus("Preparing Worldshaper Studio...");
try {
const resolvedWorldId = worldId || await resolveDefaultWorldId().catch(() => DEFAULT_EDITOR_WORLD_ID_FALLBACK);
setWorldId(resolvedWorldId);
rememberedWorldId = resolvedWorldId;
const nextWorldId = worldId || DEFAULT_EDITOR_WORLD_ID_FALLBACK;
setLaunchState("opening");
setStatus(`Opening Worldshaper Studio for ${nextWorldId}...`);
try {
const resolvedWorldId = nextWorldId || await resolveDefaultWorldId().catch(() => DEFAULT_EDITOR_WORLD_ID_FALLBACK);
setWorldId(resolvedWorldId);
setStatus(`Opening Worldshaper Studio for ${resolvedWorldId}...`);
if (openStudioPopup(resolvedWorldId)) {
setLaunchState("opened");
setStatus("Worldshaper Studio opened in a separate window.");
rememberedLaunchState = "opened";
rememberedStatus = "Worldshaper Studio opened in a separate window.";
rememberedError = "";
return;
}
setLaunchState("blocked");
setStatus("Your browser blocked the studio window.");
rememberedLaunchState = "blocked";
rememberedStatus = "Your browser blocked the studio window.";
rememberedError = "";
setStatus("Your browser blocked the studio window. Use the launch button again after allowing popups.");
} catch (nextError: unknown) {
const nextErrorText = String(nextError || "Failed to prepare Worldshaper Studio.");
setLaunchState("error");
setError(nextErrorText);
setStatus("Worldshaper Studio unavailable.");
rememberedLaunchState = "error";
rememberedStatus = "Worldshaper Studio unavailable.";
rememberedError = nextErrorText;
}
}
function handleOpenHere(): void {
const nextWorldId = worldId || DEFAULT_EDITOR_WORLD_ID_FALLBACK;
openStudioInCurrentTab(nextWorldId);
}
const isBusy = launchState === "resolving" || launchState === "opening";
const showFallbackActions = launchState === "blocked" || launchState === "opened" || launchState === "error";
const isBusy = launchState === "opening";
return (
<main
className="launcher-shell"
style={{ "--launcher-background-image": `url(${launcherBackground})` } as CSSProperties}
>
<div className="launcher-stack">
<section className="launcher-card">
<p className="launcher-eyebrow">New RPG</p>
<h1 className="launcher-title">Worldshaper Studio</h1>
<p className="launcher-status">{status}</p>
{launchState === "blocked" ? (
<p className="launcher-hint">
Click anywhere or use the button below if your browser asks for permission to open the studio window.
Allow the popup, then use the studio button again to launch the floating editor window.
</p>
) : null}
{launchState === "opened" ? (
<p className="launcher-hint">
The editor is running in its own window. You can leave this tab open as a quick relaunch point, or close it.
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}
{showFallbackActions ? (
<div className="launcher-actions">
<button type="button" className="launcher-primary-btn" onClick={() => void handleRetry()} disabled={isBusy}>
<button type="button" className="launcher-primary-btn" onClick={() => void handleLaunch()} disabled={isBusy}>
Open Floating Studio
</button>
<button type="button" className="launcher-secondary-btn" onClick={handleOpenHere} disabled={isBusy}>
Open In This Tab
</button>
<button type="button" className="launcher-secondary-btn" onClick={openRepo} disabled={isBusy}>
Open Repo
</button>
</div>
) : null}
<section className="launcher-whats-new">
<div className="launcher-whats-new-head">
<h2 className="launcher-whats-new-title">What&apos;s New</h2>
<p className="launcher-whats-new-version">{CHANGELOG_SPLASH_VERSION}</p>
</section>
<section className="launcher-changelog-window" aria-labelledby="launcher-whats-new-title">
<div className="launcher-changelog-titlebar">
<div className="launcher-changelog-title">What&apos;s New</div>
<div className="launcher-changelog-hint">Current release highlights</div>
</div>
<div className="launcher-whats-new-list">
<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="launcher-whats-new-section">
<h3 className="launcher-whats-new-section-title">{section.title}</h3>
<ul className="launcher-whats-new-bullets">
<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}`;
if (typeof item === "string") {
return <li key={key}>{item}</li>;
const normalizedItem: ChangelogItem = item;
if (typeof normalizedItem === "string") {
return <li key={key}>{normalizedItem}</li>;
}
return (
<li key={key}>
<div>{item.text}</div>
{item.note ? <div className="launcher-whats-new-note">{item.note}</div> : null}
<div>{normalizedItem.text}</div>
{normalizedItem.note ? <div className="changelog-splash-bullet-note">{normalizedItem.note}</div> : null}
</li>
);
})}
@ -256,8 +163,13 @@ function WorldshaperLauncher() {
</section>
))}
</div>
<div className="changelog-splash-footer">
<div className="changelog-splash-footnote">{CHANGELOG_SPLASH_FOOTNOTE}</div>
</div>
</div>
</div>
</section>
</section>
</div>
</main>
);
}

View file

@ -32,8 +32,8 @@ body {
isolation: isolate;
overflow: hidden;
min-height: 100dvh;
display: grid;
place-items: center;
display: flex;
justify-content: center;
padding: 48px 24px;
}
@ -63,6 +63,12 @@ body {
linear-gradient(180deg, rgba(8, 11, 17, 0.36) 0%, rgba(8, 11, 17, 0.52) 36%, rgba(7, 10, 15, 0.84) 72%, rgba(5, 7, 11, 0.96) 100%);
}
.launcher-stack {
width: min(760px, 100%);
display: grid;
gap: 18px;
}
.launcher-card {
width: min(620px, 100%);
padding: 28px 30px;
@ -119,66 +125,143 @@ body {
margin-top: 18px;
}
.launcher-whats-new {
margin-top: 24px;
padding-top: 18px;
border-top: 1px solid rgba(120, 170, 230, 0.16);
.launcher-changelog-window {
width: min(760px, 100%);
border: 1px solid #4f79af;
border-radius: 12px;
background: color-mix(in srgb, #0a1020 88%, transparent);
color: #d8e8ff;
box-shadow: 0 16px 34px rgba(3, 8, 18, 0.46);
overflow: hidden;
display: grid;
grid-template-rows: 34px minmax(0, 1fr);
backdrop-filter: blur(10px);
}
.launcher-whats-new-head {
.launcher-changelog-titlebar {
display: flex;
flex-wrap: wrap;
align-items: baseline;
justify-content: space-between;
gap: 10px;
margin-bottom: 14px;
align-items: center;
gap: 8px;
padding: 0 10px;
background: linear-gradient(180deg, #1b365e 0%, #122743 100%);
border-bottom: 1px solid #365782;
user-select: none;
}
.launcher-whats-new-title {
margin: 0;
font-size: 1.1rem;
.launcher-changelog-title {
min-width: 0;
flex: 1 1 auto;
font-size: 12px;
font-weight: 800;
color: #eef6ff;
}
.launcher-whats-new-version {
margin: 0;
color: #9fd8ff;
font-size: 0.78rem;
letter-spacing: 0.08em;
.launcher-changelog-hint {
font-size: 11px;
color: #a9c2ec;
white-space: nowrap;
}
.launcher-changelog-body {
min-height: 0;
max-height: min(52dvh, 720px);
padding: 12px;
overflow: auto;
}
.changelog-splash-card {
min-height: 100%;
display: grid;
grid-template-rows: auto minmax(0, 1fr) auto;
gap: 12px;
}
.changelog-splash-hero {
padding: 14px 16px;
border: 1px solid #4f79af;
border-radius: 12px;
background:
radial-gradient(circle at top right, rgba(100, 170, 248, 0.24) 0%, transparent 48%),
linear-gradient(180deg, rgba(27, 54, 94, 0.64) 0%, rgba(17, 32, 63, 0.82) 100%);
box-shadow: inset 0 0 0 1px rgba(10, 16, 32, 0.22);
}
.changelog-splash-kicker {
color: #ffd166;
font-size: 10px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 6px;
}
.launcher-whats-new-list {
display: grid;
gap: 14px;
.changelog-splash-title {
color: #eef6ff;
font-size: 22px;
font-weight: 800;
line-height: 1.1;
margin-bottom: 6px;
}
.launcher-whats-new-section {
display: grid;
gap: 8px;
}
.launcher-whats-new-section-title {
margin: 0;
font-size: 0.92rem;
color: #dff2ff;
}
.launcher-whats-new-bullets {
margin: 0;
padding-left: 18px;
display: grid;
gap: 8px;
color: #d5dfec;
}
.launcher-whats-new-bullets li {
.changelog-splash-meta {
color: #a9c2ec;
font-size: 12px;
line-height: 1.45;
}
.launcher-whats-new-note {
.changelog-splash-list {
min-height: 0;
overflow: auto;
display: grid;
gap: 10px;
padding-right: 4px;
}
.changelog-splash-section {
padding: 12px 14px;
border: 1px solid #365782;
border-radius: 12px;
background: rgba(17, 32, 63, 0.84);
box-shadow: inset 0 0 0 1px rgba(10, 16, 32, 0.14);
}
.changelog-splash-section-title {
margin: 0 0 8px;
color: #eef6ff;
font-size: 13px;
font-weight: 800;
letter-spacing: 0.01em;
}
.changelog-splash-bullets {
margin: 0;
padding-left: 18px;
display: grid;
gap: 5px;
color: #d7e7ff;
font-size: 12px;
line-height: 1.45;
}
.changelog-splash-bullet-note {
margin-top: 3px;
color: #9bacbe;
font-size: 0.88rem;
color: #a9c2ec;
font-size: 11px;
line-height: 1.4;
font-style: italic;
}
.changelog-splash-footer {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 10px;
align-items: center;
}
.changelog-splash-footnote {
color: #9fb8e5;
font-size: 11px;
line-height: 1.4;
}
.launcher-primary-btn,
@ -1306,6 +1389,12 @@ button.danger:not(:disabled):hover {
filter: saturate(0.82) blur(1.5px);
}
.launcher-stack,
.launcher-card,
.launcher-changelog-window {
width: 100%;
}
.launcher-card {
padding: 22px 20px;
border-radius: 16px;
@ -1315,16 +1404,15 @@ button.danger:not(:disabled):hover {
flex-direction: column;
}
.launcher-whats-new-head {
flex-direction: column;
align-items: flex-start;
}
.launcher-primary-btn,
.launcher-secondary-btn {
width: 100%;
}
.launcher-changelog-body {
max-height: none;
}
.page-shell {
width: 96vw;
margin: 1rem auto;

View file

@ -3,61 +3,37 @@ export type ChangelogItem = string | {
note?: string;
};
export const CHANGELOG_SPLASH_VERSION = "2026-06-22-world-editor-release-v6";
export const CHANGELOG_SECTIONS = [
{
title: "World Rendering",
items: [
"Added live image opacity support in the renderer for both tiles and entity sprites.",
{
text: "Fixed world painting placement drift on very wide displays.",
note: "For the many of you out there using this on superultrawide monitors.",
},
],
},
{
title: "Graphics & Animation",
items: [
"Added animation frame timelines to the Graphic Painter.",
"Added frame duplication, enable/disable, delete, drag reorder, and default-frame selection.",
"Added animation speed and playback settings to graphics data.",
"Added animation preview support from the Graphic Painter.",
],
},
{
title: "World Overview",
items: [
"Added chunk move, duplicate, rotate, flip, and delete workflows from the world overview.",
"Added safer chunk management feedback and confirmation flows.",
],
},
{
title: "Editor Stability",
items: [
"Fixed unified graphics conversion so saved image properties flow correctly into runtime tiles and sprites.",
"Improved live refresh behavior for renderer-facing graphic updates.",
],
},
{
title: "TBI (To Be Implemented [Or thought about really hard])",
items: [
"Image animations on renderer with hotkey toggle and engine override toggle.",
"Add premade frame duplication effects, like duplicate and shift by direction.",
"Add animation effects that change opacity over time.",
"Add animation effects that shift saturation over time.",
"Work on a system to use an image with animation frames as a tile strip, placing the instance once but specifying which subimage to use.",
"Add an elevation system where some tiles can appear at different z-indexes and show a different subframe.",
"Allow unsnapped tile placement, possibly via hotkey, writing into a chunk patch instead of chunk rows.",
"Explore whether unsnapped placement should be true free placement or an additional sub-layer drawn over an existing layer.",
"Add custom prompts for text input and confirmation dialogs.",
"Prototype terrain painters for meta chunk painting and tile replacement, like rivers, mountains, and woods.",
"Talk to Justin more about zone and subzone music regions via chunk painting.",
"Autotilers.",
"Prefab stamps.",
],
},
] as const satisfies ReadonlyArray<{
export type ChangelogSection = {
title: string;
items: ReadonlyArray<ChangelogItem>;
}>;
};
export const CHANGELOG_SPLASH_VERSION = "2026-06-26-launcher-presentation-update";
export const CHANGELOG_SPLASH_KICKER = "Launch Experience Update";
export const CHANGELOG_SPLASH_TITLE = "What's New";
export const CHANGELOG_SPLASH_FOOTNOTE = "This release focuses on presentation, access, and a cleaner studio handoff.";
export const CHANGELOG_SECTIONS: ReadonlyArray<ChangelogSection> = [
{
title: "Studio Launch Experience",
items: [
"Worldshaper now opens from a dedicated launch page built to frame the studio instead of burying it behind a utility screen.",
"The editor now launches only in its slim floating window, keeping the first impression focused on the intended workspace.",
"The launch page now opens with an editor showcase backdrop that sets the tone before you step inside.",
],
},
{
title: "Project Access",
items: [
"Added a direct Repo destination from the launcher, making project browsing and source access part of the front door.",
"Release highlights now live on the main page, so returning creators can catch up before jumping back into the world.",
],
},
{
title: "Presentation & Structure",
items: [
"The launcher now gives the studio controls and release notes their own stage, mirroring the feel of the in-editor update window.",
"The entry flow has been tightened into a cleaner, more cinematic handoff from main page to creation space.",
],
},
];

View file

@ -1,5 +1,11 @@
import { clampFloatingWindowRect } from "./floatingWindowUtils";
import { CHANGELOG_SECTIONS, CHANGELOG_SPLASH_VERSION } from "./changelogData";
import {
CHANGELOG_SECTIONS,
CHANGELOG_SPLASH_FOOTNOTE,
CHANGELOG_SPLASH_KICKER,
CHANGELOG_SPLASH_TITLE,
CHANGELOG_SPLASH_VERSION,
} from "./changelogData";
import type { ChangelogItem } from "./changelogData";
const CHANGELOG_SPLASH_WINDOW_KEY = "changelogSplash";
@ -160,7 +166,7 @@ export function createChangelogSplashWindowController(scope: ControllerScope) {
function refresh() {
if (state.titleEl) {
state.titleEl.textContent = "What's New";
state.titleEl.textContent = CHANGELOG_SPLASH_TITLE;
}
if (state.metaEl) {
state.metaEl.textContent = `Release ${CHANGELOG_SPLASH_VERSION}`;
@ -221,7 +227,7 @@ export function createChangelogSplashWindowController(scope: ControllerScope) {
heroEl.className = "changelog-splash-hero";
const kickerEl = document.createElement("div");
kickerEl.className = "changelog-splash-kicker";
kickerEl.textContent = "Mid-release update";
kickerEl.textContent = CHANGELOG_SPLASH_KICKER;
const titleEl = document.createElement("div");
titleEl.className = "changelog-splash-title";
const metaEl = document.createElement("div");
@ -235,7 +241,7 @@ export function createChangelogSplashWindowController(scope: ControllerScope) {
footerEl.className = "changelog-splash-footer";
const footnoteEl = document.createElement("div");
footnoteEl.className = "changelog-splash-footnote";
footnoteEl.textContent = "Major features and fixes only. Removed experiments are intentionally omitted.";
footnoteEl.textContent = CHANGELOG_SPLASH_FOOTNOTE;
const actionBtnEl = document.createElement("button");
actionBtnEl.type = "button";
actionBtnEl.className = "mini-btn";