Spaces:
Running
Running
| // ../auto-battler/src/render/spriteSheet.js | |
| var SHEET_ROWS = 4; | |
| var cellOf = (height) => Math.round(height / SHEET_ROWS); | |
| function sliceGridWith(pixi, texture, cell = cellOf(texture.source.height)) { | |
| const { Texture, Rectangle } = pixi; | |
| const src = texture.source; | |
| const rows = Math.max(1, Math.round(src.height / cell)); | |
| const cols = Math.max(1, Math.round(src.width / cell)); | |
| return Array.from({ length: rows }, (_, r) => Array.from({ length: cols }, (_2, c) => new Texture({ source: src, frame: new Rectangle(c * cell, r * cell, cell, cell) }))); | |
| } | |
| var ROW_FOR = { "front-right": 0, "front-left": 1, "back-right": 2, "back-left": 3 }; | |
| var rowFor = (grid, facing) => grid[ROW_FOR[facing]] ?? grid[0]; | |
| // ../auto-battler/src/render/spriteScene.js | |
| var SPEED = 3; | |
| var PROJ_SPEED = 5; | |
| var SCALE = 4; | |
| var ROWS = 4; | |
| var ARRIVE = SPEED; | |
| var ANIM_SPEED = { idle: 0.12, walk: 0.18, attack: 0.22, dmg: 0.2, die: 0.16, jump: 0.2 }; | |
| var SCENE_ACTIONS = [ | |
| { state: "attack", code: "Space", label: "Space", verb: "attack" }, | |
| { state: "dmg", code: "KeyH", label: "H", verb: "hurt" }, | |
| { state: "die", code: "KeyK", label: "K", verb: "die" }, | |
| { state: "jump", code: "KeyJ", label: "J", verb: "jump" } | |
| ]; | |
| var STATE_KEYS = ["idle", "walk", ...SCENE_ACTIONS.map((a) => a.state), "attackDiagonal"]; | |
| var orthoRow = (d) => d.x > 0 ? 0 : d.x < 0 ? 1 : d.y > 0 ? 2 : 3; | |
| var diagRow = (d) => d.y > 0 ? d.x > 0 ? 0 : 1 : d.x > 0 ? 2 : 3; | |
| var usesAimedAttack = (c) => c?.attackVerb === "shoot" || !!c?.attackOrtho; | |
| function createSpriteScene(pixi, host, opts = {}) { | |
| const { Application, Assets, AnimatedSprite, Graphics } = pixi; | |
| const urlFor = opts.urlFor || ((u) => u); | |
| const anim = { ...ANIM_SPEED, ...opts.anim || {} }; | |
| const sliceGrid = (texture, cell) => sliceGridWith(pixi, texture, cell); | |
| let app = null; | |
| let sprite = null; | |
| let shadow = null; | |
| let overlay = null; | |
| let frames = null; | |
| let keys = { x: 0, y: 0 }; | |
| let moveTarget = null; | |
| let action = null; | |
| let pendingAction = null; | |
| let dieHold = false; | |
| let side = "right"; | |
| let depth = "front"; | |
| let dir = { x: 1, y: 1 }; | |
| let shoot = false; | |
| let applied = ""; | |
| let extras = []; | |
| let effectsOn = true; | |
| let shadowsOn = true; | |
| let flying = []; | |
| let marker = null; | |
| let markerLife = 0; | |
| let currentKind = "idle"; | |
| let lastAimKey = ""; | |
| let changeCb = null; | |
| let destroyed = false; | |
| function snapshot() { | |
| return { | |
| kind: currentKind, | |
| facing: `${depth}-${side}`, | |
| aim: `${dir.x},${dir.y}`, | |
| shoot, | |
| moving: !!moveTarget || keys.x !== 0 || keys.y !== 0, | |
| x: sprite ? sprite.x : null, | |
| y: sprite ? sprite.y : null, | |
| toggles: { effects: effectsOn, shadows: shadowsOn } | |
| }; | |
| } | |
| function emit() { | |
| if (changeCb) changeCb(snapshot()); | |
| } | |
| const ready = (async () => { | |
| const a = new Application(); | |
| await a.init({ background: 15524556, antialias: false, resizeTo: host }); | |
| if (destroyed) { | |
| a.destroy(true, { children: true }); | |
| return; | |
| } | |
| app = a; | |
| app.canvas.setAttribute("data-testid", "pixi-canvas"); | |
| app.canvas.style.touchAction = "none"; | |
| app.canvas.addEventListener("pointerdown", onPointerDown); | |
| app.canvas.addEventListener("click", onPointerDown); | |
| host.appendChild(app.canvas); | |
| app.ticker.add(tick); | |
| })(); | |
| function onPointerDown(e) { | |
| if (!app) return; | |
| const rect = app.canvas.getBoundingClientRect(); | |
| if (!rect.width || !rect.height) return; | |
| const x = (e.clientX - rect.left) / rect.width * app.screen.width; | |
| const y = (e.clientY - rect.top) / rect.height * app.screen.height; | |
| moveTo(x, y); | |
| } | |
| function moveTo(x, y) { | |
| if (!app) return; | |
| const tx = Math.max(0, Math.min(app.screen.width, x)); | |
| const ty = Math.max(0, Math.min(app.screen.height, y)); | |
| moveTarget = { x: tx, y: ty }; | |
| if (Graphics) { | |
| if (!marker) { | |
| marker = new Graphics(); | |
| app.stage.addChildAt(marker, 0); | |
| } | |
| marker.clear(); | |
| marker.circle(0, 0, 11).stroke({ color: 7035903, width: 2, alpha: 1 }); | |
| marker.position.set(tx, ty); | |
| marker.alpha = 0.9; | |
| markerLife = 30; | |
| } | |
| } | |
| function tick(ticker) { | |
| const s = sprite; | |
| const f = frames; | |
| if (!s || !f) return; | |
| let vx = keys.x; | |
| let vy = keys.y; | |
| let usingTarget = false; | |
| let seekDX = 0; | |
| let seekDY = 0; | |
| if (vx === 0 && vy === 0 && moveTarget) { | |
| seekDX = moveTarget.x - s.x; | |
| seekDY = moveTarget.y - s.y; | |
| const d = Math.hypot(seekDX, seekDY); | |
| if (d <= ARRIVE) { | |
| s.x = moveTarget.x; | |
| s.y = moveTarget.y; | |
| moveTarget = null; | |
| } else { | |
| vx = seekDX; | |
| vy = seekDY; | |
| usingTarget = true; | |
| } | |
| } | |
| const wantMove = vx !== 0 || vy !== 0; | |
| if (pendingAction) { | |
| const pend = pendingAction; | |
| pendingAction = null; | |
| if (f[pend] && (action === null || dieHold)) { | |
| action = pend; | |
| dieHold = false; | |
| applied = ""; | |
| } | |
| } | |
| if (dieHold && wantMove) { | |
| action = null; | |
| dieHold = false; | |
| } | |
| const acting = action !== null; | |
| if (!acting && wantMove) { | |
| let sgx, sgy; | |
| if (usingTarget) { | |
| sgx = Math.abs(seekDX) > 4 ? Math.sign(seekDX) : 0; | |
| sgy = Math.abs(seekDY) > 4 ? Math.sign(seekDY) : 0; | |
| } else { | |
| sgx = Math.sign(vx); | |
| sgy = Math.sign(vy); | |
| } | |
| if (sgx > 0) side = "right"; | |
| else if (sgx < 0) side = "left"; | |
| if (sgy > 0) depth = "front"; | |
| else if (sgy < 0) depth = "back"; | |
| if (sgx || sgy) dir = { x: sgx, y: sgy }; | |
| const len = Math.hypot(vx, vy) || 1; | |
| const step = SPEED * ticker.deltaTime; | |
| s.x += vx / len * step; | |
| s.y += vy / len * step; | |
| const halfW = s.width / 2; | |
| const halfH = s.height / 2; | |
| s.x = Math.max(halfW, Math.min(app.screen.width - halfW, s.x)); | |
| s.y = Math.max(halfH, Math.min(app.screen.height - halfH, s.y)); | |
| } | |
| const aimKey = `${dir.x},${dir.y}`; | |
| if (aimKey !== lastAimKey) { | |
| lastAimKey = aimKey; | |
| emit(); | |
| } | |
| if (shadow) { | |
| shadow.x = s.x; | |
| shadow.y = s.y; | |
| } | |
| if (overlay?.visible) { | |
| overlay.x = s.x; | |
| overlay.y = s.y; | |
| } | |
| if (marker && markerLife > 0) { | |
| markerLife -= ticker.deltaTime; | |
| const t = Math.max(0, markerLife / 30); | |
| marker.alpha = t * 0.9; | |
| marker.scale.set(1 + (1 - t) * 0.6); | |
| } | |
| for (let i = flying.length - 1; i >= 0; i--) { | |
| const fl = flying[i]; | |
| fl.sprite.x += fl.dx * PROJ_SPEED * ticker.deltaTime; | |
| fl.sprite.y += fl.dy * PROJ_SPEED * ticker.deltaTime; | |
| if (!fl.impFired && fl.impGrid) { | |
| const dist = Math.hypot(fl.sprite.x - fl.startX, fl.sprite.y - fl.startY); | |
| if (dist > 150) { | |
| fl.impFired = true; | |
| const imp = new AnimatedSprite(fl.impGrid[0]); | |
| imp.anchor.set(0.5); | |
| imp.scale.set(SCALE); | |
| imp.loop = false; | |
| imp.animationSpeed = anim.attack; | |
| imp.x = fl.sprite.x; | |
| imp.y = fl.sprite.y; | |
| imp.onComplete = () => { | |
| if (imp.parent) imp.parent.removeChild(imp); | |
| imp.destroy(); | |
| }; | |
| app.stage.addChild(imp); | |
| imp.gotoAndPlay(0); | |
| } | |
| } | |
| const os = fl.sprite.x < -128 || fl.sprite.x > app.screen.width + 128 || fl.sprite.y < -128 || fl.sprite.y > app.screen.height + 128; | |
| if (os) { | |
| if (fl.sprite.parent) fl.sprite.parent.removeChild(fl.sprite); | |
| fl.sprite.destroy(); | |
| flying.splice(i, 1); | |
| } | |
| } | |
| const kind = acting ? action : wantMove ? "walk" : "idle"; | |
| currentKind = kind; | |
| let fr, key; | |
| if (kind === "attack" && shoot) { | |
| const d = dir; | |
| const useDiag = d.x !== 0 && d.y !== 0 && !!f.attackDiagonal; | |
| const grid = useDiag ? f.attackDiagonal : f.attack; | |
| const row = useDiag ? diagRow(d) : orthoRow(d); | |
| fr = grid[row] ?? grid[0]; | |
| key = `shoot:${useDiag ? "d" : "o"}${row}`; | |
| } else { | |
| const facing = `${depth}-${side}`; | |
| fr = rowFor(f[kind], facing); | |
| key = `${kind}:${facing}`; | |
| } | |
| if (key !== applied) { | |
| applied = key; | |
| const oneShot = kind !== "idle" && kind !== "walk"; | |
| s.textures = fr; | |
| s.loop = !oneShot; | |
| s.animationSpeed = anim[kind] ?? (oneShot ? anim.attack : anim.walk); | |
| s.gotoAndPlay(0); | |
| const actionKey = action ?? kind; | |
| const facing = `${depth}-${side}`; | |
| if (shadow) { | |
| let shadGrid; | |
| if (kind === "attack" && shoot) { | |
| const d = dir; | |
| const useDiag = d.x !== 0 && d.y !== 0 && !!f["shd:attackDiagonal"]; | |
| shadGrid = useDiag ? f["shd:attackDiagonal"] : f["shd:attack"]; | |
| if (shadGrid) { | |
| const row = useDiag ? diagRow(d) : orthoRow(d); | |
| shadow.textures = shadGrid[row] ?? shadGrid[0]; | |
| } | |
| } else { | |
| shadGrid = f["shd:" + actionKey]; | |
| if (shadGrid) shadow.textures = rowFor(shadGrid, facing); | |
| } | |
| if (shadGrid && shadowsOn) { | |
| shadow.loop = s.loop; | |
| shadow.animationSpeed = s.animationSpeed; | |
| shadow.visible = true; | |
| shadow.gotoAndPlay(0); | |
| } else { | |
| shadow.visible = false; | |
| } | |
| } | |
| if (overlay) { | |
| const effGrid = effectsOn && f["eff:" + actionKey]; | |
| if (effGrid && oneShot) { | |
| overlay.textures = rowFor(effGrid, facing); | |
| overlay.loop = false; | |
| overlay.animationSpeed = s.animationSpeed; | |
| overlay.x = s.x; | |
| overlay.y = s.y; | |
| overlay.visible = true; | |
| overlay.gotoAndPlay(0); | |
| } else { | |
| overlay.visible = false; | |
| } | |
| } | |
| if (oneShot && effectsOn) { | |
| const projGrid = f["proj:" + actionKey]; | |
| if (projGrid) { | |
| const d = dir; | |
| const row = orthoRow(d); | |
| const proj = new AnimatedSprite(projGrid[row] ?? projGrid[0]); | |
| proj.anchor.set(0.5); | |
| proj.scale.set(SCALE); | |
| proj.loop = true; | |
| proj.animationSpeed = anim.attack; | |
| proj.x = s.x; | |
| proj.y = s.y; | |
| proj.gotoAndPlay(0); | |
| app.stage.addChild(proj); | |
| const len = Math.hypot(d.x, d.y) || 1; | |
| flying.push({ | |
| sprite: proj, | |
| dx: d.x / len, | |
| dy: d.y / len, | |
| startX: s.x, | |
| startY: s.y, | |
| impGrid: f["imp:" + actionKey] ?? null, | |
| impFired: false | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| async function setCharacter(active) { | |
| await ready; | |
| if (!app || !active) return; | |
| const sheets = [ | |
| ...STATE_KEYS.filter((k) => active[k]).map((k) => ({ gridKey: k, url: active[k], type: "body" })), | |
| ...(active.extras ?? []).map((e) => ({ gridKey: "x:" + e.key, url: e.url, type: "body" })), | |
| ...active.attackEffect ? [{ gridKey: "eff:attack", url: active.attackEffect, type: "eff" }] : [], | |
| ...active.attackProjectile ? [{ gridKey: "proj:attack", url: active.attackProjectile, type: "proj" }] : [], | |
| ...active.attackImpact ? [{ gridKey: "imp:attack", url: active.attackImpact, type: "imp" }] : [], | |
| ...Object.entries(active.shadows ?? {}).map(([k, url]) => ({ gridKey: "shd:" + k, url, type: "body" })), | |
| ...(active.extras ?? []).flatMap((e) => [ | |
| e.effect ? { gridKey: "eff:x:" + e.key, url: e.effect, type: "eff" } : null, | |
| e.projectile ? { gridKey: "proj:x:" + e.key, url: e.projectile, type: "proj" } : null, | |
| e.impact ? { gridKey: "imp:x:" + e.key, url: e.impact, type: "imp" } : null, | |
| e.shadow ? { gridKey: "shd:x:" + e.key, url: e.shadow, type: "body" } : null | |
| ].filter(Boolean)) | |
| ]; | |
| const texs = await Promise.all(sheets.map((s) => Assets.load(urlFor(s.url)).then((t) => t, () => null))); | |
| if (!app) return; | |
| const idleIdx = sheets.findIndex((s) => s.gridKey === "idle"); | |
| if (idleIdx < 0 || !texs[idleIdx]) return; | |
| const grids = {}; | |
| const cell = cellOf(texs[idleIdx].source.height); | |
| sheets.forEach((s, i) => { | |
| const t = texs[i]; | |
| if (!t) return; | |
| t.source.scaleMode = "nearest"; | |
| if (s.type === "proj") grids[s.gridKey] = sliceGrid(t, Math.max(1, Math.round(t.source.height / ROWS))); | |
| else if (s.type === "imp") grids[s.gridKey] = sliceGrid(t, Math.max(1, t.source.height)); | |
| else grids[s.gridKey] = sliceGrid(t, cell); | |
| }); | |
| frames = grids; | |
| extras = active.extras ?? []; | |
| shoot = usesAimedAttack(active); | |
| action = null; | |
| pendingAction = null; | |
| dieHold = false; | |
| applied = ""; | |
| lastAimKey = ""; | |
| const startFrames = rowFor(grids.idle, `${depth}-${side}`); | |
| if (!sprite) { | |
| const shd = new AnimatedSprite(startFrames); | |
| shd.anchor.set(0.5); | |
| shd.scale.set(SCALE); | |
| shd.loop = true; | |
| shd.visible = false; | |
| shd.animationSpeed = anim.idle; | |
| shd.x = app.screen.width / 2; | |
| shd.y = app.screen.height / 2; | |
| app.stage.addChild(shd); | |
| shadow = shd; | |
| const s = new AnimatedSprite(startFrames); | |
| s.anchor.set(0.5); | |
| s.scale.set(SCALE); | |
| s.loop = true; | |
| s.animationSpeed = anim.idle; | |
| s.x = app.screen.width / 2; | |
| s.y = app.screen.height / 2; | |
| s.onComplete = () => { | |
| if (action === "die") dieHold = true; | |
| else action = null; | |
| }; | |
| app.stage.addChild(s); | |
| s.gotoAndPlay(0); | |
| sprite = s; | |
| const ov = new AnimatedSprite(startFrames); | |
| ov.anchor.set(0.5); | |
| ov.scale.set(SCALE); | |
| ov.loop = false; | |
| ov.visible = false; | |
| ov.animationSpeed = anim.attack; | |
| ov.onComplete = () => { | |
| ov.visible = false; | |
| }; | |
| app.stage.addChild(ov); | |
| overlay = ov; | |
| } else { | |
| const s = sprite; | |
| s.loop = true; | |
| s.textures = startFrames; | |
| s.animationSpeed = anim.idle; | |
| s.gotoAndPlay(0); | |
| if (shadow) shadow.visible = false; | |
| if (overlay) overlay.visible = false; | |
| for (const fl of flying) { | |
| if (fl.sprite.parent) fl.sprite.parent.removeChild(fl.sprite); | |
| fl.sprite.destroy(); | |
| } | |
| flying = []; | |
| } | |
| emit(); | |
| } | |
| function setVelocity(v) { | |
| keys = { x: v?.x || 0, y: v?.y || 0 }; | |
| if (keys.x || keys.y) moveTarget = null; | |
| } | |
| function triggerAction(stateKey) { | |
| if (stateKey) pendingAction = stateKey; | |
| } | |
| function setToggles(t) { | |
| if (t && "effects" in t) effectsOn = !!t.effects; | |
| if (t && "shadows" in t) { | |
| shadowsOn = !!t.shadows; | |
| if (shadow && !shadowsOn) shadow.visible = false; | |
| } | |
| emit(); | |
| } | |
| return { | |
| ready, | |
| setCharacter, | |
| setVelocity, | |
| triggerAction, | |
| moveTo, | |
| setToggles, | |
| getSnapshot: snapshot, | |
| onChange: (cb) => { | |
| changeCb = cb; | |
| }, | |
| resize: () => { | |
| if (app) app.resize(); | |
| }, | |
| // Re-place the character at the centre of the current canvas — used by hosts | |
| // that mount the stage in a hidden/0-size tab and reveal it later (the Space). | |
| recenter: () => { | |
| if (!app || !sprite) return; | |
| sprite.x = app.screen.width / 2; | |
| sprite.y = app.screen.height / 2; | |
| if (shadow) { | |
| shadow.x = sprite.x; | |
| shadow.y = sprite.y; | |
| } | |
| moveTarget = null; | |
| }, | |
| destroy: () => { | |
| destroyed = true; | |
| const a = app; | |
| app = null; | |
| if (a) { | |
| a.canvas.removeEventListener("pointerdown", onPointerDown); | |
| a.canvas.removeEventListener("click", onPointerDown); | |
| a.destroy(true, { children: true }); | |
| } | |
| sprite = shadow = overlay = frames = marker = null; | |
| flying = []; | |
| } | |
| }; | |
| } | |
| // ../auto-battler/src/render/spritePlayground.js | |
| var COMPASS_CELLS = [ | |
| { dir: "-1,-1", glyph: "\u2196" }, | |
| { dir: "0,-1", glyph: "\u2191" }, | |
| { dir: "1,-1", glyph: "\u2197" }, | |
| { dir: "-1,0", glyph: "\u2190" }, | |
| { dir: "0,0", glyph: "\xB7" }, | |
| { dir: "1,0", glyph: "\u2192" }, | |
| { dir: "-1,1", glyph: "\u2199" }, | |
| { dir: "0,1", glyph: "\u2193" }, | |
| { dir: "1,1", glyph: "\u2198" } | |
| ]; | |
| var ACTION_BY_CODE = Object.fromEntries(SCENE_ACTIONS.map((a) => [a.code, a.state])); | |
| function el(tag, props = {}, kids = []) { | |
| const n = document.createElement(tag); | |
| for (const [k, v] of Object.entries(props)) { | |
| if (k === "class") n.className = v; | |
| else if (k === "html") n.innerHTML = v; | |
| else if (k === "dataset") Object.assign(n.dataset, v); | |
| else if (k.startsWith("on") && typeof v === "function") n.addEventListener(k.slice(2), v); | |
| else if (v != null) n.setAttribute(k, v); | |
| } | |
| for (const kid of [].concat(kids)) if (kid != null) n.append(kid); | |
| return n; | |
| } | |
| var flatList = (packs) => (packs ?? []).flatMap((p) => p.characters ?? []); | |
| function mountSpritePlayground(pixi, host, opts = {}) { | |
| const packs = opts.packs ?? []; | |
| const all = flatList(packs); | |
| let active = all.find((c) => c.slug === opts.initialSlug) ?? all[0] ?? null; | |
| let selIdx = 0; | |
| const canvasEl = el("div", { class: "movement-canvas" }); | |
| const stage = el("div", { class: "movement-stage" }, [canvasEl]); | |
| const charLinks = /* @__PURE__ */ new Map(); | |
| const picker = el("aside", { class: "movement-picker" }, [ | |
| el("h2", { class: "movement-picker-title" }, "Characters"), | |
| ...packs.map((p) => { | |
| const here = (p.characters ?? []).some((c) => c.slug === active?.slug); | |
| const lis = (p.characters ?? []).map((c) => { | |
| const a = el("a", { | |
| class: "movement-char", | |
| href: "#", | |
| dataset: { slug: c.slug }, | |
| onclick: (e) => { | |
| e.preventDefault(); | |
| setCharacter(c.slug); | |
| } | |
| }, c.name); | |
| charLinks.set(c.slug, a); | |
| return el("li", {}, a); | |
| }); | |
| return el("details", { class: "movement-pack", ...here ? { open: "" } : {} }, [ | |
| el("summary", {}, p.name), | |
| el("ul", {}, lis) | |
| ]); | |
| }) | |
| ]); | |
| const view = el("div", { class: "movement-view" }, [picker, stage]); | |
| host.appendChild(view); | |
| const scene = createSpriteScene(pixi, canvasEl, { urlFor: opts.urlFor }); | |
| let compassEl = null; | |
| let compassLabel = null; | |
| scene.onChange((snap) => { | |
| if (!compassEl) return; | |
| for (const cell of compassEl.children) { | |
| if (cell.dataset.dir != null) cell.classList.toggle("on", cell.dataset.dir === snap.aim); | |
| } | |
| const [x, y] = snap.aim.split(",").map(Number); | |
| const diag = x !== 0 && y !== 0; | |
| if (compassLabel) compassLabel.textContent = `${snap.aim} ${diag ? "diag" : "ortho"} r${diag ? diagRow({ x, y }) : orthoRow({ x, y })}`; | |
| }); | |
| function renderChrome() { | |
| stage.querySelectorAll(".movement-hint,.movement-compass-wrap,.movement-extras").forEach((n) => n.remove()); | |
| compassEl = compassLabel = null; | |
| if (!active) return; | |
| if (usesAimedAttack(active)) { | |
| compassEl = el( | |
| "div", | |
| { class: "movement-compass" }, | |
| COMPASS_CELLS.map((c) => el("span", { dataset: { dir: c.dir } }, c.glyph)) | |
| ); | |
| compassLabel = el("div", { class: "movement-compass-label" }, "1,1 diag r0"); | |
| stage.append(el("div", { class: "movement-compass-wrap", "aria-hidden": "true" }, [compassEl, compassLabel])); | |
| } | |
| const hint = el("div", { class: "movement-hint", "aria-hidden": "true" }, [ | |
| el("span", { class: "movement-key" }, "WASD"), | |
| " move \xB7 ", | |
| el("span", { class: "movement-key" }, "tap"), | |
| " walk there" | |
| ]); | |
| if (active.shadows) hint.append(" \xB7 ", toggle("shadows", true, (on) => scene.setToggles({ shadows: on }))); | |
| if ((active.extras?.length ?? 0) > 0) hint.append(" \xB7 ", toggle("effects", true, (on) => scene.setToggles({ effects: on }))); | |
| for (const a of SCENE_ACTIONS) { | |
| if (!active[a.state]) continue; | |
| const verb = a.state === "attack" && active.attackVerb ? active.attackVerb : a.verb; | |
| hint.append(" \xB7 ", el("span", { class: "movement-key" }, a.label), " " + verb); | |
| if (a.state === "attack") { | |
| for (const [f, t] of [["attackEffect", "e"], ["attackProjectile", "p"], ["attackImpact", "i"]]) | |
| if (active[f]) hint.append(el("span", { class: "movement-extra-tag" }, t)); | |
| } | |
| } | |
| stage.append(hint); | |
| if ((active.extras?.length ?? 0) > 0) { | |
| const list = el("ul", { class: "movement-extras-list" }, active.extras.map((ex, i) => { | |
| const btn = el("button", { | |
| type: "button", | |
| class: "movement-extra" + (i === selIdx ? " active" : ""), | |
| dataset: { i }, | |
| onclick: () => { | |
| selectExtra(i); | |
| scene.triggerAction("x:" + ex.key); | |
| } | |
| }, [ | |
| i < 9 ? el("span", { class: "movement-extra-num" }, String(i + 1)) : null, | |
| ex.name, | |
| ...["effect", "projectile", "impact"].filter((k) => ex[k]).map((k) => el("span", { class: "movement-extra-tag" }, k[0])) | |
| ]); | |
| return el("li", {}, btn); | |
| })); | |
| stage.append(el("div", { class: "movement-extras" }, [ | |
| el("div", { class: "movement-extras-hint", html: 'extra animations \xB7 <span class="movement-key">[</span> <span class="movement-key">]</span> cycle \xB7 <span class="movement-key">Enter</span> play \xB7 <span class="movement-key">1\u20139</span> quick' }), | |
| list | |
| ])); | |
| } | |
| } | |
| function toggle(label, checked, onChange) { | |
| const cb = el("input", { type: "checkbox", ...checked ? { checked: "" } : {} }); | |
| cb.addEventListener("change", () => onChange(cb.checked)); | |
| return el("label", { class: "movement-effects-toggle" }, [cb, " " + label]); | |
| } | |
| function selectExtra(i) { | |
| selIdx = i; | |
| stage.querySelectorAll(".movement-extra").forEach((b) => b.classList.toggle("active", +b.dataset.i === i)); | |
| } | |
| function setCharacter(slug) { | |
| const c = all.find((x) => x.slug === slug); | |
| if (!c) return; | |
| active = c; | |
| selIdx = 0; | |
| charLinks.forEach((a2, s) => a2.classList.toggle("active", s === slug)); | |
| const a = charLinks.get(slug); | |
| const det = a && a.closest("details"); | |
| if (det) det.open = true; | |
| scene.setCharacter(c); | |
| renderChrome(); | |
| } | |
| const held = { x: /* @__PURE__ */ new Set(), y: /* @__PURE__ */ new Set() }; | |
| const applyVel = () => scene.setVelocity({ x: held.x.has(1) - held.x.has(-1), y: held.y.has(1) - held.y.has(-1) }); | |
| const axis = (e) => { | |
| const k = e.key.toLowerCase(); | |
| if (k === "a" || k === "arrowleft") return ["x", -1]; | |
| if (k === "d" || k === "arrowright") return ["x", 1]; | |
| if (k === "w" || k === "arrowup") return ["y", -1]; | |
| if (k === "s" || k === "arrowdown") return ["y", 1]; | |
| return null; | |
| }; | |
| const blocked = () => { | |
| const a = document.activeElement; | |
| return a && (a.tagName === "INPUT" || a.tagName === "TEXTAREA" || a.tagName === "SELECT" || a.isContentEditable) || view.offsetParent === null; | |
| }; | |
| const onKeyDown = (e) => { | |
| if (blocked()) return; | |
| const c = axis(e); | |
| if (c) { | |
| e.preventDefault(); | |
| held[c[0]].add(c[1]); | |
| applyVel(); | |
| return; | |
| } | |
| const action = ACTION_BY_CODE[e.code]; | |
| if (action) { | |
| e.preventDefault(); | |
| if (!e.repeat) scene.triggerAction(action); | |
| return; | |
| } | |
| const ex = active?.extras ?? []; | |
| if (!ex.length) return; | |
| if (e.code === "BracketLeft") { | |
| e.preventDefault(); | |
| selectExtra((selIdx - 1 + ex.length) % ex.length); | |
| return; | |
| } | |
| if (e.code === "BracketRight") { | |
| e.preventDefault(); | |
| selectExtra((selIdx + 1) % ex.length); | |
| return; | |
| } | |
| if (e.code === "Enter") { | |
| e.preventDefault(); | |
| if (!e.repeat) scene.triggerAction("x:" + ex[selIdx].key); | |
| return; | |
| } | |
| const digit = e.code.match(/^Digit([1-9])$/); | |
| if (digit) { | |
| const i = +digit[1] - 1; | |
| if (i < ex.length) { | |
| e.preventDefault(); | |
| selectExtra(i); | |
| scene.triggerAction("x:" + ex[i].key); | |
| } | |
| } | |
| }; | |
| const onKeyUp = (e) => { | |
| if (blocked()) return; | |
| const c = axis(e); | |
| if (c) { | |
| held[c[0]].delete(c[1]); | |
| applyVel(); | |
| } | |
| }; | |
| window.addEventListener("keydown", onKeyDown); | |
| window.addEventListener("keyup", onKeyUp); | |
| renderChrome(); | |
| if (active) setCharacter(active.slug); | |
| return { | |
| setCharacter, | |
| getSnapshot: () => scene.getSnapshot(), | |
| resize: () => scene.resize(), | |
| recenter: () => scene.recenter?.(), | |
| destroy: () => { | |
| window.removeEventListener("keydown", onKeyDown); | |
| window.removeEventListener("keyup", onKeyUp); | |
| scene.destroy(); | |
| if (view.parentNode) view.parentNode.removeChild(view); | |
| } | |
| }; | |
| } | |
| export { | |
| mountSpritePlayground | |
| }; | |