diff --git a/src/WorldshaperLauncher.tsx b/src/WorldshaperLauncher.tsx index 734d883..1ff37f8 100644 --- a/src/WorldshaperLauncher.tsx +++ b/src/WorldshaperLauncher.tsx @@ -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 { const response = await fetch("/api/world-default"); if (!response.ok) { @@ -39,225 +37,139 @@ 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("resolving"); - const [status, setStatus] = useState("Preparing Worldshaper Studio..."); + const [launchState, setLaunchState] = useState("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 { + async function handleLaunch(): Promise { setError(""); - setLaunchState("resolving"); - setStatus("Preparing Worldshaper Studio..."); + const nextWorldId = worldId || DEFAULT_EDITOR_WORLD_ID_FALLBACK; + setLaunchState("opening"); + setStatus(`Opening Worldshaper Studio for ${nextWorldId}...`); try { - const resolvedWorldId = worldId || await resolveDefaultWorldId().catch(() => DEFAULT_EDITOR_WORLD_ID_FALLBACK); + const resolvedWorldId = nextWorldId || await resolveDefaultWorldId().catch(() => DEFAULT_EDITOR_WORLD_ID_FALLBACK); 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 = ""; + 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 (
-
-

New RPG

-

Worldshaper Studio

-

{status}

- {launchState === "blocked" ? ( -

- Click anywhere or use the button below if your browser asks for permission to open the studio window. -

- ) : null} - {launchState === "opened" ? ( -

- The editor is running in its own window. You can leave this tab open as a quick relaunch point, or close it. -

- ) : null} - {error ?

{error}

: null} - {showFallbackActions ? ( +
+
+

New RPG

+

Worldshaper Studio

+

{status}

+ {launchState === "blocked" ? ( +

+ Allow the popup, then use the studio button again to launch the floating editor window. +

+ ) : null} + {launchState === "opened" ? ( +

+ The studio is open in its own slim window. This page stays behind as your release board and relaunch point. +

+ ) : null} + {launchState === "ready" ? ( +

+ The editor is designed to live in its own floating window, so the launcher keeps the first step clean. +

+ ) : null} + {error ?

{error}

: null}
- -
- ) : null} -
-
-

What's New

-

{CHANGELOG_SPLASH_VERSION}

+
+
+
+
What's New
+
Current release highlights
-
- {CHANGELOG_SECTIONS.map((section) => ( -
-

{section.title}

-
    - {section.items.map((item, index) => { - const key = `${section.title}-${index}`; - if (typeof item === "string") { - return
  • {item}
  • ; - } - return ( -
  • -
    {item.text}
    - {item.note ?
    {item.note}
    : null} -
  • - ); - })} -
-
- ))} +
+
+
+
{CHANGELOG_SPLASH_KICKER}
+
{CHANGELOG_SPLASH_TITLE}
+
Release {CHANGELOG_SPLASH_VERSION}
+
+
+ {CHANGELOG_SECTIONS.map((section) => ( +
+

{section.title}

+
    + {section.items.map((item, index) => { + const key = `${section.title}-${index}`; + const normalizedItem: ChangelogItem = item; + if (typeof normalizedItem === "string") { + return
  • {normalizedItem}
  • ; + } + return ( +
  • +
    {normalizedItem.text}
    + {normalizedItem.note ?
    {normalizedItem.note}
    : null} +
  • + ); + })} +
+
+ ))} +
+
+
{CHANGELOG_SPLASH_FOOTNOTE}
+
+
-
+
); } diff --git a/src/index.css b/src/index.css index 7f34799..24fc3b4 100644 --- a/src/index.css +++ b/src/index.css @@ -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; diff --git a/src/worldshaperStudio/changelogData.ts b/src/worldshaperStudio/changelogData.ts index b0b0a81..10d8c13 100644 --- a/src/worldshaperStudio/changelogData.ts +++ b/src/worldshaperStudio/changelogData.ts @@ -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; -}>; +}; + +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 = [ + { + 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.", + ], + }, +]; diff --git a/src/worldshaperStudio/changelogSplashWindowController.ts b/src/worldshaperStudio/changelogSplashWindowController.ts index 5970548..d89bf6e 100644 --- a/src/worldshaperStudio/changelogSplashWindowController.ts +++ b/src/worldshaperStudio/changelogSplashWindowController.ts @@ -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";