feat: unlock in-game galleries per owned version
This commit is contained in:
1 parent
e14184435c
commit
19924e1c5a
21 files changed
+977
-395
No files matched your search
@@ -2,9 +2,10 @@
|
||||
|
||||
import { AudioSystem } from "./audio";
|
||||
import { cutscene } from "./cutscene";
|
||||
import { galleryScreens, isMistGallery, replayEntries } from "./gallery-access";
|
||||
import { VolumeStore } from "./volume-store";
|
||||
import { LiveImages } from "../imager/live";
|
||||
import { BlockingCall, JumpSignal, PyDict, Python, decodeValue, liveObjects, nativeClasses, encodeValue, pyLen, pyStr, truthy, userBuiltins, type Scope } from "./python";
|
||||
import { BlockingCall, JumpSignal, PyDict, PyInstance, Python, decodeValue, liveObjects, nativeClasses, encodeValue, pyLen, pyStr, truthy, userBuiltins, type Scope } from "./python";
|
||||
import { Renderer, type ResolvedImage } from "./renderer";
|
||||
import { cpsPoints, fastPoint, interpolate, parseText, plainText, renderText, TEXT_DISPLAYABLE, TEXT_PARAGRAPH, TEXT_TAG, TEXT_TEXT, textHooks, waitPoints } from "./text";
|
||||
import { languageName, rememberLanguage, type Translator } from "./i18n";
|
||||
@@ -77,6 +78,12 @@ export class Engine {
|
||||
private store: Record<string, any> = {};
|
||||
private builtins: Record<string, any> = {};
|
||||
private persistent: Record<string, any>;
|
||||
private gallerySession = false;
|
||||
private galleryStash: SaveData | null = null;
|
||||
private galleryPersistent: Record<string, any> | null = null;
|
||||
private gallerySeen: Set<string> | null = null;
|
||||
private galleryHistory: { name: string; text: string }[] | null = null;
|
||||
private galleryRollback: { snap: SaveData; history: { name: string; text: string }[] }[] | null = null;
|
||||
private py: Python;
|
||||
private renderer: Renderer;
|
||||
private audio: AudioSystem;
|
||||
@@ -177,7 +184,7 @@ export class Engine {
|
||||
del: (n) => void delete this.store[n],
|
||||
};
|
||||
this.py = new Python(scope);
|
||||
this.screens = new ScreenManager(this.screenHost(), game.screens ?? {}, game.styles ?? {});
|
||||
this.screens = new ScreenManager(this.screenHost(), galleryScreens(game, !!manifest.galleryUnlocked), game.styles ?? {});
|
||||
this.py.blockingNames = new Set([
|
||||
"renpy.input", "renpy.say", "renpy.pause", "renpy.display_menu", "renpy.with_statement", "renpy.call_screen",
|
||||
"narrator", ...Object.keys(game.characters), ...(game.blocking ?? []),
|
||||
@@ -402,11 +409,14 @@ export class Engine {
|
||||
const data = decodeValue(loadJSON<Record<string, any>>(this.persistentKey, {}));
|
||||
const save = () => saveJSON(this.persistentKey, encodeValue(data));
|
||||
return new Proxy(data, {
|
||||
get: (t, k: string) => (k in t ? t[k] : k === "toJSON" ? undefined
|
||||
: k === "_clear" ? () => { for (const key of Object.keys(t)) delete t[key]; save(); return null; } : null),
|
||||
get: (t, k: string) => {
|
||||
const current = this.galleryPersistent ?? t;
|
||||
return k in current ? current[k] : k === "toJSON" ? undefined
|
||||
: k === "_clear" ? () => { for (const key of Object.keys(current)) delete current[key]; if (!this.gallerySession) save(); return null; } : null;
|
||||
},
|
||||
set: (t, k: string, v) => {
|
||||
t[k] = v;
|
||||
save();
|
||||
(this.galleryPersistent ?? t)[k] = v;
|
||||
if (!this.gallerySession) save();
|
||||
return true;
|
||||
},
|
||||
});
|
||||
@@ -1176,6 +1186,7 @@ export class Engine {
|
||||
private screenHost(): ScreenHost {
|
||||
const engine = this;
|
||||
return {
|
||||
galleryUnlocked: !!this.manifest.galleryUnlocked,
|
||||
error: (message) => this.fail(new Error(message)),
|
||||
get audio() { return engine.audio; },
|
||||
setMixer: (name, value) => this.setMixerValue(name, value),
|
||||
@@ -1188,10 +1199,14 @@ export class Engine {
|
||||
play: (channel, file, loop) => this.audio.play(channel, [file], { loop }),
|
||||
stop: (channel) => this.audio.stop(channel, 0),
|
||||
notify: (message) => this.notify(message),
|
||||
jump: (label) => this.restart(() => this.guardedJump(label)),
|
||||
jump: (label) => {
|
||||
if (this.gallerySession && isMistGallery(this.game) && label === "myroomcontrol" && !truthy(this.store.replaywatch)) {
|
||||
this.closeUnlockedGallery();
|
||||
} else this.restart(() => this.guardedJump(label));
|
||||
},
|
||||
call: (label) => this.restart(() => this.call(label)),
|
||||
quit: () => this.onExit(),
|
||||
mainMenu: () => this.restart(() => { this.stack = []; this.cur = { label: "", path: [0] }; }),
|
||||
mainMenu: () => this.gallerySession ? this.closeUnlockedGallery() : this.restart(() => { this.stack = []; this.cur = { label: "", path: [0] }; }),
|
||||
openMenu: () => this.openGameMenu(),
|
||||
screensOn: (layer) => this.renderer.screensOn(layer),
|
||||
rollback: () => this.rollback(),
|
||||
@@ -1464,6 +1479,10 @@ export class Engine {
|
||||
}
|
||||
|
||||
private jump(target: string, args?: { a: any[]; kw: Record<string, any> }, frame?: Frame) {
|
||||
if (this.gallerySession && isMistGallery(this.game) && target === "myroomcontrol") {
|
||||
if (truthy(this.store.replaywatch)) target = "rewatchscene";
|
||||
else { this.closeUnlockedGallery(); return; }
|
||||
}
|
||||
if (target === "main_menu_screen" || target === "_main_menu_screen") target = "__main_menu__";
|
||||
if (target === "__main_menu__" || target === "__quit__") {
|
||||
throw new Restart(target);
|
||||
@@ -2223,6 +2242,7 @@ export class Engine {
|
||||
}
|
||||
|
||||
private async save(slot: string, silent = false) {
|
||||
if (this.gallerySession) return;
|
||||
if (!this.running || !this.cur.label) return;
|
||||
const data = this.snapshotState();
|
||||
const title = (data.text ?? "").slice(0, 120) || this.cur.label;
|
||||
@@ -2235,6 +2255,7 @@ export class Engine {
|
||||
}
|
||||
|
||||
private async load(slot: string) {
|
||||
if (this.gallerySession) { this.notify("Закройте галерею, чтобы загрузить сохранение"); return; }
|
||||
const data = await this.saves.get(slot);
|
||||
if (!data) {
|
||||
this.notify("Сохранение не найдено");
|
||||
@@ -2287,6 +2308,88 @@ export class Engine {
|
||||
this.history = [];
|
||||
}
|
||||
|
||||
/** Open an isolated gallery session. Unlocks never turn into story progress or saves. */
|
||||
openUnlockedGallery() {
|
||||
if (!this.manifest.galleryUnlocked || this.gallerySession) return;
|
||||
this.galleryStash = this.running && this.cur.label ? this.snapshotState() : null;
|
||||
this.galleryPersistent = decodeValue(encodeValue(this.persistent));
|
||||
this.gallerySeen = this.seenImages;
|
||||
this.seenImages = new Set(this.seenImages);
|
||||
this.galleryHistory = this.history;
|
||||
this.galleryRollback = this.rollbackLog;
|
||||
this.history = [];
|
||||
this.rollbackLog = [];
|
||||
this.gallerySession = true;
|
||||
this.gen++;
|
||||
this.wakeAll();
|
||||
this.onAdvance = null;
|
||||
this.skipTick = null;
|
||||
this.running = false;
|
||||
this.setSkip(false);
|
||||
this.audio.stopAll();
|
||||
this.renderer.reset();
|
||||
this.newStore();
|
||||
this.stack = [];
|
||||
this.cur = { label: "", path: [0] };
|
||||
this.textbox.classList.add("pg-hidden");
|
||||
this.menuEl.classList.add("pg-hidden");
|
||||
this.clearNvl();
|
||||
if (isMistGallery(this.game) && this.labels.has("rewatchscene")) {
|
||||
this.restart(() => this.guardedJump("rewatchscene"));
|
||||
} else this.showUnlockedGalleryIndex();
|
||||
}
|
||||
|
||||
private closeUnlockedGallery() {
|
||||
const stash = this.galleryStash;
|
||||
const history = this.galleryHistory ?? [];
|
||||
const rollback = this.galleryRollback ?? [];
|
||||
this.galleryHistory = null;
|
||||
this.galleryRollback = null;
|
||||
this.gallerySession = false;
|
||||
this.galleryPersistent = null;
|
||||
this.galleryStash = null;
|
||||
if (this.gallerySeen) this.seenImages = this.gallerySeen;
|
||||
this.gallerySeen = null;
|
||||
this.replayStash = null;
|
||||
this.screens.clear();
|
||||
if (stash) this.restart(() => {
|
||||
this.applySave(stash);
|
||||
this.history = history;
|
||||
this.rollbackLog = rollback;
|
||||
});
|
||||
else {
|
||||
this.stack = [];
|
||||
this.cur = { label: "", path: [0] };
|
||||
this.showMainMenu();
|
||||
}
|
||||
}
|
||||
|
||||
private showUnlockedGalleryIndex() {
|
||||
this.running = false;
|
||||
this.openScreen("Галерея открыта", (body) => {
|
||||
body.classList.add("pg-gallery-list");
|
||||
let count = 0;
|
||||
const add = (name: string, action: any) => {
|
||||
if (!action) return;
|
||||
count++;
|
||||
const button = el("button", "", name);
|
||||
button.onclick = () => this.screens.run(action, null);
|
||||
body.append(button);
|
||||
};
|
||||
for (const [name, collection] of Object.entries(this.store)) {
|
||||
if (!(collection instanceof PyInstance) || collection.cls.name !== "Gallery" || !(collection.attrs.buttons instanceof PyDict)) continue;
|
||||
body.append(el("h3", "", name));
|
||||
for (const key of collection.attrs.buttons.map.keys()) {
|
||||
add(pyStr(key), this.py.attrOf(collection, "Action")(key));
|
||||
}
|
||||
}
|
||||
for (const entry of replayEntries(this.game.screens)) {
|
||||
if (this.labels.has(entry.label) || this.anchors.has(entry.label)) add(entry.label, this.safeEval(entry.expr));
|
||||
}
|
||||
if (!count) body.append(el("p", "", "В этой игре не найдены доступные сцены галереи."));
|
||||
}, () => this.closeUnlockedGallery());
|
||||
}
|
||||
|
||||
/** Gallery replay: run a label with the given variables, then return to exactly where the game was. */
|
||||
private startReplay(label: string, scope: Record<string, any> | null) {
|
||||
if (!this.labels.has(label)) {
|
||||
@@ -2545,6 +2648,7 @@ export class Engine {
|
||||
}
|
||||
|
||||
private showMainMenu() {
|
||||
if (this.gallerySession) { this.showUnlockedGalleryIndex(); return; }
|
||||
this.placeScreen(false);
|
||||
this.running = false;
|
||||
this.gen++;
|
||||
@@ -2567,6 +2671,7 @@ export class Engine {
|
||||
return b;
|
||||
};
|
||||
btn("Начать", () => this.newGame());
|
||||
if (this.manifest.galleryUnlocked) btn("Галерея открыта", () => this.openUnlockedGallery());
|
||||
const cont = btn("Продолжить", () => void this.load("auto"));
|
||||
cont.style.display = "none";
|
||||
this.saves.list().then((list) => {
|
||||
@@ -2610,6 +2715,8 @@ export class Engine {
|
||||
list.append(b);
|
||||
};
|
||||
add("Вернуться", () => this.closeScreen());
|
||||
if (this.gallerySession) add("Закрыть галерею", () => this.closeUnlockedGallery());
|
||||
else if (this.manifest.galleryUnlocked) add("Открыть галерею", () => this.openUnlockedGallery());
|
||||
add("История", () => this.openHistory());
|
||||
add(this.skip ? "Выключить пропуск" : "Пропуск", () => { this.closeScreen(); this.setSkip(!this.skip); });
|
||||
add(this.auto ? "Выключить авто" : "Автопрочтение", () => { this.closeScreen(); this.setAuto(!this.auto); });
|
||||
@@ -2640,6 +2747,7 @@ export class Engine {
|
||||
}
|
||||
|
||||
private openSaves(mode: "save" | "load", fromMain = false) {
|
||||
if (this.gallerySession) { this.notify("Закройте галерею, чтобы открыть сохранения"); return; }
|
||||
this.openScreen(mode === "save" ? "Сохранить" : "Загрузить", (body) => {
|
||||
const grid = el("div", "pg-slots");
|
||||
body.append(grid);
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { GameData, Expr } from "./types";
|
||||
|
||||
export const isMistGallery = (game: GameData) => game.build?.name === "MIST" && !!game.screens?.rewatchgirlselectscreen;
|
||||
|
||||
/** Only the gallery's view of progress changes. The underlying quest/scene objects stay intact. */
|
||||
export function galleryScreens(game: GameData, unlocked: boolean) {
|
||||
const screens = game.screens ?? {};
|
||||
if (!unlocked || !isMistGallery(game)) return screens;
|
||||
const names = new Set(["rewatchgirlselectscreen", "galleryheader", "robgallery", "miagallery", "lilgallery", "norgallery", "groupgallery"]);
|
||||
const flags = new Set(["rscene02watched", ...Array.from({ length: 7 }, (_, i) => `groupscene0${i + 1}watched`)]);
|
||||
const visit = (value: any): any => {
|
||||
if (Array.isArray(value)) return value.map(visit);
|
||||
if (!value || typeof value !== "object") return value;
|
||||
if (value.k === "name" && flags.has(value.id)) return { k: "const", v: true };
|
||||
if (value.k === "attr" && ["watched", "is_done"].includes(value.a)) return { k: "const", v: true };
|
||||
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, visit(item)]));
|
||||
};
|
||||
return Object.fromEntries(Object.entries(screens).map(([name, screen]) => [name, names.has(name) ? visit(screen) : screen]));
|
||||
}
|
||||
|
||||
/** Standard Replay actions can be offered directly, even when their original UI is locked. */
|
||||
export function replayEntries(screens: GameData["screens"]): { label: string; expr: Expr }[] {
|
||||
const found = new Map<string, Expr>();
|
||||
const visit = (value: any) => {
|
||||
if (!value || typeof value !== "object") return;
|
||||
if (value.k === "call" && value.f?.k === "name" && value.f.id === "Replay") {
|
||||
const label = value.a?.[0] ?? value.kw?.label;
|
||||
if (label?.k === "const" && typeof label.v === "string") found.set(label.v, value);
|
||||
}
|
||||
for (const item of Object.values(value)) visit(item);
|
||||
};
|
||||
visit(screens);
|
||||
return [...found].map(([label, expr]) => ({ label, expr }));
|
||||
}
|
||||
@@ -6,6 +6,7 @@ interface Picture { displayables: any[]; conditions: Condition[] }
|
||||
interface Button { pictures: Picture[]; conditions: Condition[] }
|
||||
|
||||
export function galleryClass(host: {
|
||||
unlocked?(): boolean;
|
||||
seen(name: string): boolean;
|
||||
evaluate(source: string): any;
|
||||
show(pictures: any[][], transition: any): void;
|
||||
@@ -14,7 +15,7 @@ export function galleryClass(host: {
|
||||
const gallery = new PyClass("Gallery", [OBJECT]);
|
||||
const method = (name: string, fn: (self: PyInstance, ...args: any[]) => any) =>
|
||||
gallery.attrs.set(name, nativeMethod(fn));
|
||||
const conditions = (items: Condition[]) => items.every((c) => c.kind === "seen"
|
||||
const conditions = (items: Condition[]) => host.unlocked?.() || items.every((c) => c.kind === "seen"
|
||||
? c.images.every((name) => host.seen(name)) : truthy(host.evaluate(c.source)));
|
||||
const get = (self: PyInstance, name: string): Button => {
|
||||
const buttons = self.attrs.buttons as PyDict;
|
||||
|
||||
@@ -56,7 +56,8 @@ export async function launch(root: HTMLElement, versionId: number, onExit: () =>
|
||||
(code) => loadTranslator(game, manifest, code),
|
||||
gameId === null ? `pgvn:audio:version:${versionId}` : `pgvn:audio:game:${gameId}`);
|
||||
const at = url.get("at");
|
||||
if (at) engine.startAt(at);
|
||||
if (url.get("gallery") === "1" && manifest.galleryUnlocked) engine.openUnlockedGallery();
|
||||
else if (at) engine.startAt(at);
|
||||
else engine.start();
|
||||
return engine;
|
||||
}
|
||||
+378
-378
@@ -1,378 +1,378 @@
|
||||
.pg-root {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.pg-stage {
|
||||
--pg-accent: #ff9f43;
|
||||
--pg-text: #ffffff;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
color: var(--pg-text);
|
||||
font-family: "Segoe UI", system-ui, sans-serif;
|
||||
font-size: var(--pg-font);
|
||||
transform-origin: center;
|
||||
cursor: default;
|
||||
}
|
||||
.pg-stage.pg-game-font { font-family: "pg-game-font", "Segoe UI", system-ui, sans-serif; }
|
||||
.pg-stage.pg-game-name-font .pg-namebox { font-family: "pg-game-name-font", "Segoe UI", system-ui, sans-serif; }
|
||||
.pg-stage.pg-locale-font, .pg-stage.pg-locale-font .pg-namebox { font-family: "pg-locale-font", "Segoe UI", system-ui, sans-serif; }
|
||||
|
||||
.pg-layer { position: absolute; inset: 0; }
|
||||
.pg-transition { pointer-events: none; }
|
||||
.pg-transition > .pg-old { position: absolute; inset: 0; background: #000; }
|
||||
.pg-fill { position: absolute; inset: 0; }
|
||||
.pg-image { position: absolute; will-change: transform, opacity; }
|
||||
.pg-show-text { font-size: calc(var(--pg-font) * 1.5); text-shadow: 0 2px 6px #000; white-space: nowrap; }
|
||||
|
||||
.pg-hidden { display: none !important; }
|
||||
|
||||
.pg-textbox {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(to bottom, rgba(10, 12, 24, 0.55), rgba(10, 12, 24, 0.92));
|
||||
border-top: 2px solid rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
.pg-textbox-image { background-color: transparent; background-size: 100% 100%; border: 0; }
|
||||
.pg-textbox.pg-ui-hidden { opacity: 0; }
|
||||
|
||||
.pg-namebox {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
transform: translateY(-50%);
|
||||
padding: 0.15em 0.8em;
|
||||
font-size: var(--pg-name-font);
|
||||
font-weight: 600;
|
||||
color: var(--pg-accent);
|
||||
background: rgba(10, 12, 24, 0.85);
|
||||
border-radius: 0.3em;
|
||||
text-shadow: 0 1px 2px #000;
|
||||
}
|
||||
.pg-textbox-image .pg-namebox { background: transparent; }
|
||||
|
||||
.pg-text {
|
||||
position: absolute;
|
||||
line-height: 1.35;
|
||||
white-space: pre-wrap;
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
.pg-ctc { margin-left: 0.3em; font-size: 0.6em; opacity: 0.7; animation: pg-blink 1s infinite; }
|
||||
@keyframes pg-blink { 50% { opacity: 0.1; } }
|
||||
|
||||
.pg-quick {
|
||||
position: absolute;
|
||||
bottom: 0.35em;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 0.15em;
|
||||
padding: 0.1em 0.3em;
|
||||
border-radius: 99em;
|
||||
background: rgba(8, 10, 20, 0.45);
|
||||
backdrop-filter: blur(6px);
|
||||
opacity: 0.55;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.pg-quick:hover, .pg-quick:focus-within { opacity: 1; }
|
||||
.pg-quick button {
|
||||
background: none;
|
||||
border: 0;
|
||||
border-radius: 99em;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font: inherit;
|
||||
font-size: calc(var(--pg-font) * 0.55);
|
||||
padding: 0.25em 0.75em;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
.pg-quick button:hover { background: rgba(255, 255, 255, 0.12); color: #fff; }
|
||||
.pg-quick button.pg-on { color: #1a1205; background: var(--pg-accent); }
|
||||
.pg-quick button:focus-visible { outline: 2px solid var(--pg-accent); }
|
||||
|
||||
.pg-menu {
|
||||
position: absolute;
|
||||
inset: 0 0 26% 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5em;
|
||||
}
|
||||
.pg-choice {
|
||||
min-width: 50%;
|
||||
max-width: 75%;
|
||||
padding: 0.45em 1.2em;
|
||||
font: inherit;
|
||||
color: #fff;
|
||||
background: rgba(15, 18, 35, 0.85);
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
border-radius: 0.3em;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
}
|
||||
.pg-choice:hover { background: rgba(40, 45, 80, 0.95); border-color: var(--pg-accent); }
|
||||
|
||||
.pg-nvl {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
padding: 5% 15%;
|
||||
background: rgba(0, 0, 0, 0.72) center / cover;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6em;
|
||||
overflow: hidden;
|
||||
}
|
||||
.pg-nvl-name { font-weight: 600; margin-right: 0.6em; }
|
||||
.pg-nvl-what { white-space: pre-wrap; }
|
||||
|
||||
.pg-centered {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 0 15%;
|
||||
white-space: pre-wrap;
|
||||
text-shadow: 0 2px 4px #000;
|
||||
}
|
||||
|
||||
.pg-notify {
|
||||
position: absolute;
|
||||
top: 2%;
|
||||
left: 2%;
|
||||
padding: 0.4em 1em;
|
||||
font-size: calc(var(--pg-font) * 0.7);
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
border-left: 4px solid var(--pg-accent);
|
||||
}
|
||||
|
||||
.pg-screen {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(8, 10, 20, 0.88) center / cover;
|
||||
backdrop-filter: blur(10px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 3% 6%;
|
||||
font-size: calc(var(--pg-font) * 0.85);
|
||||
z-index: 10;
|
||||
animation: pg-fade 0.15s ease-out;
|
||||
}
|
||||
@keyframes pg-fade { from { opacity: 0; } }
|
||||
.pg-screen-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.4em; }
|
||||
.pg-screen-head h2 { margin: 0 0 0.3em; color: #fff; font-weight: 650; font-size: 1.3em; letter-spacing: -0.01em; }
|
||||
.pg-screen-head h2::before { content: ""; display: inline-block; width: 0.2em; height: 0.9em; margin-right: 0.5em; border-radius: 99em; background: var(--pg-accent); vertical-align: -0.1em; }
|
||||
.pg-screen button, .pg-dialog button, .pg-screen select {
|
||||
font: inherit;
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||
border-radius: 0.5em;
|
||||
padding: 0.5em 1.1em;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, border-color 0.15s, color 0.15s;
|
||||
}
|
||||
.pg-screen button:hover:not(:disabled), .pg-dialog button:hover { background: rgba(255, 255, 255, 0.15); border-color: var(--pg-accent); }
|
||||
.pg-screen button:focus-visible, .pg-dialog button:focus-visible, .pg-screen select:focus-visible, .pg-hud-btn:focus-visible {
|
||||
outline: 3px solid var(--pg-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.pg-screen button:disabled { opacity: 0.35; cursor: default; }
|
||||
.pg-screen option { color: #111; }
|
||||
.pg-close { background: rgba(255, 255, 255, 0.08) !important; width: 2.2em; height: 2.2em; padding: 0 !important; border-radius: 50% !important; font-size: 1em !important; }
|
||||
.pg-screen-body { flex: 1; overflow: auto; padding: 0.2em; }
|
||||
.pg-menu-list { display: flex; flex-direction: column; gap: 0.5em; max-width: min(24em, 100%); }
|
||||
.pg-menu-list button { text-align: left; padding: 0.7em 1.2em; }
|
||||
.pg-history { display: flex; flex-direction: column; gap: 0.8em; max-height: 100%; overflow: auto; max-width: 52em; }
|
||||
.pg-history-row { padding: 0.5em 0.8em; border-left: 3px solid rgba(255, 255, 255, 0.12); }
|
||||
.pg-history-row b { color: var(--pg-accent); margin-right: 0.6em; }
|
||||
.pg-slots { display: grid; grid-template-columns: repeat(auto-fill, minmax(11em, 1fr)); gap: 0.7em; }
|
||||
.pg-slot { display: flex; flex-direction: column; align-items: flex-start; gap: 0.25em; text-align: left; min-height: 6em; }
|
||||
.pg-slot b { color: var(--pg-accent); font-weight: 600; }
|
||||
.pg-slot-title { font-size: 0.85em; opacity: 0.9; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
|
||||
.pg-slot small { opacity: 0.55; font-size: 0.7em; margin-top: auto; }
|
||||
.pg-settings-screen {
|
||||
--pg-accent: #ff9f43;
|
||||
--pg-font: 18px;
|
||||
z-index: 25;
|
||||
padding: clamp(16px, 4%, 40px);
|
||||
color: #edf0f7;
|
||||
font: 16px/1.5 "Segoe UI", system-ui, sans-serif;
|
||||
background: rgba(11, 14, 23, 0.96);
|
||||
container-type: inline-size;
|
||||
color-scheme: dark;
|
||||
}
|
||||
.pg-settings-screen .pg-screen-head,
|
||||
.pg-settings-screen .pg-screen-body { width: 100%; max-width: 960px; margin-inline: auto; box-sizing: border-box; }
|
||||
.pg-settings-screen .pg-screen-head { flex-shrink: 0; margin-bottom: 20px; }
|
||||
.pg-settings-screen .pg-screen-head h2 { font-size: 24px; margin: 0; }
|
||||
.pg-settings-screen .pg-close { min-width: 44px; min-height: 44px; }
|
||||
.pg-settings-screen .pg-screen-body { min-height: 0; scrollbar-width: thin; }
|
||||
.pg-settings { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; align-items: start; }
|
||||
.pg-settings-card { min-width: 0; padding: 24px; border: 1px solid #303646; border-radius: 18px; background: #191e2a; }
|
||||
.pg-settings-card:first-child { grid-row: span 2; }
|
||||
.pg-settings-card h3 { margin: 0 0 6px; font-size: 18px; font-weight: 600; }
|
||||
.pg-settings-hint { color: #aab3c7; font-size: 13px; margin: 0 0 22px; }
|
||||
.pg-setting { display: flex; flex-direction: column; gap: 8px; margin-bottom: 22px; }
|
||||
.pg-setting:last-child { margin-bottom: 0; }
|
||||
.pg-setting-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||
.pg-setting-val { color: var(--pg-accent); font-size: 14px; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
||||
.pg-setting input[type="range"] {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
cursor: pointer;
|
||||
accent-color: var(--pg-accent);
|
||||
}
|
||||
.pg-setting input[type="range"]::-webkit-slider-runnable-track {
|
||||
height: 6px;
|
||||
border-radius: 99px;
|
||||
background: linear-gradient(to right, var(--pg-accent) var(--pg-progress), #3c4458 var(--pg-progress));
|
||||
}
|
||||
.pg-setting input[type="range"]::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-top: -7px;
|
||||
border: 3px solid #191e2a;
|
||||
border-radius: 50%;
|
||||
background: var(--pg-accent);
|
||||
box-shadow: 0 0 0 1px var(--pg-accent);
|
||||
}
|
||||
.pg-setting input[type="range"]::-moz-range-track { height: 6px; border-radius: 99px; background: #3c4458; }
|
||||
.pg-setting input[type="range"]::-moz-range-progress { height: 6px; border-radius: 99px; background: var(--pg-accent); }
|
||||
.pg-setting input[type="range"]::-moz-range-thumb { width: 16px; height: 16px; border: 2px solid #191e2a; border-radius: 50%; background: var(--pg-accent); }
|
||||
.pg-setting input[type="range"]:focus-visible { outline: 2px solid var(--pg-accent); outline-offset: 3px; }
|
||||
.pg-settings-screen .pg-setting select { width: 100%; min-height: 44px; }
|
||||
.pg-settings-screen .pg-wide { width: 100%; min-height: 44px; margin: 0; }
|
||||
@container (max-width: 620px) {
|
||||
.pg-settings { grid-template-columns: minmax(0, 1fr); }
|
||||
.pg-settings-card:first-child { grid-row: auto; }
|
||||
.pg-settings-card { padding: 18px; }
|
||||
}
|
||||
.pg-wide { margin-top: 0.8em; margin-right: 0.6em; }
|
||||
|
||||
.pg-screen.pg-main { background-color: #000; justify-content: center; }
|
||||
.pg-main-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.55em;
|
||||
width: min(26em, 100%);
|
||||
padding: 2em;
|
||||
background: rgba(8, 10, 20, 0.72);
|
||||
backdrop-filter: blur(14px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 1em;
|
||||
box-shadow: 0 1em 3em rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.pg-main-panel h1 { margin: 0 0 0.5em; font-size: 1.6em; color: #fff; letter-spacing: -0.01em; }
|
||||
.pg-main-panel button { text-align: left; padding: 0.7em 1.2em; }
|
||||
.pg-main-panel button:first-of-type { background: var(--pg-accent); border-color: var(--pg-accent); color: #1a1205; font-weight: 650; }
|
||||
.pg-main-panel button:first-of-type:hover { filter: brightness(1.1); }
|
||||
|
||||
/* menu button outside the scaled stage: stays tappable on phones */
|
||||
.pg-hud-btn {
|
||||
position: absolute;
|
||||
top: max(10px, env(safe-area-inset-top));
|
||||
right: max(10px, env(safe-area-inset-right));
|
||||
z-index: 20;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
color: #fff;
|
||||
background: rgba(8, 10, 20, 0.55);
|
||||
backdrop-filter: blur(6px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s, background 0.15s;
|
||||
}
|
||||
.pg-root:hover .pg-hud-btn, .pg-hud-btn:focus-visible { opacity: 0.8; }
|
||||
.pg-hud-btn:hover { opacity: 1 !important; background: rgba(8, 10, 20, 0.85); }
|
||||
.pg-root.pg-in-main .pg-hud-btn { display: none; }
|
||||
@media (pointer: coarse) {
|
||||
.pg-hud-btn { opacity: 0.6; }
|
||||
}
|
||||
|
||||
/* in-player confirmation */
|
||||
.pg-dialog-back {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 30;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 16px;
|
||||
background: rgba(5, 7, 12, 0.7);
|
||||
backdrop-filter: blur(4px);
|
||||
font-family: "Segoe UI", system-ui, sans-serif;
|
||||
}
|
||||
.pg-dialog {
|
||||
width: min(420px, 100%);
|
||||
padding: 22px;
|
||||
color: #eceef5;
|
||||
background: #171b25;
|
||||
border: 1px solid #2a3042;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.55);
|
||||
font-size: 16px;
|
||||
}
|
||||
.pg-dialog p { margin: 0 0 18px; line-height: 1.45; }
|
||||
.pg-dialog-row { display: flex; justify-content: flex-end; gap: 10px; }
|
||||
.pg-dialog button { font-size: 15px; padding: 9px 18px; }
|
||||
.pg-dialog .pg-primary { background: #ff9f43; border-color: #ff9f43; color: #1a1205; font-weight: 650; }
|
||||
|
||||
/* --- game screens (Ren'Py screen language) ------------------------------------------ */
|
||||
.pg-usr { position: absolute; inset: 0; pointer-events: none; }
|
||||
.pg-sinst { position: absolute; inset: 0; pointer-events: none; }
|
||||
.pg-sinst.pg-smodal { pointer-events: auto; }
|
||||
.pg-sw { pointer-events: none; min-width: 0; min-height: 0; color: var(--pg-text, #fff); font-size: var(--pg-font, 24px); }
|
||||
.pg-sframe, .pg-swindow, .pg-sviewport, .pg-sbar, .pg-sbtn { pointer-events: auto; }
|
||||
.pg-sbtn { cursor: pointer; user-select: none; }
|
||||
.pg-sbtn.pg-sdisabled { cursor: default; }
|
||||
.pg-sbtn:focus-visible { outline: 2px solid var(--pg-accent, #4af); outline-offset: 2px; }
|
||||
.pg-simgbtn img, .pg-simgbtn .pg-simg { display: block; pointer-events: none; }
|
||||
.pg-stext { white-space: pre-wrap; }
|
||||
.pg-sbar { background: rgba(255, 255, 255, 0.18); min-width: 40px; min-height: 12px; }
|
||||
.pg-sbar-fill { background: var(--pg-accent, #4af); width: 100%; height: 100%; }
|
||||
.pg-sbar:not(.pg-svbar) .pg-sbar-fill { height: 100%; }
|
||||
.pg-sadd { pointer-events: none; }
|
||||
.pg-sviewport { scrollbar-width: thin; }
|
||||
|
||||
/* --- renpy.input / renpy.display_menu ----------------------------------------------- */
|
||||
.pg-input { display: flex; flex-direction: column; gap: 12px; align-items: center; pointer-events: auto; }
|
||||
.pg-input input { font: inherit; font-size: var(--pg-font, 24px); padding: 6px 10px; min-width: 40%; }
|
||||
.pg-caption { color: var(--pg-text, #fff); opacity: 0.8; text-align: center; padding: 4px 0; }
|
||||
|
||||
/* checkbox row of the settings screen */
|
||||
.pg-check { display: flex; align-items: center; gap: 10px; margin: 4px 0 14px; cursor: pointer; font-size: 14px; }
|
||||
.pg-check input { width: 18px; height: 18px; accent-color: var(--pg-accent, #f5a040); }
|
||||
|
||||
/* Kinetic Text Tags: {bt}, {sc}, {fi}, {rotat} move each letter on its own */
|
||||
.pg-fx { display: inline-block; white-space: pre; }
|
||||
/* SnowBlossom: pictures that fall and drift while the scene is shown */
|
||||
.pg-snow { position: absolute; inset: 0; overflow: hidden; pointer-events: none; }
|
||||
.pg-snow > * { position: absolute; top: -10%; animation: pg-snow-fall linear infinite; }
|
||||
@keyframes pg-snow-fall { from { transform: translate(0, 0); } to { transform: translate(var(--drift, 40px), 120vh); } }
|
||||
.pg-root {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.pg-stage {
|
||||
--pg-accent: #ff9f43;
|
||||
--pg-text: #ffffff;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
color: var(--pg-text);
|
||||
font-family: "Segoe UI", system-ui, sans-serif;
|
||||
font-size: var(--pg-font);
|
||||
transform-origin: center;
|
||||
cursor: default;
|
||||
}
|
||||
.pg-stage.pg-game-font { font-family: "pg-game-font", "Segoe UI", system-ui, sans-serif; }
|
||||
.pg-stage.pg-game-name-font .pg-namebox { font-family: "pg-game-name-font", "Segoe UI", system-ui, sans-serif; }
|
||||
.pg-stage.pg-locale-font, .pg-stage.pg-locale-font .pg-namebox { font-family: "pg-locale-font", "Segoe UI", system-ui, sans-serif; }
|
||||
|
||||
.pg-layer { position: absolute; inset: 0; }
|
||||
.pg-transition { pointer-events: none; }
|
||||
.pg-transition > .pg-old { position: absolute; inset: 0; background: #000; }
|
||||
.pg-fill { position: absolute; inset: 0; }
|
||||
.pg-image { position: absolute; will-change: transform, opacity; }
|
||||
.pg-show-text { font-size: calc(var(--pg-font) * 1.5); text-shadow: 0 2px 6px #000; white-space: nowrap; }
|
||||
|
||||
.pg-hidden { display: none !important; }
|
||||
|
||||
.pg-textbox {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(to bottom, rgba(10, 12, 24, 0.55), rgba(10, 12, 24, 0.92));
|
||||
border-top: 2px solid rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
.pg-textbox-image { background-color: transparent; background-size: 100% 100%; border: 0; }
|
||||
.pg-textbox.pg-ui-hidden { opacity: 0; }
|
||||
|
||||
.pg-namebox {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
transform: translateY(-50%);
|
||||
padding: 0.15em 0.8em;
|
||||
font-size: var(--pg-name-font);
|
||||
font-weight: 600;
|
||||
color: var(--pg-accent);
|
||||
background: rgba(10, 12, 24, 0.85);
|
||||
border-radius: 0.3em;
|
||||
text-shadow: 0 1px 2px #000;
|
||||
}
|
||||
.pg-textbox-image .pg-namebox { background: transparent; }
|
||||
|
||||
.pg-text {
|
||||
position: absolute;
|
||||
line-height: 1.35;
|
||||
white-space: pre-wrap;
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
.pg-ctc { margin-left: 0.3em; font-size: 0.6em; opacity: 0.7; animation: pg-blink 1s infinite; }
|
||||
@keyframes pg-blink { 50% { opacity: 0.1; } }
|
||||
|
||||
.pg-quick {
|
||||
position: absolute;
|
||||
bottom: 0.35em;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 0.15em;
|
||||
padding: 0.1em 0.3em;
|
||||
border-radius: 99em;
|
||||
background: rgba(8, 10, 20, 0.45);
|
||||
backdrop-filter: blur(6px);
|
||||
opacity: 0.55;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.pg-quick:hover, .pg-quick:focus-within { opacity: 1; }
|
||||
.pg-quick button {
|
||||
background: none;
|
||||
border: 0;
|
||||
border-radius: 99em;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font: inherit;
|
||||
font-size: calc(var(--pg-font) * 0.55);
|
||||
padding: 0.25em 0.75em;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
.pg-quick button:hover { background: rgba(255, 255, 255, 0.12); color: #fff; }
|
||||
.pg-quick button.pg-on { color: #1a1205; background: var(--pg-accent); }
|
||||
.pg-quick button:focus-visible { outline: 2px solid var(--pg-accent); }
|
||||
|
||||
.pg-menu {
|
||||
position: absolute;
|
||||
inset: 0 0 26% 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5em;
|
||||
}
|
||||
.pg-choice {
|
||||
min-width: 50%;
|
||||
max-width: 75%;
|
||||
padding: 0.45em 1.2em;
|
||||
font: inherit;
|
||||
color: #fff;
|
||||
background: rgba(15, 18, 35, 0.85);
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
border-radius: 0.3em;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
}
|
||||
.pg-choice:hover { background: rgba(40, 45, 80, 0.95); border-color: var(--pg-accent); }
|
||||
|
||||
.pg-nvl {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
padding: 5% 15%;
|
||||
background: rgba(0, 0, 0, 0.72) center / cover;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6em;
|
||||
overflow: hidden;
|
||||
}
|
||||
.pg-nvl-name { font-weight: 600; margin-right: 0.6em; }
|
||||
.pg-nvl-what { white-space: pre-wrap; }
|
||||
|
||||
.pg-centered {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 0 15%;
|
||||
white-space: pre-wrap;
|
||||
text-shadow: 0 2px 4px #000;
|
||||
}
|
||||
|
||||
.pg-notify {
|
||||
position: absolute;
|
||||
top: 2%;
|
||||
left: 2%;
|
||||
padding: 0.4em 1em;
|
||||
font-size: calc(var(--pg-font) * 0.7);
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
border-left: 4px solid var(--pg-accent);
|
||||
}
|
||||
|
||||
.pg-screen {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(8, 10, 20, 0.88) center / cover;
|
||||
backdrop-filter: blur(10px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 3% 6%;
|
||||
font-size: calc(var(--pg-font) * 0.85);
|
||||
z-index: 10;
|
||||
animation: pg-fade 0.15s ease-out;
|
||||
}
|
||||
@keyframes pg-fade { from { opacity: 0; } }
|
||||
.pg-screen-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.4em; }
|
||||
.pg-screen-head h2 { margin: 0 0 0.3em; color: #fff; font-weight: 650; font-size: 1.3em; letter-spacing: -0.01em; }
|
||||
.pg-screen-head h2::before { content: ""; display: inline-block; width: 0.2em; height: 0.9em; margin-right: 0.5em; border-radius: 99em; background: var(--pg-accent); vertical-align: -0.1em; }
|
||||
.pg-screen button, .pg-dialog button, .pg-screen select {
|
||||
font: inherit;
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||
border-radius: 0.5em;
|
||||
padding: 0.5em 1.1em;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, border-color 0.15s, color 0.15s;
|
||||
}
|
||||
.pg-screen button:hover:not(:disabled), .pg-dialog button:hover { background: rgba(255, 255, 255, 0.15); border-color: var(--pg-accent); }
|
||||
.pg-screen button:focus-visible, .pg-dialog button:focus-visible, .pg-screen select:focus-visible, .pg-hud-btn:focus-visible {
|
||||
outline: 3px solid var(--pg-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.pg-screen button:disabled { opacity: 0.35; cursor: default; }
|
||||
.pg-screen option { color: #111; }
|
||||
.pg-close { background: rgba(255, 255, 255, 0.08) !important; width: 2.2em; height: 2.2em; padding: 0 !important; border-radius: 50% !important; font-size: 1em !important; }
|
||||
.pg-screen-body { flex: 1; overflow: auto; padding: 0.2em; }
|
||||
.pg-menu-list { display: flex; flex-direction: column; gap: 0.5em; max-width: min(24em, 100%); }
|
||||
.pg-menu-list button { text-align: left; padding: 0.7em 1.2em; }
|
||||
.pg-history { display: flex; flex-direction: column; gap: 0.8em; max-height: 100%; overflow: auto; max-width: 52em; }
|
||||
.pg-history-row { padding: 0.5em 0.8em; border-left: 3px solid rgba(255, 255, 255, 0.12); }
|
||||
.pg-history-row b { color: var(--pg-accent); margin-right: 0.6em; }
|
||||
.pg-slots { display: grid; grid-template-columns: repeat(auto-fill, minmax(11em, 1fr)); gap: 0.7em; }
|
||||
.pg-slot { display: flex; flex-direction: column; align-items: flex-start; gap: 0.25em; text-align: left; min-height: 6em; }
|
||||
.pg-slot b { color: var(--pg-accent); font-weight: 600; }
|
||||
.pg-slot-title { font-size: 0.85em; opacity: 0.9; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
|
||||
.pg-slot small { opacity: 0.55; font-size: 0.7em; margin-top: auto; }
|
||||
.pg-settings-screen {
|
||||
--pg-accent: #ff9f43;
|
||||
--pg-font: 18px;
|
||||
z-index: 25;
|
||||
padding: clamp(16px, 4%, 40px);
|
||||
color: #edf0f7;
|
||||
font: 16px/1.5 "Segoe UI", system-ui, sans-serif;
|
||||
background: rgba(11, 14, 23, 0.96);
|
||||
container-type: inline-size;
|
||||
color-scheme: dark;
|
||||
}
|
||||
.pg-settings-screen .pg-screen-head,
|
||||
.pg-settings-screen .pg-screen-body { width: 100%; max-width: 960px; margin-inline: auto; box-sizing: border-box; }
|
||||
.pg-settings-screen .pg-screen-head { flex-shrink: 0; margin-bottom: 20px; }
|
||||
.pg-settings-screen .pg-screen-head h2 { font-size: 24px; margin: 0; }
|
||||
.pg-settings-screen .pg-close { min-width: 44px; min-height: 44px; }
|
||||
.pg-settings-screen .pg-screen-body { min-height: 0; scrollbar-width: thin; }
|
||||
.pg-settings { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; align-items: start; }
|
||||
.pg-settings-card { min-width: 0; padding: 24px; border: 1px solid #303646; border-radius: 18px; background: #191e2a; }
|
||||
.pg-settings-card:first-child { grid-row: span 2; }
|
||||
.pg-settings-card h3 { margin: 0 0 6px; font-size: 18px; font-weight: 600; }
|
||||
.pg-settings-hint { color: #aab3c7; font-size: 13px; margin: 0 0 22px; }
|
||||
.pg-setting { display: flex; flex-direction: column; gap: 8px; margin-bottom: 22px; }
|
||||
.pg-setting:last-child { margin-bottom: 0; }
|
||||
.pg-setting-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||
.pg-setting-val { color: var(--pg-accent); font-size: 14px; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
||||
.pg-setting input[type="range"] {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
cursor: pointer;
|
||||
accent-color: var(--pg-accent);
|
||||
}
|
||||
.pg-setting input[type="range"]::-webkit-slider-runnable-track {
|
||||
height: 6px;
|
||||
border-radius: 99px;
|
||||
background: linear-gradient(to right, var(--pg-accent) var(--pg-progress), #3c4458 var(--pg-progress));
|
||||
}
|
||||
.pg-setting input[type="range"]::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-top: -7px;
|
||||
border: 3px solid #191e2a;
|
||||
border-radius: 50%;
|
||||
background: var(--pg-accent);
|
||||
box-shadow: 0 0 0 1px var(--pg-accent);
|
||||
}
|
||||
.pg-setting input[type="range"]::-moz-range-track { height: 6px; border-radius: 99px; background: #3c4458; }
|
||||
.pg-setting input[type="range"]::-moz-range-progress { height: 6px; border-radius: 99px; background: var(--pg-accent); }
|
||||
.pg-setting input[type="range"]::-moz-range-thumb { width: 16px; height: 16px; border: 2px solid #191e2a; border-radius: 50%; background: var(--pg-accent); }
|
||||
.pg-setting input[type="range"]:focus-visible { outline: 2px solid var(--pg-accent); outline-offset: 3px; }
|
||||
.pg-settings-screen .pg-setting select { width: 100%; min-height: 44px; }
|
||||
.pg-settings-screen .pg-wide { width: 100%; min-height: 44px; margin: 0; }
|
||||
@container (max-width: 620px) {
|
||||
.pg-settings { grid-template-columns: minmax(0, 1fr); }
|
||||
.pg-settings-card:first-child { grid-row: auto; }
|
||||
.pg-settings-card { padding: 18px; }
|
||||
}
|
||||
.pg-wide { margin-top: 0.8em; margin-right: 0.6em; }
|
||||
|
||||
.pg-screen.pg-main { background-color: #000; justify-content: center; }
|
||||
.pg-main-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.55em;
|
||||
width: min(26em, 100%);
|
||||
padding: 2em;
|
||||
background: rgba(8, 10, 20, 0.72);
|
||||
backdrop-filter: blur(14px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 1em;
|
||||
box-shadow: 0 1em 3em rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.pg-main-panel h1 { margin: 0 0 0.5em; font-size: 1.6em; color: #fff; letter-spacing: -0.01em; }
|
||||
.pg-main-panel button { text-align: left; padding: 0.7em 1.2em; }
|
||||
.pg-main-panel button:first-of-type { background: var(--pg-accent); border-color: var(--pg-accent); color: #1a1205; font-weight: 650; }
|
||||
.pg-main-panel button:first-of-type:hover { filter: brightness(1.1); }
|
||||
|
||||
/* menu button outside the scaled stage: stays tappable on phones */
|
||||
.pg-hud-btn {
|
||||
position: absolute;
|
||||
top: max(10px, env(safe-area-inset-top));
|
||||
right: max(10px, env(safe-area-inset-right));
|
||||
z-index: 20;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
color: #fff;
|
||||
background: rgba(8, 10, 20, 0.55);
|
||||
backdrop-filter: blur(6px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s, background 0.15s;
|
||||
}
|
||||
.pg-root:hover .pg-hud-btn, .pg-hud-btn:focus-visible { opacity: 0.8; }
|
||||
.pg-hud-btn:hover { opacity: 1 !important; background: rgba(8, 10, 20, 0.85); }
|
||||
.pg-root.pg-in-main .pg-hud-btn { display: none; }
|
||||
@media (pointer: coarse) {
|
||||
.pg-hud-btn { opacity: 0.6; }
|
||||
}
|
||||
|
||||
/* in-player confirmation */
|
||||
.pg-dialog-back {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 30;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 16px;
|
||||
background: rgba(5, 7, 12, 0.7);
|
||||
backdrop-filter: blur(4px);
|
||||
font-family: "Segoe UI", system-ui, sans-serif;
|
||||
}
|
||||
.pg-dialog {
|
||||
width: min(420px, 100%);
|
||||
padding: 22px;
|
||||
color: #eceef5;
|
||||
background: #171b25;
|
||||
border: 1px solid #2a3042;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.55);
|
||||
font-size: 16px;
|
||||
}
|
||||
.pg-dialog p { margin: 0 0 18px; line-height: 1.45; }
|
||||
.pg-dialog-row { display: flex; justify-content: flex-end; gap: 10px; }
|
||||
.pg-dialog button { font-size: 15px; padding: 9px 18px; }
|
||||
.pg-dialog .pg-primary { background: #ff9f43; border-color: #ff9f43; color: #1a1205; font-weight: 650; }
|
||||
|
||||
/* --- game screens (Ren'Py screen language) ------------------------------------------ */
|
||||
.pg-usr { position: absolute; inset: 0; pointer-events: none; }
|
||||
.pg-sinst { position: absolute; inset: 0; pointer-events: none; }
|
||||
.pg-sinst.pg-smodal { pointer-events: auto; }
|
||||
.pg-sw { pointer-events: none; min-width: 0; min-height: 0; color: var(--pg-text, #fff); font-size: var(--pg-font, 24px); }
|
||||
.pg-sframe, .pg-swindow, .pg-sviewport, .pg-sbar, .pg-sbtn { pointer-events: auto; }
|
||||
.pg-sbtn { cursor: pointer; user-select: none; }
|
||||
.pg-sbtn.pg-sdisabled { cursor: default; }
|
||||
.pg-sbtn:focus-visible { outline: 2px solid var(--pg-accent, #4af); outline-offset: 2px; }
|
||||
.pg-simgbtn img, .pg-simgbtn .pg-simg { display: block; pointer-events: none; }
|
||||
.pg-stext { white-space: pre-wrap; }
|
||||
.pg-sbar { background: rgba(255, 255, 255, 0.18); min-width: 40px; min-height: 12px; }
|
||||
.pg-sbar-fill { background: var(--pg-accent, #4af); width: 100%; height: 100%; }
|
||||
.pg-sbar:not(.pg-svbar) .pg-sbar-fill { height: 100%; }
|
||||
.pg-sadd { pointer-events: none; }
|
||||
.pg-gallery-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; align-content: start; }
|
||||
.pg-gallery-list h3, .pg-gallery-list p { grid-column: 1 / -1; }
|
||||
.pg-sviewport { scrollbar-width: thin; }
|
||||
|
||||
/* --- renpy.input / renpy.display_menu ----------------------------------------------- */
|
||||
.pg-input { display: flex; flex-direction: column; gap: 12px; align-items: center; pointer-events: auto; }
|
||||
.pg-input input { font: inherit; font-size: var(--pg-font, 24px); padding: 6px 10px; min-width: 40%; }
|
||||
.pg-caption { color: var(--pg-text, #fff); opacity: 0.8; text-align: center; padding: 4px 0; }
|
||||
|
||||
/* checkbox row of the settings screen */
|
||||
.pg-check { display: flex; align-items: center; gap: 10px; margin: 4px 0 14px; cursor: pointer; font-size: 14px; }
|
||||
.pg-check input { width: 18px; height: 18px; accent-color: var(--pg-accent, #f5a040); }
|
||||
|
||||
/* SnowBlossom: pictures that fall and drift while the scene is shown */
|
||||
.pg-snow { position: absolute; inset: 0; overflow: hidden; pointer-events: none; }
|
||||
.pg-snow > * { position: absolute; top: -10%; animation: pg-snow-fall linear infinite; }
|
||||
@keyframes pg-snow-fall { from { transform: translate(0, 0); } to { transform: translate(var(--drift, 40px), 120vh); } }
|
||||
@@ -28,6 +28,7 @@ export interface StyleDef {
|
||||
}
|
||||
|
||||
export interface ScreenHost {
|
||||
galleryUnlocked?: boolean;
|
||||
py: Python;
|
||||
fileUrl(path: string): string | null;
|
||||
/** Element for an image name, file path or colour. */
|
||||
@@ -1690,7 +1691,7 @@ export class ScreenManager {
|
||||
if (isNativeAction(a)) return a.get_sensitive();
|
||||
if (a instanceof PyInstance) return this.askAction(a, "get_sensitive", true);
|
||||
return !(a instanceof ScreenAction) ||
|
||||
(a.kind === "SensitiveIf" ? truthy(a.args[0]) : a.kind === "Replay" ? !truthy(a.kw.locked ?? a.args[2]) : true);
|
||||
(a.kind === "SensitiveIf" ? truthy(a.args[0]) : a.kind === "Replay" ? this.host.galleryUnlocked || !truthy(a.kw.locked ?? a.args[2]) : true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2071,6 +2072,7 @@ export class ScreenManager {
|
||||
out.Image = shape("image");
|
||||
out.ImageReference = shape("image");
|
||||
out.Gallery = galleryClass({
|
||||
unlocked: () => !!this.host.galleryUnlocked,
|
||||
seen: (name) => truthy(this.host.py.eval({ k: "call", f: { k: "attr", o: { k: "name", id: "renpy" }, a: "seen_image" }, a: [{ k: "const", v: name }], kw: {} })),
|
||||
evaluate: (source) => this.host.py.eval(parseExpression(source)),
|
||||
show: (pictures, transition) => this.showGallery(pictures, transition),
|
||||
|
||||
@@ -148,6 +148,7 @@ export interface GameData {
|
||||
}
|
||||
|
||||
export interface Manifest {
|
||||
galleryUnlocked?: boolean;
|
||||
versionId?: number;
|
||||
revision?: string;
|
||||
format: string;
|
||||
|
||||
Reference in new issue
Block a user