Fix MIST gallery scene controls, video positioning and audio overlap
This commit is contained in:
1 parent
19924e1c5a
commit
4dc660f786
10 files changed
+197
-12
No files matched your search
@@ -147,6 +147,14 @@ export class AudioSystem {
|
||||
|
||||
stop(ch: string, fadeout = 0) {
|
||||
const c = this.channels.get(ch);
|
||||
// Rapid script advances can replace a track before its previous crossfade
|
||||
// ends. Retire that older tail instead of accumulating music on one channel.
|
||||
for (const [el, playback] of this.playing) {
|
||||
if (playback.channel !== ch || el === c?.el) continue;
|
||||
playback.cancelFade();
|
||||
el.pause();
|
||||
this.playing.delete(el);
|
||||
}
|
||||
if (!c?.el) return;
|
||||
const el = c.el;
|
||||
c.el = null;
|
||||
|
||||
@@ -249,6 +249,7 @@ export class Engine {
|
||||
this.gen++;
|
||||
this.wakeAll();
|
||||
this.audio.stopAll();
|
||||
this.renderer.reset();
|
||||
this.resizeObserver.disconnect();
|
||||
window.removeEventListener("keydown", this.onKey);
|
||||
window.removeEventListener("keyup", this.onKeyUp);
|
||||
@@ -1480,7 +1481,14 @@ 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";
|
||||
if (truthy(this.store.replaywatch)) {
|
||||
// Scene endings restore the room's HUD before this jump. The isolated
|
||||
// gallery bypasses the room label, so retire that HUD and scene media here.
|
||||
this.screens.clear();
|
||||
this.audio.stopAll();
|
||||
this.renderer.reset();
|
||||
target = "rewatchscene";
|
||||
}
|
||||
else { this.closeUnlockedGallery(); return; }
|
||||
}
|
||||
if (target === "main_menu_screen" || target === "_main_menu_screen") target = "__main_menu__";
|
||||
@@ -2039,15 +2047,15 @@ export class Engine {
|
||||
}
|
||||
if (this.skip) return Promise.resolve();
|
||||
return this.wait((done) => {
|
||||
const timer = window.setTimeout(() => {
|
||||
this.onAdvance = null;
|
||||
done();
|
||||
}, seconds * 1000);
|
||||
this.onAdvance = () => {
|
||||
const finish = () => {
|
||||
clearTimeout(timer);
|
||||
this.onAdvance = null;
|
||||
this.skipTick = null;
|
||||
done();
|
||||
};
|
||||
const timer = window.setTimeout(finish, seconds * 1000);
|
||||
this.onAdvance = finish;
|
||||
this.skipTick = () => { if (this.skip) finish(); };
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
.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-layer { position: absolute; inset: 0; pointer-events: none; }
|
||||
.pg-transition { pointer-events: none; }
|
||||
.pg-transition > .pg-old { position: absolute; inset: 0; background: #000; }
|
||||
.pg-fill { position: absolute; inset: 0; }
|
||||
|
||||
@@ -239,6 +239,7 @@ export class RenderHost {
|
||||
private events(root: HTMLElement, value: PyInstance, started: number) {
|
||||
const handler = this.options.py.attrOf(value, "event");
|
||||
if (typeof handler !== "function") return;
|
||||
root.style.pointerEvents = "auto";
|
||||
const send = (type: number, ev: PointerEvent, button = 0) => {
|
||||
const box = root.getBoundingClientRect();
|
||||
const scale = box.width && root.offsetWidth ? root.offsetWidth / box.width : 1;
|
||||
|
||||
@@ -43,6 +43,7 @@ export interface RendererHost {
|
||||
interface Entry extends ShownImage {
|
||||
el: HTMLElement;
|
||||
runners: { runner: AtlRunner; block: AtlBlock }[];
|
||||
resize?: ResizeObserver;
|
||||
}
|
||||
|
||||
/** One of the game's layers (config.layers): its own pictures, its own transform (camera) and, if screens are shown on it, their container. */
|
||||
@@ -166,6 +167,7 @@ export class Renderer {
|
||||
const layer = this.layers.get(layerName);
|
||||
if (!layer) return;
|
||||
for (const e of layer.shown.values()) {
|
||||
e.resize?.disconnect();
|
||||
e.runners.forEach((r) => r.runner.stop());
|
||||
e.el.remove();
|
||||
}
|
||||
@@ -192,6 +194,7 @@ export class Renderer {
|
||||
const entry: Entry = { tag, name: img.name, at: atSpecs, zorder: opts.zorder ?? prev?.zorder ?? 0, el,
|
||||
runners: [] };
|
||||
prev?.runners.forEach((r) => r.runner.stop());
|
||||
prev?.resize?.disconnect();
|
||||
if (prev) {
|
||||
prev.el.replaceWith(el);
|
||||
} else {
|
||||
@@ -201,6 +204,12 @@ export class Renderer {
|
||||
}
|
||||
layer.shown.set(tag, entry);
|
||||
this.style(el, { ...base }); // now that it is in the page it has a size
|
||||
// Movies acquire their intrinsic dimensions after metadata loads. Animations
|
||||
// can also change size when replacing a frame. Keep the current ATL position.
|
||||
entry.resize = new ResizeObserver(() => {
|
||||
if (el.isConnected && (el as any).__style) this.style(el, (el as any).__style);
|
||||
});
|
||||
entry.resize.observe(el);
|
||||
this.animate(entry, base, [...(img.def.at ?? []), ...atSpecs], img.def.timeline);
|
||||
}
|
||||
|
||||
@@ -254,6 +263,7 @@ export class Renderer {
|
||||
const layer = this.layers.get(layerName);
|
||||
const e = layer?.shown.get(tag);
|
||||
if (!layer || !e) return;
|
||||
e.resize?.disconnect();
|
||||
layer.shown.delete(tag);
|
||||
const exit = e.runners.find((r) => r.runner.hasHideHandler(r.block));
|
||||
if (!exit || this.fast) {
|
||||
|
||||
@@ -1400,6 +1400,19 @@ export class ScreenManager {
|
||||
const sensitive = props.sensitive ? truthy(this.ev(props.sensitive)) && this.actionSensitive(action) : this.actionSensitive(action);
|
||||
const selected = props.selected ? truthy(this.ev(props.selected)) : this.actionSelected(action);
|
||||
let hover = false;
|
||||
// Keep every state in the same grid cell. Replacing the idle image with an
|
||||
// unloaded hover image collapses the hit area and causes enter/leave loops.
|
||||
const pictures = new Map<any, HTMLElement>();
|
||||
if (images) for (const file of Object.values(images)) {
|
||||
if (!file || pictures.has(file)) continue;
|
||||
const picture = this.displayable(file);
|
||||
if (!picture) continue;
|
||||
picture.classList.add("pg-simg");
|
||||
picture.style.gridArea = "1 / 1";
|
||||
picture.style.pointerEvents = "none";
|
||||
pictures.set(file, picture);
|
||||
el.append(picture);
|
||||
}
|
||||
const paint = () => {
|
||||
const state = !sensitive ? (selected ? "selected_insensitive" : "insensitive")
|
||||
: selected ? (hover ? "selected_hover" : "selected_idle") : hover ? "hover" : "idle";
|
||||
@@ -1409,10 +1422,7 @@ export class ScreenManager {
|
||||
}
|
||||
el.dataset.state = state;
|
||||
if (images) {
|
||||
const file = images[state];
|
||||
const picture = file ? this.displayable(file) : null;
|
||||
el.replaceChildren(...(picture ? [picture] : []));
|
||||
picture?.classList.add("pg-simg");
|
||||
for (const [file, picture] of pictures) picture.style.opacity = file === images[state] ? "1" : "0";
|
||||
} else {
|
||||
this.look(el, props, state);
|
||||
if (label) this.textLook(label, { ...props, ...Object.fromEntries(Object.entries(textProps).map(([k, v]) => [k, v])) }, state);
|
||||
@@ -1741,7 +1751,9 @@ export class ScreenManager {
|
||||
|
||||
/** Run what a widget's `action` evaluated to. */
|
||||
run(action: any, screen: Instance | null) {
|
||||
if (this.suspended) return;
|
||||
// Absent hover actions must not rebuild the button under the pointer:
|
||||
// its replacement would receive mouseenter again and replay hover_sound.
|
||||
if (this.suspended || action === null || action === undefined || action === false) return;
|
||||
// actions act on the game, not on the screen's own variable scope
|
||||
const { py } = this.host;
|
||||
const savedFn = py.fn;
|
||||
|
||||
Reference in new issue
Block a user