diff --git a/.gitignore b/.gitignore index b3e8174..fe407e4 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,12 @@ dist dist-ssr *.local +# Electron 打包产物与冒烟/后台/关卡验证截图 +release +electron-smoke-*.png +admin-ui-*.png +level-ui-*.png + # Editor directories and files .vscode/* !.vscode/extensions.json diff --git a/build/icon.png b/build/icon.png new file mode 100644 index 0000000..6783c5a Binary files /dev/null and b/build/icon.png differ diff --git a/e2e-out/starve-1-autumn-day.png b/e2e-out/starve-1-autumn-day.png new file mode 100644 index 0000000..644319f Binary files /dev/null and b/e2e-out/starve-1-autumn-day.png differ diff --git a/e2e-out/starve-10-mod-map-nofog.png b/e2e-out/starve-10-mod-map-nofog.png new file mode 100644 index 0000000..f6e2840 Binary files /dev/null and b/e2e-out/starve-10-mod-map-nofog.png differ diff --git a/e2e-out/starve-2-craft.png b/e2e-out/starve-2-craft.png new file mode 100644 index 0000000..e9b3960 Binary files /dev/null and b/e2e-out/starve-2-craft.png differ diff --git a/e2e-out/starve-3-night.png b/e2e-out/starve-3-night.png new file mode 100644 index 0000000..5619f7f Binary files /dev/null and b/e2e-out/starve-3-night.png differ diff --git a/e2e-out/starve-4-winter.png b/e2e-out/starve-4-winter.png new file mode 100644 index 0000000..9fff9ac Binary files /dev/null and b/e2e-out/starve-4-winter.png differ diff --git a/e2e-out/starve-5-deerclops.png b/e2e-out/starve-5-deerclops.png new file mode 100644 index 0000000..4c99acc Binary files /dev/null and b/e2e-out/starve-5-deerclops.png differ diff --git a/e2e-out/starve-6-minimap.png b/e2e-out/starve-6-minimap.png new file mode 100644 index 0000000..1f8f0d0 Binary files /dev/null and b/e2e-out/starve-6-minimap.png differ diff --git a/e2e-out/starve-7-mod-pick.png b/e2e-out/starve-7-mod-pick.png new file mode 100644 index 0000000..62104fc Binary files /dev/null and b/e2e-out/starve-7-mod-pick.png differ diff --git a/e2e-out/starve-8-mod-farm.png b/e2e-out/starve-8-mod-farm.png new file mode 100644 index 0000000..18682f4 Binary files /dev/null and b/e2e-out/starve-8-mod-farm.png differ diff --git a/e2e-out/starve-9-mod-chester.png b/e2e-out/starve-9-mod-chester.png new file mode 100644 index 0000000..d42268f Binary files /dev/null and b/e2e-out/starve-9-mod-chester.png differ diff --git a/electron/e2e-starve.cjs b/electron/e2e-starve.cjs new file mode 100644 index 0000000..6327ed9 --- /dev/null +++ b/electron/e2e-starve.cjs @@ -0,0 +1,383 @@ +// 饥荒专项端到端自检(开发用):登录 → 进入饥荒 → 单人开局 → +// 通过 window.__starve 后门验证:世界生成/三围/合成科技/查理/季节体温/巨鹿/小地图/存档 +// 追加:宽屏布局无滚动条断言 + Mod 体系全链路(8 个内置 mod 勾选开局逐一验证) +// 运行:npm run electron:dist; npx electron electron/e2e-starve.cjs +const { app, BrowserWindow, ipcMain } = require('electron') +const path = require('path') +const fs = require('fs') + +const SERVER = process.env.NLG_SERVER || 'http://127.0.0.1:8080' +const OUT_DIR = process.env.NLG_E2E_OUT || path.join(process.cwd(), 'e2e-out') +const pageErrors = [] + +ipcMain.on('nlg:get-server-base', (e) => { e.returnValue = SERVER }) +ipcMain.handle('nlg:set-server-base', () => ({ ok: true })) +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)) +let failures = 0 +const check = (cond, msg) => { + if (cond) console.log(' PASS', msg) + else { failures++; console.error(' FAIL', msg) } +} + +async function main() { + fs.mkdirSync(OUT_DIR, { recursive: true }) + const win = new BrowserWindow({ + width: 1360, + height: 860, + show: true, + backgroundColor: '#0f1220', + webPreferences: { preload: path.join(__dirname, 'preload.cjs') }, + }) + win.webContents.on('console-message', (e) => { + if (e?.level === 'error' && e?.message) pageErrors.push(e.message) + }) + const js = (code) => win.webContents.executeJavaScript(code, true) + const shot = async (name) => { + for (let i = 0; i < 3; i++) { + try { + const img = await win.webContents.capturePage() + fs.writeFileSync(path.join(OUT_DIR, `${name}.png`), img.toPNG()) + console.log('[e2e] screenshot:', name) + return + } catch { await sleep(600) } + } + } + const waitFor = async (cond, timeoutMs, desc) => { + const t0 = Date.now() + while (Date.now() - t0 < timeoutMs) { + try { if (await js(cond)) return true } catch {} + await sleep(250) + } + throw new Error(`等待超时:${desc}`) + } + const st = () => js('window.__starve ? JSON.stringify(window.__starve.state()) : "null"').then((s) => JSON.parse(s)) + + const distIndex = path.join(__dirname, '../dist/index.html') + if (!fs.existsSync(distIndex)) throw new Error('缺少 dist,请先 npm run electron:dist') + await win.loadFile(distIndex) + + // 登录 + await js('localStorage.clear(); true') + await win.webContents.reload() + await waitFor(`!!document.querySelector('input[placeholder^="用户名"]')`, 10000, '登录页') + await js(`(() => { + const u = document.querySelector('input[placeholder^="用户名"]') + u.value = 'xiaoming'; u.dispatchEvent(new Event('input', { bubbles: true })) + const p = document.querySelector('input[type="password"]') + p.value = '123456'; p.dispatchEvent(new Event('input', { bubbles: true })) + document.querySelector('button.submit').click() + return true + })()`) + await waitFor(`!!localStorage.getItem('token') && location.hash === '#/'`, 10000, '登录成功') + console.log('[e2e] 登录成功') + + // 确保饥荒可玩(必要时购买) + const gameName = await js(` + fetch(window.desktop.serverBase + '/api/games', { headers: { Authorization: 'Bearer ' + localStorage.getItem('token') } }) + .then((r) => r.json()).then((j) => ((j.data || []).find((g) => g.code === 'starve') || {}).name || '') + `) + if (!gameName) throw new Error('饥荒游戏不存在') + await sleep(1200) + const cardBtn = `(() => { + const card = [...document.querySelectorAll('.game-card')].find((c) => (c.querySelector('.name')?.textContent || '').trim() === ${JSON.stringify(gameName)}) + const btn = card && card.querySelector('.actions button:last-child') + return btn ? btn.textContent.trim() : '' + })()` + if ((await js(cardBtn)) === '去购买') { + await js(`(() => { + const card = [...document.querySelectorAll('.game-card')].find((c) => (c.querySelector('.name')?.textContent || '').trim() === ${JSON.stringify(gameName)}) + card.querySelector('.actions button:last-child').click(); return true + })()`) + await waitFor(`!!document.querySelector('.buy-modal')`, 4000, '购买弹窗') + await js(`document.querySelector('.buy-modal .bm-actions .btn:last-child').click(); true`) + await waitFor(`${cardBtn} === '开始游戏'`, 8000, '购买完成') + console.log('[e2e] 已购买饥荒') + } + + // 进入饥荒 → 单人冒险 + await js(`location.hash = '#/play/starve'; true`) + await waitFor(`!!document.querySelector('.overlay .mode-card')`, 10000, '模式选择面板') + await js(`document.querySelector('.overlay .mode-card').click(); true`) // 单人冒险 + await sleep(400) + // 有旧档则弃档重开,保证确定性 + await waitFor(`!![...document.querySelectorAll('.overlay button')].find((b) => b.textContent.includes('新的冒险') || b.textContent.includes('放弃存档重新开始'))`, 6000, '单人面板') + await js(`(() => { + const btns = [...document.querySelectorAll('.overlay button')] + const btn = btns.find((b) => b.textContent.includes('放弃存档重新开始')) || btns.find((b) => b.textContent.includes('新的冒险')) + btn.click() + return true + })()`) + await waitFor(`!!window.__starve && window.__starve.state().ents > 50`, 10000, '游戏开局 + 世界生成') + console.log('[e2e] 饥荒已开局') + await sleep(2500) + + // 1) 开局状态 + let s = await st() + console.log('[e2e] 开局状态:', JSON.stringify(s)) + check(s.hp === 150 && s.hunger <= 150 && s.san <= 200, `三围官方值 (${s.hp}/${Math.round(s.hunger)}/${Math.round(s.san)})`) + check(s.season === 'autumn' && s.day === 1, `秋季第 1 天开局`) + check(!!s.biome, `出生在群系 ${s.biome}`) + check(s.fps > 25, `帧率正常 (${s.fps})`) + check(s.monKinds.includes('beefalo') && s.monKinds.includes('pig') && s.monKinds.includes('frog') && s.monKinds.includes('spider'), `生物齐备 (${[...new Set(s.monKinds)].join(',')})`) + // 宽屏布局:整页贴合视口不出滚动条,画布铺满容器 + const layout = await js(`(() => { + const root = document.scrollingElement || document.documentElement + const cv = document.querySelector('canvas.starve-canvas') + const wrap = cv && cv.parentElement + const r1 = cv && cv.getBoundingClientRect() + const r2 = wrap && wrap.getBoundingClientRect() + return JSON.stringify({ + wide: document.body.classList.contains('wide-page'), + noScroll: root.scrollHeight <= window.innerHeight + 1 && root.scrollWidth <= window.innerWidth + 1, + sh: root.scrollHeight, ih: window.innerHeight, + fill: !!(r1 && r2 && Math.abs(r1.height - r2.height) < 3 && Math.abs(r1.width - r2.width) < 3), + ch: r1 ? Math.round(r1.height) : 0, wh: r2 ? Math.round(r2.height) : 0, + }) + })()`).then(JSON.parse) + check(layout.wide, 'wide-page 布局生效') + check(layout.noScroll, `无滚动条(页高 ${layout.sh} <= 视口 ${layout.ih})`) + check(layout.fill, `画布铺满游戏区 (画布高 ${layout.ch} / 容器高 ${layout.wh})`) + await shot('starve-1-autumn-day') + + // 2) 合成与科技 + await js(`(() => { const g = window.__starve; g.give('twig',8); g.give('flint',6); g.give('grass',12); g.give('log',24); g.give('rock',24); g.give('gold',8); return true })()`) + await js(`window.__starve.craft('axe'); true`) + await sleep(300) + s = await st() + check(s.equip.hand === 'axe', `打造斧头并自动装备 (${s.equip.hand})`) + const spearBefore = await js(`(() => { window.__starve.craft('spear'); return window.__starve.state().slots.join(',') })()`) + check(!spearBefore.includes('spear'), '无科学机器时长矛不可合成(科技封锁)') + await js(`window.__starve.craft('sciencemachine'); window.__starve.give('rope',2); true`) + await sleep(300) + await js(`window.__starve.craft('spear'); true`) + await sleep(300) + s = await st() + check(s.slots.join(',').includes('spear'), `科学机器旁解锁长矛原型 (${s.slots.join(',')})`) + await js(`window.__starve.craft('campfire'); true`) + await sleep(300) + s = await st() + check(s.fires >= 1, '篝火点燃') + await shot('starve-2-craft') + + // 3) 夜晚 + 查理 + await js(`window.__starve.time(150); true`) + await sleep(800) + await shot('starve-3-night') + // 走离篝火再测查理(先记录当前 hp) + const hpBefore = (await st()).hp + await js(`(() => { + const g = window.__starve + // 传送到远处黑暗地带(直接改不了坐标,用给火把再收走的方式不行——改用等待查理打到) + return true + })()`) + // 玩家出生点附近有篝火,把时间拨到夜晚但玩家未必在火圈外——直接验证黑暗提示逻辑成本高, + // 改为验证「夜晚黑暗掉血」:等 8 秒看是否遭袭(若在光圈内则跳过该断言) + await sleep(8000) + s = await st() + const struck = s.hp < hpBefore - 50 + console.log(`[e2e] 查理测试:hp ${hpBefore} -> ${s.hp}${struck ? '(遭袭)' : '(在光圈内,跳过)'}`) + await js(`window.__starve.time(20); true`) // 拨回白天防连击致死 + + // 4) 冬季 + 体温 + 巨鹿(先传送远离篝火,避免火焰取暖干扰) + await js(`(() => { const p = window.__starve.state().pos; window.__starve.pos(p[0] + 400, p[1]); window.__starve.season('winter'); return true })()`) + await sleep(400) + s = await st() + check(s.season === 'winter', '跳转冬季') + const tempBefore = s.temp + await sleep(4000) + s = await st() + check(s.temp < tempBefore, `冬季体温下降 (${tempBefore}° -> ${s.temp}°)`) + await shot('starve-4-winter') + await js(`window.__starve.deerclops(); true`) + await sleep(2500) + s = await st() + check(s.monKinds.includes('deerclops'), `独眼巨鹿降临 (${[...new Set(s.monKinds)].join(',')})`) + await sleep(1500) + await shot('starve-5-deerclops') + + // 5) 小地图 + await js(`window.dispatchEvent(new KeyboardEvent('keydown', { key: 'm' })); true`) + await sleep(800) + const mapVisible = await js(`(() => { const el = document.querySelector('.ds-map'); return el && el.style.display !== 'none' })()`) + check(mapVisible, '小地图 M 键开启') + await shot('starve-6-minimap') + await js(`window.dispatchEvent(new KeyboardEvent('keydown', { key: 'm' })); window.dispatchEvent(new KeyboardEvent('keyup', { key: 'm' })); true`) + + // 6) 存档往返 + await js(`[...document.querySelectorAll('.hud-left .ds-btn')].find((b) => b.textContent.includes('保存'))?.click(); true`) + await sleep(1200) + const saved = await js(` + fetch(window.desktop.serverBase + '/api/games/starve/save', { headers: { Authorization: 'Bearer ' + localStorage.getItem('token') } }) + .then((r) => r.json()).then((j) => j.data || j).then((d) => !!(d && d.exists !== false && d.data)).catch(() => false) + `) + check(saved, '云存档已写入(v4)') + + // ===== 7) Mod 体系全链路 ===== + console.log('[e2e] === Mod 体系 ===') + // 重新进入游玩页:单人面板勾选全部 mod 后弃档重开 + await js(`location.hash = '#/'; true`) + await sleep(700) + await js(`location.hash = '#/play/starve'; true`) + await waitFor(`!!document.querySelector('.overlay .mode-card')`, 10000, '模式选择面板(二次)') + await js(`document.querySelector('.overlay .mode-card').click(); true`) + await waitFor(`document.querySelectorAll('.mod-pick input').length >= 8`, 6000, 'Mod 勾选列表') + const modCount = await js(`(() => { + const boxes = [...document.querySelectorAll('.mod-pick input')] + boxes.forEach((cb) => { if (!cb.checked) cb.click() }) + return boxes.filter((cb) => cb.checked).length + })()`) + check(modCount === 8, `勾选全部 8 个 mod (${modCount})`) + const factorTxt = await js(`(document.querySelector('.mod-pick .mp-total') || {}).textContent || ''`) + check(factorTxt.includes('0.64'), `面板显示难度系数 (${factorTxt.trim()})`) + await shot('starve-7-mod-pick') + await js(`(() => { + const btns = [...document.querySelectorAll('.overlay button')] + const btn = btns.find((b) => b.textContent.includes('放弃存档重新开始')) || btns.find((b) => b.textContent.includes('新的冒险')) + btn.click() + return true + })()`) + await waitFor(`!!window.__starve && window.__starve.mods().length === 8 && window.__starve.state().ents > 50`, 12000, 'Mod 局开局') + s = await st() + check(s.mods.length === 8, `本局激活 mod:${s.mods.join(',')}`) + check(s.scoreFactor === 0.64, `得分系数 0.64 (${s.scoreFactor})`) + + // 和平模式:现在就排猎犬波,结尾处验证一直没有狗(无 mod 时 6 秒后必出) + await js(`window.__starve.hound(); true`) + + // 宝藏:世界生成撒了箱子;传送过去开箱得战利品 + const chestN = await js(`window.__starve.entCount('chest')`) + check(chestN >= 6 && chestN <= 10, `宝箱撒点 6~10 个 (${chestN})`) + const chestPos = await js(`JSON.stringify(window.__starve.find('chest'))`).then(JSON.parse) + await js(`window.__starve.pos(${chestPos[0]}, ${chestPos[1] + 36}); true`) + const slotsBeforeChest = (await st()).slots.join(',') + await js(`window.__starve.poke('chest'); true`) + await sleep(300) + s = await st() + check(s.slots.join(',') !== slotsBeforeChest, `开箱获得战利品 (${s.slots.join(',')})`) + + // 便利包:堆叠上限 99(原版 40) + await js(`window.__starve.give('grass', 90); true`) + await sleep(200) + s = await st() + check(s.slots.some((x) => x === 'grassx90'), `草堆叠 90/99 一格 (${s.slots.find((x) => x.startsWith('grass'))})`) + + // 农场:锄头开垦 → 播种 → 拨快生长 → 收获(撒点太密会提示"太挤",挪几步重试) + await js(`window.__starve.give('hoe', 1); true`) + await sleep(200) + for (let k = 0; k < 8; k++) { + await js(`window.__starve.useCode('hoe'); true`) + await sleep(150) + if (await js(`window.__starve.entCount('farmplot')`)) break + await js(`(() => { const g = window.__starve; const p = g.state().pos; g.pos(p[0] + 56, p[1] + (${k} % 2 ? 64 : -64)); return true })()`) + } + check((await js(`window.__starve.entCount('farmplot')`)) >= 1, '锄头开垦出农田') + await js(`window.__starve.give('carrotseeds', 2); window.__starve.poke('farmplot'); true`) + await sleep(200) + await js(`window.__starve.tickEnts(400); true`) // 拨快 400 秒 → 成熟 + await sleep(200) + await js(`window.__starve.poke('farmplot'); true`) + await sleep(200) + s = await st() + check(s.slots.some((x) => x.startsWith('carrotx')), `农田收获胡萝卜 (${s.slots.find((x) => x.startsWith('carrotx'))})`) + await shot('starve-8-mod-farm') + + // 噩梦模式:蜘蛛血量 100 → 150(就近读我们刚召的那只,别误读远处巢里的) + await js(`window.__starve.spider(); true`) + await sleep(300) + const spiderHp = await js(`window.__starve.nearMobHp('spider')`) + check(spiderHp === 150, `噩梦蜘蛛血量 150 (${spiderHp})`) + + // 魔法:火魔杖 AoE 灼烧(35 伤害)→ 回旋镖补刀(20 伤害) + await js(`window.__starve.give('firestaff', 1); true`) + await sleep(200) + await js(`window.__starve.useCode('firestaff'); true`) + await sleep(300) + const afterStaff = await js(`window.__starve.nearMobHp('spider')`) + check(afterStaff === 115, `火魔杖灼烧 150→115 (${afterStaff})`) + await js(`window.__starve.give('boomerang', 1); true`) + await sleep(200) + await js(`window.__starve.useCode('boomerang'); true`) + await sleep(1600) + const afterBoom = await js(`window.__starve.nearMobHp('spider')`) + check(afterBoom < afterStaff, `回旋镖命中 (${afterStaff}→${afterBoom})`) + + // 清场:清掉敌对生物(那只 95 血的噩梦蜘蛛会一路追杀)+ 回满三围,保证后续断言确定性 + await js(`window.__starve.wipeHostiles(); window.__starve.heal(); true`) + await js(`(() => { const t = window.__starve.find('tree'); if (t) window.__starve.pos(t[0] + 40, t[1] + 40); return true })()`) + await sleep(300) + + // 切斯特:拿到眼骨后现身跟随;丢地上的东西会被它吞掉,点它吐出 + await js(`window.__starve.give('eyebone', 1); true`) + await waitFor(`window.__starve.state().monKinds.includes('chester')`, 6000, '切斯特现身') + check(true, '眼骨召来切斯特') + const store0 = await js(`JSON.stringify(window.__starve.mobStore('chester') || [])`).then(JSON.parse) + check(store0.length === 0, '切斯特初始空仓') + await js(`window.__starve.drop('log', 5); window.__starve.drop('flint', 3); true`) + await sleep(3000) + const store1 = await js(`JSON.stringify(window.__starve.mobStore('chester') || [])`).then(JSON.parse) + check(store1.length >= 1, `切斯特吞了掉落物 (${store1.join(',') || '空'})`) + await js(`window.__starve.pokeMob('chester'); true`) + await sleep(300) + const store2 = await js(`JSON.stringify(window.__starve.mobStore('chester') || [])`).then(JSON.parse) + check(store2.length === 0, '点击切斯特吐出全部物品') + await shot('starve-9-mod-chester') + + // 复活雕像:二级科技建造 → 致死伤害被拦截原地复活(先清背包防溢出落地) + await js(`(() => { const g = window.__starve; g.clearInv(); g.give('rock', 40); g.give('gold', 14); g.give('log', 12); return true })()`) + await js(`window.__starve.craft('sciencemachine'); true`) + await sleep(200) + await js(`window.__starve.craft('alchemyengine'); true`) + await sleep(200) + await js(`window.__starve.craft('revivestatue'); true`) + await sleep(200) + check((await js(`window.__starve.entCount('revivestatue')`)) === 1, '建成复活雕像') + await js(`window.__starve.hurt(999); true`) + await sleep(400) + s = await st() + check(s.hp === 75, `致死伤害被雕像拦截,半血复活 (hp=${s.hp})`) + check((await js(`window.__starve.entCount('revivestatue')`)) === 0, '雕像已消耗') + const overlayDead = await js(`!!document.querySelector('.death-overlay')`) + check(!overlayDead, '未进入死亡结算') + + // 和平模式收尾断言:排波已过去 10 秒+,仍然无猎犬 + s = await st() + check(!s.monKinds.includes('hound') && !s.monKinds.includes('icehound'), `和平模式全程无猎犬 (${[...new Set(s.monKinds)].join(',')})`) + + // 便利包无迷雾:直接开地图截图(全图可见) + await js(`window.dispatchEvent(new KeyboardEvent('keydown', { key: 'm' })); true`) + await sleep(700) + await shot('starve-10-mod-map-nofog') + await js(`window.dispatchEvent(new KeyboardEvent('keydown', { key: 'm' })); window.dispatchEvent(new KeyboardEvent('keyup', { key: 'm' })); true`) + + // Mod 存档往返:保存 → 云端应带 mods 字段 + await js(`[...document.querySelectorAll('.hud-left .ds-btn')].find((b) => b.textContent.includes('保存'))?.click(); true`) + await sleep(1200) + const savedMods = await js(` + fetch(window.desktop.serverBase + '/api/games/starve/save', { headers: { Authorization: 'Bearer ' + localStorage.getItem('token') } }) + .then((r) => r.json()).then((j) => j.data || j) + .then((d) => { try { return JSON.parse(d.data).mods || [] } catch { return [] } }) + .catch(() => []) + `) + check(Array.isArray(savedMods) && savedMods.length === 8, `存档记录 mod 集合 (${savedMods.length} 个)`) + + // 汇总 + const errs = pageErrors.filter((m) => !m.includes('favicon') && !m.includes('DevTools')) + if (errs.length) { + console.error(`[e2e] 页面报错 ${errs.length} 条:`) + errs.slice(0, 10).forEach((m) => console.error(' -', m)) + failures += errs.length + } + console.log(failures ? `[e2e] ${failures} 项失败` : '[e2e] 饥荒专项自检全部通过') + app.exit(failures ? 1 : 0) +} + +app.whenReady().then(() => + main().catch((err) => { + console.error('[e2e] 失败:', err.message) + app.exit(1) + }) +) +setTimeout(() => { + console.error('[e2e] 总超时') + app.exit(1) +}, 220000) diff --git a/electron/e2e.cjs b/electron/e2e.cjs new file mode 100644 index 0000000..9aad7be --- /dev/null +++ b/electron/e2e.cjs @@ -0,0 +1,233 @@ +// 端到端自检(开发用,不随应用打包),支持两种模式: +// desktop(默认):以 file:// 加载 dist 构建产物 + preload 注入,验证桌面端(hash 路由 + 配置服务器地址) +// web:不带 preload 加载 Vite 开发服务器,等价纯浏览器环境,验证网页版(history 路由 + 同源代理) +// 流程:清空登录态 → 演示账号真实登录 → 大厅(含购买弹窗)→ 对战大厅 → WS 建房/退房 → 单机开一局 → 汇总页面报错 +// 运行:npm run electron:e2e(桌面模式);$env:NLG_E2E_MODE='web'; npx electron electron/e2e.cjs(网页模式,需 5173 开发服务器) +// NLG_E2E_GAME 可指定单机步骤玩哪款游戏(默认 snake,如 mario / linkgame) +const { app, BrowserWindow, ipcMain } = require('electron') +const path = require('path') +const fs = require('fs') + +const MODE = process.env.NLG_E2E_MODE === 'web' ? 'web' : 'desktop' +const SERVER = process.env.NLG_SERVER || 'http://127.0.0.1:8080' +const WEB_URL = process.env.NLG_E2E_URL || 'http://localhost:5173' +const OUT_DIR = process.env.NLG_E2E_OUT || process.cwd() +const GAME = process.env.NLG_E2E_GAME || 'snake' +const SUFFIX = MODE === 'web' ? '-web' : '' +const pageErrors = [] + +// preload 依赖该同步 IPC 提供服务器地址(仅桌面模式挂 preload) +ipcMain.on('nlg:get-server-base', (e) => { e.returnValue = SERVER }) +ipcMain.handle('nlg:set-server-base', () => ({ ok: true })) + +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)) + +async function main() { + const win = new BrowserWindow({ + width: 1280, + height: 820, + show: true, + backgroundColor: '#0f1220', + webPreferences: MODE === 'desktop' ? { preload: path.join(__dirname, 'preload.cjs') } : {}, + }) + // 收集页面报错(Electron 新版事件对象:event.level 为 'error' 等字符串) + win.webContents.on('console-message', (e) => { + if (e?.level === 'error' && e?.message) pageErrors.push(e.message) + }) + const js = (code) => win.webContents.executeJavaScript(code, true) + // 截图:capturePage 在 Windows 上偶发 UnknownVizError(GPU 合成器抖动),重试且失败不阻断流程 + const shot = async (name) => { + for (let i = 0; i < 3; i++) { + try { + const img = await win.webContents.capturePage() + const file = path.join(OUT_DIR, `${name}${SUFFIX}.png`) + fs.writeFileSync(file, img.toPNG()) + console.log('[e2e] screenshot:', file) + return + } catch (err) { + if (i === 2) console.warn(`[e2e] 截图 ${name} 失败(不影响流程):${err.message}`) + else await sleep(600) + } + } + } + // 轮询等待页面条件成立 + const waitFor = async (cond, timeoutMs, desc) => { + const t0 = Date.now() + while (Date.now() - t0 < timeoutMs) { + try { + if (await js(cond)) return true + } catch {} + await sleep(300) + } + throw new Error(`等待超时:${desc}`) + } + // 路由条件与跳转:桌面端 hash 模式,网页版 history 模式 + const atRoute = (p) => (MODE === 'desktop' ? `location.hash === '#${p}'` : `location.pathname === '${p}'`) + const goto = async (p) => { + if (MODE === 'desktop') { + await js(`location.hash = '#${p}'; true`) + } else { + // history 模式下直接整页跳转(等价用户输入地址),开发服务器有 SPA 回退 + await win.loadURL(`${WEB_URL}${p}`) + } + await waitFor(atRoute(p), 5000, `路由到 ${p}`) + } + + if (MODE === 'desktop') { + const distIndex = path.join(__dirname, '../dist/index.html') + if (!fs.existsSync(distIndex)) throw new Error('缺少 dist/index.html,请先 npm run electron:dist') + await win.loadFile(distIndex) + } else { + await win.loadURL(WEB_URL).catch(() => { + throw new Error(`无法访问 ${WEB_URL},请先 npm run dev 启动开发服务器`) + }) + } + + // 第 0 步:清空历史登录态,确保从登录页开始 + await js('localStorage.clear(); true') + await win.webContents.reload() + await waitFor(`!!document.querySelector('input[placeholder^="用户名"]')`, 10000, '登录页渲染') + console.log(`[e2e] 登录页已渲染(${MODE === 'desktop' ? 'file:// + hash 路由' : 'http + history 路由'})`) + + // 第 1 步:填写演示账号并提交(触发 input 事件以驱动 v-model) + await js(` + (() => { + const u = document.querySelector('input[placeholder^="用户名"]') + u.value = 'xiaoming'; u.dispatchEvent(new Event('input', { bubbles: true })) + const p = document.querySelector('input[type="password"]') + p.value = '123456'; p.dispatchEvent(new Event('input', { bubbles: true })) + document.querySelector('button.submit').click() + return true + })() + `) + // 第 2 步:等待登录成功(Token 落库 + 路由跳到大厅) + await waitFor(`!!localStorage.getItem('token') && ${atRoute('/')}`, 10000, '登录成功并跳转大厅') + console.log('[e2e] 登录成功,Token 已写入,已跳转大厅') + await sleep(1800) // 等游戏列表与字体渲染 + await shot('electron-smoke-lobby') + + // 第 2.5 步:大厅购买弹窗(若存在待解锁游戏则点开验证展示与关闭) + const hasBuyBtn = await js( + `!![...document.querySelectorAll('.game-card button')].find((b) => b.textContent.includes('去购买'))` + ) + if (hasBuyBtn) { + await js(` + [...document.querySelectorAll('.game-card button')].find((b) => b.textContent.includes('去购买')).click(); true + `) + await waitFor(`!!document.querySelector('.buy-modal')`, 4000, '购买弹窗打开') + await sleep(400) + await shot('electron-smoke-buymodal') + await js(`document.querySelector('.buy-modal .bm-close').click(); true`) + await waitFor(`!document.querySelector('.buy-modal')`, 3000, '购买弹窗关闭') + console.log('[e2e] 大厅购买弹窗:打开展示详情 / 关闭均正常') + } else { + console.log('[e2e] 大厅无待解锁游戏,跳过购买弹窗检查') + } + + // 第 2.6 步:确保目标游戏可玩——未拥有则通过弹窗「立即购买」走一遍真实购买闭环 + const gameName = await js(` + fetch((window.desktop && window.desktop.serverBase ? window.desktop.serverBase : '') + '/api/games', { + headers: { Authorization: 'Bearer ' + localStorage.getItem('token') }, + }).then((r) => r.json()).then((j) => ((j.data || []).find((g) => g.code === '${GAME}') || {}).name || '') + `) + if (!gameName) throw new Error(`游戏 ${GAME} 不存在或列表接口异常`) + const cardBtnLabel = `(() => { + const card = [...document.querySelectorAll('.game-card')].find((c) => (c.querySelector('.name')?.textContent || '').trim() === ${JSON.stringify(gameName)}) + const btn = card && card.querySelector('.actions button:last-child') + return btn ? btn.textContent.trim() : '' + })()` + if ((await js(cardBtnLabel)) === '去购买') { + await js(`(() => { + const card = [...document.querySelectorAll('.game-card')].find((c) => (c.querySelector('.name')?.textContent || '').trim() === ${JSON.stringify(gameName)}) + card.querySelector('.actions button:last-child').click() + return true + })()`) + await waitFor(`!!document.querySelector('.buy-modal')`, 4000, '目标游戏购买弹窗打开') + const canBuy = await js(`!document.querySelector('.buy-modal .bm-actions .btn:last-child').disabled`) + if (!canBuy) throw new Error(`积分不足,无法购买 ${gameName},无法继续单机游戏步骤`) + await js(`document.querySelector('.buy-modal .bm-actions .btn:last-child').click(); true`) + await waitFor(`!document.querySelector('.buy-modal')`, 6000, '购买完成弹窗自动关闭') + await waitFor(`${cardBtnLabel} === '开始游戏'`, 6000, '购买后卡片刷新为可玩') + console.log(`[e2e] 已通过大厅弹窗直接购买「${gameName}」,卡片已解锁`) + } else { + console.log(`[e2e] 「${gameName}」已可玩,无需购买`) + } + + // 第 3 步:进入对战大厅 + await goto('/battle') + await sleep(1500) + await shot('electron-smoke-battle') + + // 第 4 步:WebSocket 建房(联机核心链路)——创建斗地主 AI 房间并拿到邀请码 + await js(` + (() => { + const btn = [...document.querySelectorAll('button')].find((b) => b.textContent.includes('创建房间')) + btn.click() + return true + })() + `) + await waitFor( + `${atRoute('/battle/room')} && /^[A-Z0-9]{6}$/.test(document.querySelector('.code-box b')?.textContent?.trim() || '')`, + 10000, + 'WS 建房成功并显示 6 位邀请码' + ) + const roomCode = await js(`document.querySelector('.code-box b').textContent.trim()`) + console.log('[e2e] WebSocket 建房成功,邀请码:' + roomCode) + await sleep(900) + await shot('electron-smoke-room') + // 退出房间,回到对战大厅 + await js(` + (() => { + const btn = [...document.querySelectorAll('button')].find((b) => b.textContent.includes('退出房间')) + btn.click() + return true + })() + `) + await waitFor(atRoute('/battle'), 5000, '退房返回对战大厅') + console.log('[e2e] 已退出房间') + + // 第 5 步:单机游戏运行(渲染与游戏循环)——默认贪吃蛇,可用 NLG_E2E_GAME 换游戏 + // 普通游戏是「开始游戏」按钮;带关卡的游戏(连连看/消消乐/马里奥)是关卡选择面板的继续按钮 + await goto(`/play/${GAME}`) + const startBtnExpr = `(() => { + return document.querySelector('.overlay .ls-continue') || + [...document.querySelectorAll('.overlay button')].find((b) => b.textContent.includes('开始游戏')) || null + })()` + await waitFor(`!!${startBtnExpr}`, 10000, `${GAME} 游玩页就绪`) + const isLevelGame = await js(`!!document.querySelector('.overlay .level-select')`) + if (isLevelGame) { + console.log(`[e2e] ${GAME} 为关卡制游戏,关卡选择面板已展示`) + } + await js(`(${startBtnExpr}).click(); true`) + await sleep(2200) // 让游戏跑几帧 + // Canvas 游戏看画布,DOM 游戏(连连看/消消乐)看棋盘容器 + await waitFor( + `!!document.querySelector('.stage canvas') || !!document.querySelector('.stage .link-board') || !!document.querySelector('.stage .m3-board .board')`, + 5000, + '游戏画面渲染' + ) + console.log(`[e2e] ${GAME} 已开局,画面渲染正常`) + await shot('electron-smoke-game') + + // 汇总 + if (pageErrors.length) { + console.error(`[e2e] 页面报错 ${pageErrors.length} 条:`) + for (const m of pageErrors.slice(0, 10)) console.error(' -', m) + app.exit(1) + return + } + console.log(`[e2e] 全部通过(${MODE} 模式):登录 / 路由 / 大厅 / WS 建房退房 / 单机游戏均正常,无页面报错`) + app.exit(0) +} + +app.whenReady().then(() => + main().catch((err) => { + console.error('[e2e] 失败:', err.message) + app.exit(1) + }) +) +// 兜底超时 +setTimeout(() => { + console.error('[e2e] 总超时') + app.exit(1) +}, 90000) diff --git a/electron/main.cjs b/electron/main.cjs new file mode 100644 index 0000000..c48ffd6 --- /dev/null +++ b/electron/main.cjs @@ -0,0 +1,461 @@ +// Electron 主进程:窗口生命周期、应用菜单、后端服务器地址配置、外链拦截 +// 加载策略:开发模式(未打包)优先连 Vite 开发服务器(5173),连不上回退到本地 dist 构建产物; +// 打包后固定加载 asar 内的 dist。渲染进程通过 preload 注入的 window.desktop 获取服务器地址 +const { app, BrowserWindow, Menu, Tray, nativeImage, ipcMain, shell, dialog, net } = require('electron') +const path = require('path') +const fs = require('fs') + +// 后端服务器默认地址(可在「菜单 → 服务器设置」中修改,保存在用户数据目录) +// 生产桌面端默认连线上;本机调试可在菜单里改回 http://127.0.0.1:8080 +const DEFAULT_SERVER = 'https://game.nailaoyun.cn' +const DEV_URL = process.env.VITE_DEV_SERVER_URL || 'http://localhost:5173' +// NLG_LOAD=dist 可在开发模式下强制加载 dist(用于模拟打包后的运行环境) +const FORCE_DIST = process.env.NLG_LOAD === 'dist' +// 冒烟自检模式:页面加载完成后截图保存并退出(用于自动化验证桌面端能正常启动) +const SMOKE = process.argv.includes('--smoke') || process.env.NLG_SMOKE === '1' + +let mainWin = null +let settingsWin = null +let tray = null +// 真正退出时置 true(托盘「退出」/ app.quit);否则点关闭按钮只隐藏到托盘 +let isQuitting = false + +function appIconPath() { + return path.join(__dirname, '../build/icon.png') +} + +// --------------------------------------------------------------------- +// 配置读写:%APPDATA%//config.json +// --------------------------------------------------------------------- +function cfgFile() { + return path.join(app.getPath('userData'), 'config.json') +} +function readCfg() { + try { + return { serverBase: DEFAULT_SERVER, ...JSON.parse(fs.readFileSync(cfgFile(), 'utf8')) } + } catch { + return { serverBase: DEFAULT_SERVER } + } +} +function writeCfg(patch) { + const next = { ...readCfg(), ...patch } + fs.mkdirSync(path.dirname(cfgFile()), { recursive: true }) + fs.writeFileSync(cfgFile(), JSON.stringify(next, null, 2)) + return next +} + +// --------------------------------------------------------------------- +// IPC:preload 同步取服务器地址;设置窗口保存新地址后重载主窗口 +// --------------------------------------------------------------------- +ipcMain.on('nlg:get-server-base', (e) => { + e.returnValue = readCfg().serverBase +}) +ipcMain.handle('nlg:set-server-base', (_e, url) => { + const clean = String(url || '').trim().replace(/\/+$/, '') + if (!/^https?:\/\/.+/i.test(clean)) { + return { ok: false, msg: '地址需以 http:// 或 https:// 开头' } + } + writeCfg({ serverBase: clean }) + if (settingsWin && !settingsWin.isDestroyed()) settingsWin.close() + // 重载后 preload 会重新读取配置,前端以新地址发起请求 + if (mainWin && !mainWin.isDestroyed()) mainWin.reload() + return { ok: true } +}) + +// --------------------------------------------------------------------- +// 版本检查:拉取后端 /api/version/latest 与本地 app.getVersion() 比较 +// 后端返回 { version, download_url, release_notes, force } +// --------------------------------------------------------------------- + +// 语义化版本比较:返回 -1/0/1(ab),非法版本视为 0.0.0 +function compareVer(a, b) { + const pa = String(a || '0').split('.').map((n) => parseInt(n, 10) || 0) + const pb = String(b || '0').split('.').map((n) => parseInt(n, 10) || 0) + const len = Math.max(pa.length, pb.length) + for (let i = 0; i < len; i++) { + const va = pa[i] || 0 + const vb = pb[i] || 0 + if (va > vb) return 1 + if (va < vb) return -1 + } + return 0 +} + +// 拉取一次最新版本信息(用 Electron 内置 net,自动走系统代理) +function fetchLatestVersion() { + return new Promise((resolve) => { + const base = readCfg().serverBase.replace(/\/+$/, '') + if (!base) return resolve({ ok: false, msg: '未配置服务器地址' }) + const req = net.request({ + method: 'GET', + url: base + '/api/version/latest', + redirect: 'follow', + }) + let body = '' + req.on('response', (resp) => { + // 3xx 已由 redirect:'follow' 处理;这里只关心 2xx 最终响应 + resp.on('data', (chunk) => (body += chunk.toString('utf8'))) + resp.on('end', () => { + try { + const parsed = JSON.parse(body) + // 后端统一返回 { code, msg, data },code=0 表示成功 + const data = parsed && parsed.data ? parsed.data : parsed + resolve({ ok: true, data }) + } catch { + resolve({ ok: false, msg: '解析版本信息失败' }) + } + }) + }) + req.on('error', (err) => resolve({ ok: false, msg: '网络错误:' + err.message })) + // 超时兜底(10s) + setTimeout(() => { + try { req.abort() } catch {} + resolve({ ok: false, msg: '请求超时' }) + }, 10000) + req.end() + }) +} + +// 弹出更新对话框(silent=true 表示自动检查,已是最新版不弹窗) +async function checkUpdate(silent = false) { + const local = app.getVersion() + const r = await fetchLatestVersion() + if (!r.ok) { + if (!silent) dialog.showErrorBox('检查更新失败', r.msg || '无法连接服务器') + return + } + const { version, download_url: url, release_notes: notes, force } = r.data || {} + if (compareVer(version, local) <= 0) { + if (!silent) { + dialog.showMessageBox(mainWin, { + type: 'info', + title: '已是最新版本', + message: '当前已是最新版本', + detail: `版本 ${local}`, + }) + } + return + } + const buttons = url ? ['立即下载', force ? '确定' : '稍后提醒'] : [force ? '确定' : '稍后提醒'] + const { response } = await dialog.showMessageBox(mainWin, { + type: force ? 'warning' : 'info', + title: '发现新版本', + message: `发现新版本 ${version}`, + detail: notes || '请下载最新版本安装包覆盖安装。', + buttons, + defaultId: 0, + cancelId: buttons.length - 1, + noLink: true, + }) + // 点了「立即下载」(按钮 0 且存在下载链接);相对路径拼服务器基址 + if (response === 0 && url) { + const abs = /^https?:\/\//i.test(url) + ? url + : readCfg().serverBase.replace(/\/+$/, '') + (url.startsWith('/') ? url : '/' + url) + shell.openExternal(abs) + } +} + +// IPC:渲染进程可触发检查更新(预留菜单按钮 / 页面入口) +ipcMain.handle('nlg:check-update', (_e, silent) => checkUpdate(silent)) + +// --------------------------------------------------------------------- +// 服务器设置窗口(极简内嵌页面,深色风格与主站一致) +// --------------------------------------------------------------------- +function openSettings() { + if (settingsWin && !settingsWin.isDestroyed()) { + settingsWin.focus() + return + } + settingsWin = new BrowserWindow({ + width: 460, + height: 240, + parent: mainWin || undefined, + modal: !!mainWin, + resizable: false, + minimizable: false, + maximizable: false, + autoHideMenuBar: true, + backgroundColor: '#12142a', + title: '服务器设置', + webPreferences: { preload: path.join(__dirname, 'preload.cjs') }, + }) + const cur = readCfg().serverBase + const html = `服务器设置 + +

后端服务器地址

+

桌面端连接的游戏服务器(修改后自动重新加载)

+ +
+
+ + +
+ +` + settingsWin.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(html)) + settingsWin.on('closed', () => { settingsWin = null }) +} + +// --------------------------------------------------------------------- +// 系统托盘:关闭窗口隐藏到托盘,右键可显示/检查更新/退出 +// --------------------------------------------------------------------- +function showMainWindow() { + if (!mainWin || mainWin.isDestroyed()) { + createWindow() + return + } + if (mainWin.isMinimized()) mainWin.restore() + mainWin.show() + mainWin.focus() +} + +function createTray() { + if (tray) return + const iconFile = appIconPath() + let img = fs.existsSync(iconFile) + ? nativeImage.createFromPath(iconFile) + : nativeImage.createEmpty() + // Windows 托盘建议 16×16;过大图标会被系统缩放发糊,主动缩一下 + if (!img.isEmpty() && (img.getSize().width > 32 || img.getSize().height > 32)) { + img = img.resize({ width: 16, height: 16 }) + } + tray = new Tray(img) + tray.setToolTip('像素游戏厅') + tray.setContextMenu(Menu.buildFromTemplate([ + { label: '显示主窗口', click: () => showMainWindow() }, + { label: '检查更新…', click: () => checkUpdate(false) }, + { type: 'separator' }, + { + label: '退出', + click: () => { + isQuitting = true + app.quit() + }, + }, + ])) + // 单击 / 双击托盘图标都拉起主窗口(Windows 习惯) + tray.on('click', () => showMainWindow()) + tray.on('double-click', () => showMainWindow()) +} + +// --------------------------------------------------------------------- +// 应用菜单 +// --------------------------------------------------------------------- +function buildMenu() { + const template = [ + { + label: '应用', + submenu: [ + { label: '服务器设置…', accelerator: 'CmdOrCtrl+,', click: openSettings }, + { label: '检查更新…', click: () => checkUpdate(false) }, + { type: 'separator' }, + { label: '刷新', role: 'reload' }, + { label: '强制刷新(忽略缓存)', role: 'forceReload' }, + { label: '开发者工具', role: 'toggleDevTools' }, + { type: 'separator' }, + { + label: '退出', + accelerator: 'CmdOrCtrl+Q', + click: () => { + isQuitting = true + app.quit() + }, + }, + ], + }, + { + label: '编辑', + submenu: [ + { label: '撤销', role: 'undo' }, + { label: '重做', role: 'redo' }, + { type: 'separator' }, + { label: '剪切', role: 'cut' }, + { label: '复制', role: 'copy' }, + { label: '粘贴', role: 'paste' }, + { label: '全选', role: 'selectAll' }, + ], + }, + { + label: '视图', + submenu: [ + { label: '放大', role: 'zoomIn' }, + { label: '缩小', role: 'zoomOut' }, + { label: '实际大小', role: 'resetZoom' }, + { type: 'separator' }, + { label: '全屏', role: 'togglefullscreen' }, + ], + }, + { + label: '帮助', + submenu: [ + { + label: '关于', + click: () => { + dialog.showMessageBox(mainWin, { + type: 'info', + title: '关于', + message: '像素游戏厅 桌面版', + detail: `版本 ${app.getVersion()}\nElectron ${process.versions.electron} / Chromium ${process.versions.chrome}\n服务器:${readCfg().serverBase}`, + }) + }, + }, + ], + }, + ] + Menu.setApplicationMenu(Menu.buildFromTemplate(template)) +} + +// --------------------------------------------------------------------- +// 主窗口 +// --------------------------------------------------------------------- +function createWindow() { + // 窗口图标:打包后 exe 自带图标,这里主要供开发模式(electron .)使用 + const iconPath = appIconPath() + // 恢复上次的窗口大小与位置(若显示器变化导致越界,Electron 会自动拉回可见区域) + const cfg = readCfg() + const saved = cfg.winBounds || {} + mainWin = new BrowserWindow({ + width: saved.width || 1280, + height: saved.height || 820, + ...(Number.isFinite(saved.x) && Number.isFinite(saved.y) ? { x: saved.x, y: saved.y } : {}), + minWidth: 960, + minHeight: 640, + show: false, + backgroundColor: '#0f1220', + title: '像素游戏厅', + ...(fs.existsSync(iconPath) ? { icon: iconPath } : {}), + webPreferences: { + preload: path.join(__dirname, 'preload.cjs'), + contextIsolation: true, + nodeIntegration: false, + spellcheck: false, + }, + }) + mainWin.once('ready-to-show', () => { + if (cfg.winMax) mainWin.maximize() + mainWin.show() + // 启动后 3 秒静默检查一次更新(已是最新版不弹窗,发现新版本才提示) + setTimeout(() => { checkUpdate(true).catch(() => {}) }, 3000) + }) + // 点关闭:记住窗口状态并隐藏到托盘(冒烟模式 / 真正退出时直接关) + mainWin.on('close', (e) => { + try { + writeCfg({ winBounds: mainWin.getNormalBounds(), winMax: mainWin.isMaximized() }) + } catch {} + if (!isQuitting && !SMOKE && tray) { + e.preventDefault() + mainWin.hide() + } + }) + // 页面内打开新窗口(target=_blank 等)→ 交给系统浏览器 + mainWin.webContents.setWindowOpenHandler(({ url }) => { + if (/^https?:\/\//i.test(url)) shell.openExternal(url) + return { action: 'deny' } + }) + // 阻止主窗口导航离开应用(外部 http 链接转系统浏览器) + mainWin.webContents.on('will-navigate', (e, url) => { + const inApp = url.startsWith(DEV_URL) || url.startsWith('file://') + if (!inApp) { + e.preventDefault() + if (/^https?:\/\//i.test(url)) shell.openExternal(url) + } + }) + + const distIndex = path.join(__dirname, '../dist/index.html') + const loadDist = () => { + if (!fs.existsSync(distIndex)) { + dialog.showErrorBox('缺少构建产物', '未找到 dist/index.html,请先执行 npm run electron:dist') + app.quit() + return + } + mainWin.loadFile(distIndex) + } + if (!app.isPackaged && !FORCE_DIST) { + // 开发模式:优先 Vite 开发服务器,连不上回退 dist + mainWin.loadURL(DEV_URL) + mainWin.webContents.once('did-fail-load', (_e, _code, _desc, _url, isMainFrame) => { + if (isMainFrame) loadDist() + }) + } else { + loadDist() + } + + // 冒烟自检:渲染完成后截图退出,供自动化验证 + if (SMOKE) { + let done = false + mainWin.webContents.on('did-finish-load', () => { + setTimeout(async () => { + if (done) return + done = true + try { + const img = await mainWin.webContents.capturePage() + const out = process.env.NLG_SHOT_PATH || path.join(process.cwd(), 'electron-smoke.png') + fs.writeFileSync(out, img.toPNG()) + console.log('[smoke] screenshot saved:', out) + app.exit(0) + } catch (err) { + console.error('[smoke] capture failed:', err) + app.exit(1) + } + }, 2200) + }) + // 兜底:25 秒仍未完成视为失败 + setTimeout(() => { + if (!done) { + console.error('[smoke] timeout') + app.exit(1) + } + }, 25000) + } + + mainWin.on('closed', () => { mainWin = null }) +} + +// 单实例锁:二次启动聚焦已有窗口 +const gotLock = app.requestSingleInstanceLock() +if (!gotLock) { + app.quit() +} else { + app.on('second-instance', () => { + showMainWindow() + }) + app.whenReady().then(() => { + // Windows 任务栏/托盘分组 ID,避免与其他 Electron 应用挤在一起 + if (process.platform === 'win32') { + app.setAppUserModelId('cn.nailaoyun.pixelarcade') + } + buildMenu() + createTray() + createWindow() + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) createWindow() + else showMainWindow() + }) + }) + // 有托盘时关窗不等于退出,不要在 window-all-closed 里 quit + app.on('window-all-closed', () => { + if (process.platform !== 'darwin' && !tray) app.quit() + }) + app.on('before-quit', () => { isQuitting = true }) +} diff --git a/electron/preload.cjs b/electron/preload.cjs new file mode 100644 index 0000000..a4a10ed --- /dev/null +++ b/electron/preload.cjs @@ -0,0 +1,20 @@ +// Electron 预加载脚本:以 contextBridge 向渲染进程暴露最小桌面端 API +// 前端凭 window.desktop 是否存在来区分「桌面端 / 浏览器」两种运行环境 +const { contextBridge, ipcRenderer } = require('electron') + +// 同步读取后端服务器地址:必须在页面脚本执行前就绪(axios/WS 初始化要用) +const serverBase = ipcRenderer.sendSync('nlg:get-server-base') + +contextBridge.exposeInMainWorld('desktop', { + // 后端服务器地址(形如 http://127.0.0.1:8080,结尾不带斜杠) + serverBase, + // 修改服务器地址(校验通过后主进程会自动重载页面) + setServerBase: (url) => ipcRenderer.invoke('nlg:set-server-base', url), + // 检查桌面端新版本(silent=true 时已是最新版不弹窗,用于启动自动检查) + checkUpdate: (silent = false) => ipcRenderer.invoke('nlg:check-update', silent), + // 运行环境版本信息 + versions: { + electron: process.versions.electron, + chrome: process.versions.chrome, + }, +}) diff --git a/index.html b/index.html index 8388c4b..259507b 100644 --- a/index.html +++ b/index.html @@ -1,10 +1,11 @@ - - + + - + + - Vite + Vue + 像素游戏厅
diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e2805e8 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5170 @@ +{ + "name": "nl-game", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nl-game", + "version": "1.0.0", + "dependencies": { + "@fontsource/press-start-2p": "^5.3.0", + "@fontsource/vt323": "^5.3.0", + "axios": "^1.19.0", + "pinia": "^4.0.2", + "three": "^0.185.1", + "vue": "^3.5.41", + "vue-router": "^5.2.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.8", + "electron": "^43.4.0", + "electron-builder": "^26.15.3", + "vite": "^8.2.1" + } + }, + "node_modules/@babel/generator": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.0.tgz", + "integrity": "sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^8.0.0", + "@babel/types": "^8.0.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "@types/jsesc": "^2.5.0", + "jsesc": "^3.0.2" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/generator/node_modules/@babel/helper-string-parser": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz", + "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==", + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/generator/node_modules/@babel/helper-validator-identifier": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz", + "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==", + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/generator/node_modules/@babel/parser": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.4.tgz", + "integrity": "sha512-srpptsAkEbbNIC/q8nT7o+m6CQe8CJUTV/t7MYc9NnWlgYVtHOb7JH6SorxMhN0kuRJjVqXbKClG6xSbPtzz+g==", + "license": "MIT", + "dependencies": { + "@babel/types": "^8.0.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/generator/node_modules/@babel/types": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz", + "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^8.0.0", + "@babel/helper-validator-identifier": "^8.0.4" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@electron-internal/extract-zip": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.5.tgz", + "integrity": "sha512-+bqFCP98pLI0Tt0XQo1TmlXtwjWchISndDOxCkEcIuUgXWpBnLyRI+2DU+mesvnMMX6L1XDqYNA0lXNDHd/yiA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@electron/asar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/asar/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/asar/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/fuses": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", + "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.1", + "fs-extra": "^9.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "electron-fuses": "dist/bin.js" + } + }, + "node_modules/@electron/fuses/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/get": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-5.1.0.tgz", + "integrity": "sha512-3kSBtG8ObcTVfXanm5vVJ6UnBLEVmVsRk1M+vGqCuMBV+XLCbJYuWQful+yIy0GQDsSlK0kHEriEHn7SPk4EnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^3.0.0", + "graceful-fs": "^4.2.11", + "progress": "^2.0.3", + "semver": "^7.6.3", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=22.12.0" + }, + "optionalDependencies": { + "undici": "^7.24.4" + } + }, + "node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", + "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/rebuild": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.2.0.tgz", + "integrity": "sha512-RKL/O+jGoXJMxrx/5771y1n0xTKmFuOYGO3gMmwypBM6rsH0kou0mswwdXA2JrhIkE4xyC7v9vGk0n6NPzgOxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.1.1", + "node-abi": "^4.2.0", + "node-api-version": "^0.2.1", + "node-gyp": "^12.2.0", + "read-binary-file-arch": "^1.0.6" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@electron/universal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", + "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.3.1", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" + }, + "engines": { + "node": ">=16.4" + } + }, + "node_modules/@electron/universal/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz", + "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/windows-sign": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", + "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "cross-dirname": "^0.1.0", + "debug": "^4.3.4", + "fs-extra": "^11.1.1", + "minimist": "^1.2.8", + "postject": "^1.0.0-alpha.6" + }, + "bin": { + "electron-windows-sign": "bin/electron-windows-sign.js" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/fs-extra": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz", + "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@fontsource/press-start-2p": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/press-start-2p/-/press-start-2p-5.3.0.tgz", + "integrity": "sha512-gxWFxdeDglPhEYSwEeooomcaFK8kSQYLQwVxfnOJeYRj2sPEHXjpVtFgEaYfqzOjuAJGrIcJOipu2KY5YkbyuQ==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/vt323": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/vt323/-/vt323-5.3.0.tgz", + "integrity": "sha512-3w33Rg/0+R1587HQfz4t7Q7e0GGeCqr3wmzIwqdGInUnrGEwMA5eX39GP4Z9wpTQnwfhVOhYuxuRwRUeIcicbw==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@noble/hashes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.3.0.tgz", + "integrity": "sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz", + "integrity": "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==", + "devOptional": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.8.0.tgz", + "integrity": "sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/json-schema": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", + "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@peculiar/utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", + "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/webcrypto": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.7.1.tgz", + "integrity": "sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/json-schema": "^1.1.12", + "@peculiar/utils": "^2.0.2", + "tslib": "^2.8.1", + "webcrypto-core": "^1.9.2" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.3.tgz", + "integrity": "sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.3.tgz", + "integrity": "sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.3.tgz", + "integrity": "sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.3.tgz", + "integrity": "sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.3.tgz", + "integrity": "sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.3.tgz", + "integrity": "sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.3.tgz", + "integrity": "sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.3.tgz", + "integrity": "sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.3.tgz", + "integrity": "sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.3.tgz", + "integrity": "sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.3.tgz", + "integrity": "sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.3.tgz", + "integrity": "sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.3.tgz", + "integrity": "sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.3.tgz", + "integrity": "sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsesc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@types/jsesc/-/jsesc-2.5.1.tgz", + "integrity": "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==", + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.8.tgz", + "integrity": "sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue-macros/common": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-3.1.4.tgz", + "integrity": "sha512-/5Fv+6DgIcM9ajY05ZmKBv+LMX1M9A0X+IUwDRVdt67ciw8OV9bvG2r34p3RiEadlsQybjhKPRKNXDC8Bp23cw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-sfc": "^3.5.22", + "ast-kit": "^2.1.2", + "local-pkg": "^1.1.2", + "magic-string-ast": "^1.0.2", + "unplugin-utils": "^0.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/vue-macros" + }, + "peerDependencies": { + "vue": "^2.7.0 || ^3.2.25" + }, + "peerDependenciesMeta": { + "vue": { + "optional": true + } + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.41.tgz", + "integrity": "sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/shared": "3.5.41", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.41.tgz", + "integrity": "sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.41.tgz", + "integrity": "sha512-XJhip7R2wy6vX3knCxdZN4KracFaZUef58s1KYewqluedHIJaPIVfXoYT7MF1F8nCvv6k8bWWxDC8opMkg1VTQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/compiler-core": "3.5.41", + "@vue/compiler-dom": "3.5.41", + "@vue/compiler-ssr": "3.5.41", + "@vue/shared": "3.5.41", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.19", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.41.tgz", + "integrity": "sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/devtools-api": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.2.1.tgz", + "integrity": "sha512-6u4vXBlIBAC1wMplIZgpyPn7uh/s4Bf6F5bMzvLv+EdJ0aHs/+4B7Ygv864EStQSjRbsRzTko/kUG1A1IejQ3A==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^8.2.1" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.2.1.tgz", + "integrity": "sha512-FIGIuq3AWReEpbAHY/cRGeHDfI0qOb8OCQ3YjbEAX04uaxIDbGc9rhkbVcG7rnfHPXE3RsU5KrWOu9V/okd8AQ==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^8.2.1", + "birpc": "^2.6.1", + "hookable": "^5.5.3", + "perfect-debounce": "^2.0.0" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.2.1.tgz", + "integrity": "sha512-Fkac7lUdGReh6pVOi3AYPRGe82LQqRmAfThW7RRligOAP0ZA/Z1z9XLHDM9dv34pV2HRc79DK8uKPeG2fLnA/g==", + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.41.tgz", + "integrity": "sha512-rznsqKM0np0x18EjzF8x88MpEhdNsffbvFbckLL5+oUKz1BxAImEmO7J1ArRYSyo6aQaVoBDp7jEkT91OOxydA==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.41.tgz", + "integrity": "sha512-Vcry58hiAKwGen9Z1jUZE0feFsNArPCMOImYI8el48A9Idf6DuQYD0U05zZIF2Iad1hGhPSvcbBbAOhNr55fhg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.41.tgz", + "integrity": "sha512-3vVBahVBS9+U6cmXBLyb8nE6/yYo4J/CGI9eVFs3KiMc0YHuudwKyShTD65jtJy/L9PUUxNAFu4cj4LiJ0UFbw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.41", + "@vue/runtime-core": "3.5.41", + "@vue/shared": "3.5.41", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.41.tgz", + "integrity": "sha512-n6hx/pNFfbD6SuyeuMVkvqox8bwf/ET9JlA/kAz/imw8sw++wkqKe2mHX5KutjPpbKE4Z56yTHszoOjGMI9igQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.41", + "@vue/runtime-dom": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.41.tgz", + "integrity": "sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==", + "license": "MIT" + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/abbrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/app-builder-lib": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.15.3.tgz", + "integrity": "sha512-2VnyWkqsP5v5XbBhL3tD5Syx8iNPBYsoU7kY4S2fz7wg8Rj/nztWKCUzGKaFRTv0Xwf3/H058CR1Kvtd/3lRow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "3.4.1", + "@electron/fuses": "^1.8.0", + "@electron/get": "^3.0.0", + "@electron/notarize": "2.5.0", + "@electron/osx-sign": "1.3.3", + "@electron/rebuild": "^4.0.4", + "@electron/universal": "2.0.3", + "@malept/flatpak-bundler": "^0.4.0", + "@noble/hashes": "^2.2.0", + "@peculiar/webcrypto": "^1.7.1", + "@types/fs-extra": "9.0.13", + "ajv": "^8.18.0", + "asn1js": "^3.0.10", + "async-exit-hook": "^2.0.1", + "builder-util": "26.15.3", + "builder-util-runtime": "9.7.0", + "chromium-pickle-js": "^0.2.0", + "ci-info": "4.3.1", + "debug": "^4.3.4", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "ejs": "^3.1.8", + "electron-publish": "26.15.3", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "isbinaryfile": "^5.0.0", + "jiti": "^2.4.2", + "js-yaml": "^4.1.0", + "json5": "^2.2.3", + "lazy-val": "^1.0.5", + "minimatch": "^10.2.5", + "pkijs": "^3.4.0", + "plist": "3.1.0", + "proper-lockfile": "^4.1.2", + "resedit": "^1.7.0", + "semver": "~7.7.3", + "tar": "^7.5.7", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0", + "unzipper": "^0.12.3", + "which": "^5.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "26.15.3", + "electron-builder-squirrel-windows": "26.15.3" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", + "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/app-builder-lib/node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/app-builder-lib/node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/app-builder-lib/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/asn1js": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", + "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.5", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/ast-kit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz", + "integrity": "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "pathe": "^2.0.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/ast-walker-scope": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/ast-walker-scope/-/ast-walker-scope-0.9.0.tgz", + "integrity": "sha512-IJdzo2vLiElBxKzwS36VsCue/62d6IdWjnPB2v3nuPKeWGynp6FF/CYoLa5i/3jXH/z97ZDdsXz6abpgM6w07A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@babel/types": "^7.29.0", + "ast-kit": "^2.2.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz", + "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/builder-util": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.15.3.tgz", + "integrity": "sha512-q2hn7Mbo2nFNkVekPiHFx6Nfo3hURmES3tfBn+k5Pqxl2RkmP3QGqZUhH/q9Pch/4G05NRhPjDlVj1O8q4Txvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "builder-util-runtime": "9.7.0", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.6", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "js-yaml": "^4.1.0", + "sanitize-filename": "^1.6.3", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.7.0.tgz", + "integrity": "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/builder-util/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/builder-util/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/bytestreamjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", + "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-dirname": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", + "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/dir-compare": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " + } + }, + "node_modules/dir-compare/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/dir-compare/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/dir-compare/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dmg-builder": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.15.3.tgz", + "integrity": "sha512-O3zJUFUYHJKgzPqioHxfxzBzlSC1eXCSr79gMSBKBP5AgjjpmrydMsMLotEg9fAJF36vdUncb+4ndRNxoPdlSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.15.3", + "builder-util": "26.15.3", + "fs-extra": "^10.1.0", + "js-yaml": "^4.1.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "43.4.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-43.4.0.tgz", + "integrity": "sha512-3qxGF0CeQbiox5oWV1JlbWGQ1VerbmDhTFqW4sJ8h7uqTHniFYPObXJcDna0DMh32et0fFyKzz0YY8lJv3t5jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-internal/extract-zip": "^1.0.1", + "@electron/get": "^5.0.0", + "@types/node": "^24.9.0" + }, + "bin": { + "electron": "cli.js", + "install-electron": "install.js" + }, + "engines": { + "node": ">= 22.12.0" + } + }, + "node_modules/electron-builder": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.15.3.tgz", + "integrity": "sha512-a1KM5heqS3gQCZzizXEI8RjJy3QVogULPdeSknt76uLDpBIW/HDGsMg/XgP0riP6PI9COsRvFITKKGDqA8fJxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.15.3", + "builder-util": "26.15.3", + "builder-util-runtime": "9.7.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "dmg-builder": "26.15.3", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder-squirrel-windows": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.15.3.tgz", + "integrity": "sha512-Jc19XPV9y9+2bAdZPkXuVNGNIEFBq9poHC61l8Kv6FdK7DRG3+Ic0rerC0DXOaeHNz8yW0fg/JnF8GQROOF5MA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "26.15.3", + "builder-util": "26.15.3", + "electron-winstaller": "5.4.0" + } + }, + "node_modules/electron-publish": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.15.3.tgz", + "integrity": "sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "aws4": "^1.13.2", + "builder-util": "26.15.3", + "builder-util-runtime": "9.7.0", + "chalk": "^4.1.2", + "form-data": "^4.0.5", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-winstaller": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", + "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@electron/asar": "^3.2.1", + "debug": "^4.1.1", + "fs-extra": "^7.0.1", + "lodash": "^4.17.21", + "temp": "^0.9.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "@electron/windows-sign": "^1.1.2" + } + }, + "node_modules/electron-winstaller/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/electron-winstaller/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "peer": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-winstaller/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/exsolve": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.1.tgz", + "integrity": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "devOptional": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/local-pkg": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.2.1.tgz", + "integrity": "sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==", + "license": "MIT", + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.3.0", + "quansync": "^0.2.11" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magic-string-ast": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/magic-string-ast/-/magic-string-ast-1.0.3.tgz", + "integrity": "sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==", + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.19" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/mlly/node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "license": "MIT" + }, + "node_modules/mlly/node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-abi": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.33.0.tgz", + "integrity": "sha512-vLBWCKb+7LWsX+TbfzWOkw0W81m377tyx3hOweBTjO43CXZnRGS1/JPWs20fr0PgZyDXk6ROYrylsEycK8raDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/node-api-version": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + } + }, + "node_modules/node-gyp": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz", + "integrity": "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "undici": "^6.25.0", + "which": "^6.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-gyp/node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/node-gyp/node_modules/undici": { + "version": "6.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz", + "integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^4.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nostics": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/nostics/-/nostics-1.2.0.tgz", + "integrity": "sha512-FGqEfhQjrvo1lL8KFifdTQiNwwQHJxC1jtYE1Rc54qF/jxONUNL+kC9gS1krX8Q65PgrQ5fCqH/I4NhWBvdSqg==", + "license": "MIT" + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/pe-library": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", + "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-4.0.2.tgz", + "integrity": "sha512-yKVVA7bSj5oRZFp/Ab9wLlmyb5gPUYEiIm4ryiWTe/xe7PtkRdMVOp1X1ggvq0c6Uj7Q0Du1HnV2mtAwM0Ks1g==", + "license": "MIT", + "dependencies": { + "nostics": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "@vue/devtools-api": "^8.1.5", + "typescript": ">=5.6.0", + "vue": "^3.5.11" + }, + "peerDependenciesMeta": { + "@vue/devtools-api": { + "optional": false + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/pkg-types": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", + "license": "MIT", + "dependencies": { + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" + } + }, + "node_modules/pkijs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.0.tgz", + "integrity": "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@noble/hashes": "1.4.0", + "asn1js": "^3.0.6", + "bytestreamjs": "^2.0.1", + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/pkijs/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postject": { + "version": "1.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", + "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "commander": "^9.4.0" + }, + "bin": { + "postject": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/postject/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.2.0.tgz", + "integrity": "sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readdirp": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resedit": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", + "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pe-library": "^0.4.1" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/rolldown": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.3.tgz", + "integrity": "sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.143.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.2.3", + "@rolldown/binding-darwin-arm64": "1.2.3", + "@rolldown/binding-darwin-x64": "1.2.3", + "@rolldown/binding-freebsd-x64": "1.2.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.3", + "@rolldown/binding-linux-arm64-gnu": "1.2.3", + "@rolldown/binding-linux-arm64-musl": "1.2.3", + "@rolldown/binding-linux-ppc64-gnu": "1.2.3", + "@rolldown/binding-linux-s390x-gnu": "1.2.3", + "@rolldown/binding-linux-x64-gnu": "1.2.3", + "@rolldown/binding-linux-x64-musl": "1.2.3", + "@rolldown/binding-openharmony-arm64": "1.2.3", + "@rolldown/binding-win32-arm64-msvc": "1.2.3", + "@rolldown/binding-win32-x64-msvc": "1.2.3" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/sanitize-filename": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", + "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/scule": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", + "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/three": { + "version": "0.185.1", + "resolved": "https://registry.npmjs.org/three/-/three-0.185.1.tgz", + "integrity": "sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==", + "license": "MIT" + }, + "node_modules/tiny-async-pool": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", + "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.5.0" + } + }, + "node_modules/tiny-async-pool/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "license": "MIT" + }, + "node_modules/undici": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unplugin": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.3.0.tgz", + "integrity": "sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "picomatch": "^4.0.4", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@farmfe/core": "*", + "@rspack/core": "*", + "bun-types-no-globals": "*", + "esbuild": "*", + "rolldown": "*", + "rollup": "*", + "unloader": "*", + "vite": "*", + "webpack": "*" + }, + "peerDependenciesMeta": { + "@farmfe/core": { + "optional": true + }, + "@rspack/core": { + "optional": true + }, + "bun-types-no-globals": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "rolldown": { + "optional": true + }, + "rollup": { + "optional": true + }, + "unloader": { + "optional": true + }, + "vite": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/unplugin-utils": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.2.tgz", + "integrity": "sha512-xVToRh2CTmLk2HnEG7ac4rl1MJTT3RFkpS8B++/SnB0kXvuaavD+n3m/vrzyWQOdJNSZQACnbz01pnppbwV5BA==", + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/unzipper": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.12.5.tgz", + "integrity": "sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "~3.7.2", + "duplexer2": "~0.1.4", + "fs-extra": "11.3.1", + "graceful-fs": "^4.2.2", + "node-int64": "^0.4.0" + } + }, + "node_modules/unzipper/node_modules/fs-extra": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz", + "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz", + "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.25", + "rolldown": "~1.2.1", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.41.tgz", + "integrity": "sha512-2laE0p+aK+/AOPG/XL/WepOs/GlK755LJ1XECi9kDUrz1FKNw8rb2Xzlw9JS1rqEV55nb0ttsKxVlTCcd+R5cg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.41", + "@vue/compiler-sfc": "3.5.41", + "@vue/runtime-dom": "3.5.41", + "@vue/server-renderer": "3.5.41", + "@vue/shared": "3.5.41" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.2.0.tgz", + "integrity": "sha512-QAC5i0LEb1GLG0LXDQmHu8L7FX12j0KwU/JTKmLQUJMrn04gQdKP6Du+p0QwpHb3iy71vBlqnHQ8WAfOSAWhqw==", + "license": "MIT", + "dependencies": { + "@babel/generator": "^8.0.0", + "@vue-macros/common": "^3.1.3", + "@vue/devtools-api": "^8.1.5", + "ast-walker-scope": "^0.9.0", + "chokidar": "^5.0.0", + "json5": "^2.2.3", + "local-pkg": "^1.2.1", + "magic-string": "^0.30.21", + "mlly": "^1.8.2", + "muggle-string": "^0.4.1", + "nostics": "^1.1.4", + "pathe": "^2.0.3", + "picomatch": "^4.0.5", + "scule": "^1.3.0", + "tinyglobby": "^0.2.17", + "unplugin": "^3.3.0", + "unplugin-utils": "^0.3.2", + "yaml": "^2.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "@pinia/colada": ">=0.21.2", + "@vue/compiler-sfc": "^3.5.34 || ^4.0.0", + "pinia": "^3.0.4 || ^4.0.2", + "vite": "^7.3.0 || ^8.0.0", + "vue": "^3.5.34 || ^4.0.0" + }, + "peerDependenciesMeta": { + "@pinia/colada": { + "optional": true + }, + "@vue/compiler-sfc": { + "optional": true + }, + "pinia": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/webcrypto-core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.9.2.tgz", + "integrity": "sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/json-schema": "^1.1.12", + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "license": "MIT" + }, + "node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json index 3c82eaa..82daba8 100644 --- a/package.json +++ b/package.json @@ -1,31 +1,66 @@ { - "name": "spa-view", + "name": "nl-game", "private": true, - "version": "0.0.0", + "version": "1.0.0", + "description": "像素游戏厅:43 款小游戏平台(单机 + 联机对战 + AI 陪玩)", + "author": "nl-game", "type": "module", + "main": "electron/main.cjs", "scripts": { "dev": "vite", "build": "vite build", - "preview": "vite preview" + "preview": "vite preview", + "electron": "electron .", + "electron:e2e": "vite build --base=./ && electron electron/e2e.cjs", + "electron:dist": "vite build --base=./", + "electron:pack": "vite build --base=./ && electron-builder --win --dir", + "electron:build": "vite build --base=./ && electron-builder --win nsis" }, "dependencies": { - "tailwindcss": "^4.1.4", - "vue": "^3.5.13", - "@ant-design-vue/pro-layout": "^3.2.5", - "@ant-design/icons-vue": "^7.0.1", - "@fortawesome/fontawesome-free": "^6.7.2", - "@tailwindcss/vite": "^4.1.4", - "@vueuse/head": "^2.0.0", - "ant-design-vue": "^4.1.1", - "axios": "^1.6.2", - "lucide-vue-next": "^0.507.0", - "pinia": "^2.1.7", - "swiper": "^11.0.5", - "vue-router": "^4.2.5" + "@fontsource/press-start-2p": "^5.3.0", + "@fontsource/vt323": "^5.3.0", + "axios": "^1.19.0", + "pinia": "^4.0.2", + "three": "^0.185.1", + "vue": "^3.5.41", + "vue-router": "^5.2.0" }, "devDependencies": { - "@vitejs/plugin-vue": "^5.2.2", - "sass": "^1.71.0", - "vite": "^6.3.1" + "@vitejs/plugin-vue": "^6.0.8", + "electron": "^43.4.0", + "electron-builder": "^26.15.3", + "vite": "^8.2.1" + }, + "build": { + "appId": "com.nlgame.desktop", + "productName": "像素游戏厅", + "electronDist": "node_modules/electron/dist", + "directories": { + "output": "release" + }, + "files": [ + "dist/**/*", + "electron/**/*", + "!electron/e2e.cjs", + "package.json" + ], + "asar": true, + "win": { + "executableName": "PixelArcade", + "target": [ + { + "target": "nsis", + "arch": [ + "x64" + ] + } + ] + }, + "nsis": { + "oneClick": false, + "allowToChangeInstallationDirectory": true, + "shortcutName": "像素游戏厅", + "artifactName": "PixelArcade-Setup-${version}.${ext}" + } } } diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 0000000..47edd32 Binary files /dev/null and b/public/favicon.png differ diff --git a/src/App.vue b/src/App.vue index 3602e60..faf9548 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,10 +1,77 @@ - + + diff --git a/src/api/http.js b/src/api/http.js new file mode 100644 index 0000000..7d8bb4e --- /dev/null +++ b/src/api/http.js @@ -0,0 +1,57 @@ +// axios 请求封装:自动携带 Token、统一错误提示、401 跳登录 +import axios from 'axios' +import router from '../router' + +// 后端服务器基址:Electron 桌面端由 preload 注入(file:// 下相对路径不可用), +// 浏览器环境返回空串走同源相对路径(开发时由 Vite 代理转发) +export function serverBase() { + return (window.desktop?.serverBase || '').replace(/\/+$/, '') +} + +// 全局轻量提示(右上角浮动消息,避免引入 UI 库) +export function toast(msg, type = 'error') { + const el = document.createElement('div') + el.className = `toast toast-${type}` + el.textContent = msg + document.body.appendChild(el) + // 入场动画 + requestAnimationFrame(() => el.classList.add('show')) + setTimeout(() => { + el.classList.remove('show') + setTimeout(() => el.remove(), 300) + }, 2200) +} + +const http = axios.create({ baseURL: serverBase() + '/api', timeout: 15000 }) + +// 请求拦截:附加 JWT +http.interceptors.request.use((config) => { + const token = localStorage.getItem('token') + if (token) config.headers.Authorization = `Bearer ${token}` + return config +}) + +// 响应拦截:code!=0 视为业务失败并弹提示;401 清凭证回登录页 +http.interceptors.response.use( + (res) => { + const body = res.data + if (body.code !== 0) { + toast(body.msg || '操作失败') + return Promise.reject(new Error(body.msg)) + } + return body.data + }, + (err) => { + if (err.response?.status === 401) { + localStorage.removeItem('token') + localStorage.removeItem('user') + toast('登录已失效,请重新登录') + router.push('/login') + } else if (err.code !== 'ERR_CANCELED') { + toast(err.response?.data?.msg || '网络异常,请稍后再试') + } + return Promise.reject(err) + } +) + +export default http diff --git a/src/api/request.js b/src/api/request.js deleted file mode 100644 index 193d05f..0000000 --- a/src/api/request.js +++ /dev/null @@ -1,75 +0,0 @@ -import axios from 'axios'; -import {message as msg} from "ant-design-vue"; - -// 创建 Axios 实例 -const service = axios.create({ - baseURL: '/api/', // 这里可以设置你的 API 基础地址 - timeout: 5000 // 请求超时时间 -}); - -// 请求拦截器 -service.interceptors.request.use( - config => { - // 从本地存储中获取 token - const token = localStorage.getItem('token'); - if (token) { - // 设置请求头中的 Authorization - config.headers['Authorization'] = `Bearer ${token}`; - } - return config; - }, - error => { - console.log(error); // 打印错误信息 - return Promise.reject(error); - } -); - -// 响应拦截器 -service.interceptors.response.use( - response => { - const {code, result, message} = response.data; - if (code === 0) { - return result; - } else { - if (code === 401) { - msg.error(message).then(r => { - localStorage.removeItem('token'); - localStorage.removeItem('user'); - window.location.href = '/' - msg.destroy() - }) - return; - } - msg.error(message).then(r => { - msg.destroy() - }) - } - }, - error => { - console.log('err' + error); // 打印错误信息 - return Promise.reject(error); - } -); - -// 封装 get 请求 -const get = (url, params = {}) => { - return service.get(url, { params }); -}; - -// 封装 post 请求 -const post = (url, data = {}) => { - return service.post(url, data); -}; - -// 封装上传文件请求 -const upload = (url, file) => { - const formData = new FormData(); - formData.append('file', file); - return service.post(url, formData, { - headers: { - 'Content-Type': 'multipart/form-data' - } - }); -}; - -export { get, post, upload }; \ No newline at end of file diff --git a/src/components/ChatWidget.vue b/src/components/ChatWidget.vue new file mode 100644 index 0000000..1f82674 --- /dev/null +++ b/src/components/ChatWidget.vue @@ -0,0 +1,448 @@ + + + + + diff --git a/src/components/GameCard.vue b/src/components/GameCard.vue new file mode 100644 index 0000000..91ba102 --- /dev/null +++ b/src/components/GameCard.vue @@ -0,0 +1,142 @@ + + + + + diff --git a/src/components/GameCarousel.vue b/src/components/GameCarousel.vue new file mode 100644 index 0000000..4e4c301 --- /dev/null +++ b/src/components/GameCarousel.vue @@ -0,0 +1,290 @@ + + + + + diff --git a/src/components/GameIcon.vue b/src/components/GameIcon.vue new file mode 100644 index 0000000..b0775f9 --- /dev/null +++ b/src/components/GameIcon.vue @@ -0,0 +1,117 @@ + + + + + diff --git a/src/components/LevelSelect.vue b/src/components/LevelSelect.vue new file mode 100644 index 0000000..f0c3b1d --- /dev/null +++ b/src/components/LevelSelect.vue @@ -0,0 +1,170 @@ + + + + + diff --git a/src/components/NavBar.vue b/src/components/NavBar.vue new file mode 100644 index 0000000..ff40ab1 --- /dev/null +++ b/src/components/NavBar.vue @@ -0,0 +1,290 @@ + + + + + diff --git a/src/components/NavIcon.vue b/src/components/NavIcon.vue new file mode 100644 index 0000000..fd40845 --- /dev/null +++ b/src/components/NavIcon.vue @@ -0,0 +1,204 @@ + + + diff --git a/src/components/PixelAvatar.vue b/src/components/PixelAvatar.vue new file mode 100644 index 0000000..88d4f0e --- /dev/null +++ b/src/components/PixelAvatar.vue @@ -0,0 +1,68 @@ +// 像素头像渲染组件:把 'px:NN' 编码画成像素图(8×8 网格放大,锐利无插值) +// 兼容旧 emoji 数据:非像素编码时原样以文字显示,历史用户零迁移 + + + + + diff --git a/src/components/RoomInviteModal.vue b/src/components/RoomInviteModal.vue new file mode 100644 index 0000000..2256430 --- /dev/null +++ b/src/components/RoomInviteModal.vue @@ -0,0 +1,168 @@ +// 全局房间邀请弹窗:好友房主邀请你时实时弹出(挂载在 App.vue,任何页面可见) +// 接受 → 跳到对应房间页并自动入座;拒绝/超时 → 关闭 + + + + + diff --git a/src/components/VirtualList.vue b/src/components/VirtualList.vue new file mode 100644 index 0000000..f7ec0ca --- /dev/null +++ b/src/components/VirtualList.vue @@ -0,0 +1,101 @@ + + + + + diff --git a/src/components/battle/BilliardsTable.vue b/src/components/battle/BilliardsTable.vue new file mode 100644 index 0000000..aea9322 --- /dev/null +++ b/src/components/battle/BilliardsTable.vue @@ -0,0 +1,1365 @@ + + + + + diff --git a/src/components/battle/ChessBoard.vue b/src/components/battle/ChessBoard.vue new file mode 100644 index 0000000..976a11d --- /dev/null +++ b/src/components/battle/ChessBoard.vue @@ -0,0 +1,487 @@ + + + + + diff --git a/src/components/battle/DdzTable.vue b/src/components/battle/DdzTable.vue new file mode 100644 index 0000000..67da03f --- /dev/null +++ b/src/components/battle/DdzTable.vue @@ -0,0 +1,623 @@ + + + + + diff --git a/src/components/battle/Dice3D.vue b/src/components/battle/Dice3D.vue new file mode 100644 index 0000000..f8769df --- /dev/null +++ b/src/components/battle/Dice3D.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/src/components/battle/LudoBoard.vue b/src/components/battle/LudoBoard.vue new file mode 100644 index 0000000..696c63b --- /dev/null +++ b/src/components/battle/LudoBoard.vue @@ -0,0 +1,719 @@ + + + + + diff --git a/src/components/battle/MonopolyBoard.vue b/src/components/battle/MonopolyBoard.vue new file mode 100644 index 0000000..938bd81 --- /dev/null +++ b/src/components/battle/MonopolyBoard.vue @@ -0,0 +1,884 @@ + + + + + diff --git a/src/components/battle/PlayingCard.vue b/src/components/battle/PlayingCard.vue new file mode 100644 index 0000000..51863ba --- /dev/null +++ b/src/components/battle/PlayingCard.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/src/games/components/AimGame.vue b/src/games/components/AimGame.vue new file mode 100644 index 0000000..89bd41a --- /dev/null +++ b/src/games/components/AimGame.vue @@ -0,0 +1,175 @@ + + + diff --git a/src/games/components/AsteroidsGame.vue b/src/games/components/AsteroidsGame.vue new file mode 100644 index 0000000..9497f43 --- /dev/null +++ b/src/games/components/AsteroidsGame.vue @@ -0,0 +1,270 @@ + + + diff --git a/src/games/components/BallMaze3dGame.vue b/src/games/components/BallMaze3dGame.vue new file mode 100644 index 0000000..05fc1ca --- /dev/null +++ b/src/games/components/BallMaze3dGame.vue @@ -0,0 +1,251 @@ + + + + + diff --git a/src/games/components/BreakoutGame.vue b/src/games/components/BreakoutGame.vue new file mode 100644 index 0000000..7e8c525 --- /dev/null +++ b/src/games/components/BreakoutGame.vue @@ -0,0 +1,206 @@ + + + diff --git a/src/games/components/BubbleGame.vue b/src/games/components/BubbleGame.vue new file mode 100644 index 0000000..87787b5 --- /dev/null +++ b/src/games/components/BubbleGame.vue @@ -0,0 +1,333 @@ + + + diff --git a/src/games/components/ColorReactGame.vue b/src/games/components/ColorReactGame.vue new file mode 100644 index 0000000..3352de4 --- /dev/null +++ b/src/games/components/ColorReactGame.vue @@ -0,0 +1,175 @@ + + + + + diff --git a/src/games/components/Connect4Game.vue b/src/games/components/Connect4Game.vue new file mode 100644 index 0000000..392ef5c --- /dev/null +++ b/src/games/components/Connect4Game.vue @@ -0,0 +1,214 @@ + + + + + diff --git a/src/games/components/DinoGame.vue b/src/games/components/DinoGame.vue new file mode 100644 index 0000000..5a31668 --- /dev/null +++ b/src/games/components/DinoGame.vue @@ -0,0 +1,190 @@ + + + diff --git a/src/games/components/Down100Game.vue b/src/games/components/Down100Game.vue new file mode 100644 index 0000000..41b7dfc --- /dev/null +++ b/src/games/components/Down100Game.vue @@ -0,0 +1,181 @@ + + + diff --git a/src/games/components/FlappyGame.vue b/src/games/components/FlappyGame.vue new file mode 100644 index 0000000..d0265bc --- /dev/null +++ b/src/games/components/FlappyGame.vue @@ -0,0 +1,173 @@ + + + diff --git a/src/games/components/FruitGame.vue b/src/games/components/FruitGame.vue new file mode 100644 index 0000000..457bca5 --- /dev/null +++ b/src/games/components/FruitGame.vue @@ -0,0 +1,249 @@ + + + diff --git a/src/games/components/Game2048.vue b/src/games/components/Game2048.vue new file mode 100644 index 0000000..f755712 --- /dev/null +++ b/src/games/components/Game2048.vue @@ -0,0 +1,147 @@ + + + diff --git a/src/games/components/GomokuGame.vue b/src/games/components/GomokuGame.vue new file mode 100644 index 0000000..05730e3 --- /dev/null +++ b/src/games/components/GomokuGame.vue @@ -0,0 +1,244 @@ + + + diff --git a/src/games/components/JumpJumpGame.vue b/src/games/components/JumpJumpGame.vue new file mode 100644 index 0000000..e04af9d --- /dev/null +++ b/src/games/components/JumpJumpGame.vue @@ -0,0 +1,201 @@ + + + diff --git a/src/games/components/KlotskiGame.vue b/src/games/components/KlotskiGame.vue new file mode 100644 index 0000000..db5f298 --- /dev/null +++ b/src/games/components/KlotskiGame.vue @@ -0,0 +1,204 @@ + + + + + diff --git a/src/games/components/LightsOutGame.vue b/src/games/components/LightsOutGame.vue new file mode 100644 index 0000000..df751cc --- /dev/null +++ b/src/games/components/LightsOutGame.vue @@ -0,0 +1,147 @@ + + + + + diff --git a/src/games/components/LinkGame.vue b/src/games/components/LinkGame.vue new file mode 100644 index 0000000..b837302 --- /dev/null +++ b/src/games/components/LinkGame.vue @@ -0,0 +1,527 @@ + + + + + diff --git a/src/games/components/MarioGame.vue b/src/games/components/MarioGame.vue new file mode 100644 index 0000000..1ed5a2c --- /dev/null +++ b/src/games/components/MarioGame.vue @@ -0,0 +1,904 @@ + + + diff --git a/src/games/components/Match3Game.vue b/src/games/components/Match3Game.vue new file mode 100644 index 0000000..0dc5529 --- /dev/null +++ b/src/games/components/Match3Game.vue @@ -0,0 +1,746 @@ + + + + + diff --git a/src/games/components/MathRushGame.vue b/src/games/components/MathRushGame.vue new file mode 100644 index 0000000..d5869e1 --- /dev/null +++ b/src/games/components/MathRushGame.vue @@ -0,0 +1,187 @@ + + + + + diff --git a/src/games/components/MazeGame.vue b/src/games/components/MazeGame.vue new file mode 100644 index 0000000..faf5c81 --- /dev/null +++ b/src/games/components/MazeGame.vue @@ -0,0 +1,160 @@ + + + diff --git a/src/games/components/MemoryGame.vue b/src/games/components/MemoryGame.vue new file mode 100644 index 0000000..21d083b --- /dev/null +++ b/src/games/components/MemoryGame.vue @@ -0,0 +1,150 @@ + + + + + diff --git a/src/games/components/MinesweeperGame.vue b/src/games/components/MinesweeperGame.vue new file mode 100644 index 0000000..cb9c113 --- /dev/null +++ b/src/games/components/MinesweeperGame.vue @@ -0,0 +1,195 @@ + + + diff --git a/src/games/components/PianoTilesGame.vue b/src/games/components/PianoTilesGame.vue new file mode 100644 index 0000000..094a1e5 --- /dev/null +++ b/src/games/components/PianoTilesGame.vue @@ -0,0 +1,166 @@ + + + diff --git a/src/games/components/PongGame.vue b/src/games/components/PongGame.vue new file mode 100644 index 0000000..91fce91 --- /dev/null +++ b/src/games/components/PongGame.vue @@ -0,0 +1,168 @@ + + + diff --git a/src/games/components/Puzzle15Game.vue b/src/games/components/Puzzle15Game.vue new file mode 100644 index 0000000..5df6e6c --- /dev/null +++ b/src/games/components/Puzzle15Game.vue @@ -0,0 +1,123 @@ + + + + + diff --git a/src/games/components/PvzGame.vue b/src/games/components/PvzGame.vue new file mode 100644 index 0000000..5b67e6c --- /dev/null +++ b/src/games/components/PvzGame.vue @@ -0,0 +1,440 @@ + + + + + diff --git a/src/games/components/Racing3dGame.vue b/src/games/components/Racing3dGame.vue new file mode 100644 index 0000000..0d1bd07 --- /dev/null +++ b/src/games/components/Racing3dGame.vue @@ -0,0 +1,241 @@ + + + diff --git a/src/games/components/SimonGame.vue b/src/games/components/SimonGame.vue new file mode 100644 index 0000000..85f584b --- /dev/null +++ b/src/games/components/SimonGame.vue @@ -0,0 +1,154 @@ + + + + + diff --git a/src/games/components/SnakeGame.vue b/src/games/components/SnakeGame.vue new file mode 100644 index 0000000..fe622bf --- /dev/null +++ b/src/games/components/SnakeGame.vue @@ -0,0 +1,194 @@ + + + diff --git a/src/games/components/SpaceShooterGame.vue b/src/games/components/SpaceShooterGame.vue new file mode 100644 index 0000000..4cced55 --- /dev/null +++ b/src/games/components/SpaceShooterGame.vue @@ -0,0 +1,243 @@ + + + diff --git a/src/games/components/Stack3dGame.vue b/src/games/components/Stack3dGame.vue new file mode 100644 index 0000000..543c06e --- /dev/null +++ b/src/games/components/Stack3dGame.vue @@ -0,0 +1,177 @@ + + + diff --git a/src/games/components/StarveGame.vue b/src/games/components/StarveGame.vue new file mode 100644 index 0000000..853fa52 --- /dev/null +++ b/src/games/components/StarveGame.vue @@ -0,0 +1,3506 @@ + + + + + diff --git a/src/games/components/SudokuGame.vue b/src/games/components/SudokuGame.vue new file mode 100644 index 0000000..dd63e43 --- /dev/null +++ b/src/games/components/SudokuGame.vue @@ -0,0 +1,248 @@ + + + + + diff --git a/src/games/components/TankGame.vue b/src/games/components/TankGame.vue new file mode 100644 index 0000000..1748158 --- /dev/null +++ b/src/games/components/TankGame.vue @@ -0,0 +1,311 @@ + + + diff --git a/src/games/components/TetrisGame.vue b/src/games/components/TetrisGame.vue new file mode 100644 index 0000000..a4babcf --- /dev/null +++ b/src/games/components/TetrisGame.vue @@ -0,0 +1,217 @@ + + + diff --git a/src/games/components/TicTacToeGame.vue b/src/games/components/TicTacToeGame.vue new file mode 100644 index 0000000..4f24a00 --- /dev/null +++ b/src/games/components/TicTacToeGame.vue @@ -0,0 +1,148 @@ + + + + + diff --git a/src/games/components/TypingGame.vue b/src/games/components/TypingGame.vue new file mode 100644 index 0000000..e091f50 --- /dev/null +++ b/src/games/components/TypingGame.vue @@ -0,0 +1,161 @@ + + + diff --git a/src/games/components/WhackGame.vue b/src/games/components/WhackGame.vue new file mode 100644 index 0000000..ed6c66d --- /dev/null +++ b/src/games/components/WhackGame.vue @@ -0,0 +1,158 @@ + + + + + diff --git a/src/games/components/marioLevels.js b/src/games/components/marioLevels.js new file mode 100644 index 0000000..dc5b745 --- /dev/null +++ b/src/games/components/marioLevels.js @@ -0,0 +1,317 @@ +// 超级玛丽关卡数据:按原版《超级马里奥兄弟》World 1-1 / 1-2 / 1-3 / 1-4 布局还原 +// 网格 15 行(13 行天空 + 2 行地面),每格 32px,与原版 NES 屏幕比例一致 +// 图例: +// # 地面砖/墙 % 硬块(阶梯石块) = 可顶碎的砖块 +// ? 问号块(金币) M 问号块(蘑菇) [ ] 水管左/右半 +// o 金币 E 板栗仔(走到悬崖会掉下去) +// T 乌龟(红龟习性,走到平台边缘会折返) +// S 出生点 F 旗杆 +// 城堡关(1-4)专用: +// ~ 岩浆(碰到即死) X 旋转火棍轴心(硬块 + 火球链) +// - 木桥板(砍斧后塌落) A 斧头(摸到砍断桥,库巴落岩浆过关) +// B 库巴(守桥 Boss,不可踩,只能跳过) +export const ROWS = 15 + +function emptyGrid(width) { + return Array.from({ length: ROWS }, () => Array(width).fill(' ')) +} +function put(g, c, r, ch) { + g[r][c] = ch +} +function fillRow(g, r, c1, c2, ch) { + for (let c = c1; c <= c2; c++) g[r][c] = ch +} +// 地面:两行厚的地砖(原版地面即 2 格厚) +function ground(g, c1, c2) { + fillRow(g, 13, c1, c2, '#') + fillRow(g, 14, c1, c2, '#') +} +// 水管:高 h 格,占两列 +function pipe(g, c, h) { + for (let r = 13 - h; r <= 12; r++) { + put(g, c, r, '[') + put(g, c + 1, r, ']') + } +} +// 实心柱:从地面往上 h 格 +function column(g, c, h, ch) { + for (let r = 13 - h; r <= 12; r++) put(g, c, r, ch) +} +// 上行阶梯:从左到右逐级升高(原版金字塔/终点阶梯) +function stairUp(g, c1, h) { + for (let i = 0; i < h; i++) column(g, c1 + i, i + 1, '%') +} +// 下行阶梯:从左到右逐级降低 +function stairDown(g, c1, h) { + for (let i = 0; i < h; i++) column(g, c1 + i, h - i, '%') +} +// 竖墙:指定行区间填实 +function wall(g, c, r1, r2) { + for (let r = r1; r <= r2; r++) put(g, c, r, '#') +} +// 岩浆池:填满地面两行的坑 +function lava(g, c1, c2) { + fillRow(g, 13, c1, c2, '~') + fillRow(g, 14, c1, c2, '~') +} + +// --------------------------------------------------------------- +// World 1-1:地面关。所有标志性段落按原版顺序还原: +// 开场问号块阵 → 四根渐高水管(第四根可钻入地下奖励室)→ 双坑 → +// 高空砖排 → 金字塔阶梯对 → 终点八级阶梯 → 旗杆与城堡 +// --------------------------------------------------------------- +function buildWorld11() { + const g = emptyGrid(234) + // 地面四段,之间是三个坑(2 格、3 格、2 格,与原版一致) + ground(g, 0, 68) + ground(g, 71, 87) + ground(g, 91, 135) + ground(g, 138, 203) + put(g, 3, 12, 'S') + // 开场:单个问号块 + 砖?砖?砖 五连阵,高处再悬一个问号块 + put(g, 16, 9, '?') + put(g, 20, 9, '=') + put(g, 21, 9, 'M') // 原版第一个蘑菇就藏在这里 + put(g, 22, 9, '=') + put(g, 23, 9, '?') + put(g, 24, 9, '=') + put(g, 22, 5, '?') + put(g, 25, 12, 'E') // 全游戏第一只板栗仔 + // 四根水管:高度 2/3/4/4,最后一根是通往地下奖励室的暗道 + pipe(g, 28, 2) + pipe(g, 38, 3) + pipe(g, 46, 4) + pipe(g, 57, 4) + put(g, 36, 12, 'E') + put(g, 51, 12, 'E') + put(g, 53, 12, 'E') + // 第一个坑之后:砖?砖 + 8 连高空砖排 + put(g, 77, 9, '=') + put(g, 78, 9, '?') + put(g, 79, 9, '=') + fillRow(g, 5, 80, 87, '=') + put(g, 81, 12, 'E') + put(g, 84, 12, 'E') + // 第二个坑上方:高空砖收尾 + 蘑菇问号块 + fillRow(g, 5, 91, 92, '=') + put(g, 93, 5, 'M') + put(g, 96, 9, '=') + put(g, 100, 12, 'E') + put(g, 102, 12, 'E') + // 双问号块 + 乌龟(原版中段唯一一只绿龟位置) + put(g, 106, 9, '?') + put(g, 109, 9, '?') + put(g, 107, 12, 'T') + fillRow(g, 5, 112, 114, '=') + // 金字塔阶梯第一对:上四级、平两格、下四级 + stairUp(g, 118, 4) + stairDown(g, 124, 4) + // 金字塔第二对:上四级后隔着 2 格深坑接双柱下三级(原版著名的"坑上跳") + stairUp(g, 132, 4) + column(g, 138, 4, '%') + column(g, 139, 4, '%') + stairDown(g, 140, 3) + // 终点前两根矮水管夹着一对板栗仔 + pipe(g, 147, 2) + put(g, 151, 12, 'E') + put(g, 153, 12, 'E') + pipe(g, 163, 2) + // 终点八级大阶梯 → 旗杆 → 城堡 + stairUp(g, 169, 8) + put(g, 184, 12, 'F') + // 右侧挡墙:防止跑出主世界(正好在镜头最大范围之外) + wall(g, 204, 0, 12) + wall(g, 205, 0, 12) + ground(g, 204, 205) + // 地下奖励室(原版 1-1 暗道):顶棚 + 四面墙 + 两排金币 + 出口水管 + fillRow(g, 0, 206, 232, '#') + fillRow(g, 1, 206, 232, '#') + ground(g, 206, 232) + wall(g, 206, 2, 12) + wall(g, 232, 2, 12) + fillRow(g, 8, 212, 222, 'o') + fillRow(g, 11, 212, 222, 'o') + pipe(g, 227, 2) + return { + name: '1-1', + theme: 'overworld', + grid: g, + mainCols: 204, // 主世界镜头边界(列) + bonusStartCol: 206, // 奖励室起始列(镜头与配色切换分界) + castleCol: 189, + warps: [ + // 站在第四根水管顶按 ↓ → 进地下奖励室;站在出口水管顶按 ↓ → 回主世界 + { cols: [57, 58], row: 9, dest: { c: 209, r: 4 } }, + { cols: [227, 228], row: 11, dest: { c: 61, r: 13 } }, + ], + } +} + +// --------------------------------------------------------------- +// World 1-2:地下关。全程顶棚封顶,蓝砖配色: +// 开场双层砖台阶配金币 → 砖柱穿行 → 深坑 → 高台金币 → +// 硬块阶梯群 → 地底水管 → 出口长阶梯 +// --------------------------------------------------------------- +function buildWorld12() { + const g = emptyGrid(140) + // 顶棚(原版地下关标志性的两格厚天花板) + fillRow(g, 0, 0, 139, '#') + fillRow(g, 1, 0, 139, '#') + // 地面三段,两个深坑 + ground(g, 0, 45) + ground(g, 49, 65) + ground(g, 69, 139) + put(g, 2, 12, 'S') + // 开场双层砖平台 + 顶上金币排(原版进洞第一眼) + fillRow(g, 9, 8, 13, '=') + fillRow(g, 8, 9, 12, 'o') + fillRow(g, 5, 16, 21, '=') + fillRow(g, 4, 17, 20, 'o') + put(g, 18, 12, 'E') + put(g, 24, 12, 'E') + // 砖柱穿行段:三根渐高砖柱,柱间夹金币 + column(g, 28, 3, '=') + column(g, 34, 4, '=') + column(g, 40, 3, '=') + fillRow(g, 11, 30, 32, 'o') + fillRow(g, 11, 36, 38, 'o') + put(g, 37, 12, 'T') + // 第一个坑(46-48)后的高台:砖排 + 金币 + 台上乌龟巡逻 + fillRow(g, 9, 50, 54, '=') + fillRow(g, 8, 50, 54, 'o') + put(g, 52, 8, 'T') + put(g, 58, 12, 'E') + // 蘑菇问号块 + 金币问号块 + put(g, 60, 9, 'M') + put(g, 62, 9, '?') + // 第二个坑(66-68)后的硬块阶梯群:2/3/4 三柱,柱间乌龟看守 + column(g, 72, 2, '%') + column(g, 76, 3, '%') + column(g, 80, 4, '%') + put(g, 78, 12, 'T') + // 地面金币长排 + 板栗仔双人组 + fillRow(g, 11, 84, 90, 'o') + put(g, 88, 12, 'E') + put(g, 91, 12, 'E') + // 地底水管两根(原版地下关也有水管) + pipe(g, 96, 2) + pipe(g, 104, 3) + put(g, 101, 12, 'E') + // 砖台 + 金币收尾 + fillRow(g, 10, 108, 112, '=') + fillRow(g, 9, 108, 112, 'o') + // 出口六级长阶梯 → 旗杆 + stairUp(g, 118, 6) + put(g, 134, 12, 'F') + return { name: '1-2', theme: 'underground', grid: g, mainCols: 140, warps: [] } +} + +// --------------------------------------------------------------- +// World 1-3:高空平台关。大部分是悬崖,踩着树顶平台跳跃前进: +// 起步島 → 渐高平台链(乌龟看守)→ 中段小岛(蘑菇块)→ +// 高空金币平台 → 踏板跳 → 终点阶梯与旗杆 +// --------------------------------------------------------------- +function buildWorld13() { + const g = emptyGrid(124) + // 起步岛 + ground(g, 0, 8) + put(g, 2, 12, 'S') + // 平台链第一段:低台阶配金币 + fillRow(g, 11, 12, 16, '=') + fillRow(g, 10, 13, 15, 'o') + // 乌龟看守的平台(红龟习性来回巡逻) + fillRow(g, 9, 20, 23, '=') + fillRow(g, 7, 20, 23, 'o') + put(g, 21, 8, 'T') + fillRow(g, 11, 27, 29, '=') + // 渐高平台 + 金币 + 乌龟 + fillRow(g, 8, 33, 37, '=') + fillRow(g, 7, 34, 36, 'o') + put(g, 35, 7, 'T') + fillRow(g, 10, 41, 44, '=') + put(g, 42, 9, 'T') + fillRow(g, 12, 48, 52, '=') + // 中段落脚小岛:悬空蘑菇块 + 板栗仔双人组 + ground(g, 56, 63) + put(g, 58, 8, 'M') + put(g, 60, 12, 'E') + put(g, 62, 12, 'E') + // 后半段高空平台链 + fillRow(g, 10, 67, 70, '=') + fillRow(g, 7, 74, 77, '=') + fillRow(g, 4, 74, 77, 'o') + put(g, 75, 6, 'T') + // 双踏板跳 + fillRow(g, 10, 82, 84, '=') + fillRow(g, 10, 87, 89, '=') + fillRow(g, 9, 87, 89, 'o') + // 终点大陆:板栗仔迎接 + 四级阶梯 + 旗杆 + 城堡 + ground(g, 93, 123) + put(g, 97, 12, 'E') + put(g, 99, 12, 'E') + stairUp(g, 103, 4) + put(g, 112, 12, 'F') + return { name: '1-3', theme: 'athletic', grid: g, mainCols: 124, castleCol: 116, warps: [] } +} + +// --------------------------------------------------------------- +// World 1-4:城堡关(第一世界收官)。灰砖封顶配色,按原版节奏还原: +// 入口大厅 → 岩浆坑与旋转火棍交替的长廊 → 高空砖台捷径 → +// 桥前高台 → 库巴守木桥 → 摸到斧头砍断锁链,桥塌库巴落岩浆 → 过关 +// --------------------------------------------------------------- +function buildWorld14() { + const g = emptyGrid(150) + // 城堡封顶(两格厚天花板) + fillRow(g, 0, 0, 149, '#') + fillRow(g, 1, 0, 149, '#') + // 地面段,坑内全部是岩浆 + ground(g, 0, 37) + ground(g, 42, 63) + ground(g, 68, 95) + ground(g, 100, 121) + ground(g, 138, 149) + lava(g, 38, 41) + lava(g, 64, 67) + lava(g, 96, 99) + lava(g, 122, 137) // 库巴桥下的大岩浆池 + put(g, 2, 12, 'S') + // 入口大厅:一对装饰柱,先见识第一根火棍 + column(g, 8, 1, '%') + column(g, 11, 1, '%') + put(g, 16, 9, 'X') // 半空火棍:练习钻档期 + put(g, 25, 12, 'X') // 贴地火棍:跳过轴心块 + fillRow(g, 8, 30, 33, 'o') + // 第一岩浆坑上方的高空砖台(可走上路避火) + fillRow(g, 9, 44, 48, '=') + fillRow(g, 8, 45, 47, 'o') + put(g, 52, 9, 'X') + put(g, 60, 12, 'X') + // 第二段长廊:蘑菇补给 + 双火棍 + put(g, 71, 9, 'M') + put(g, 76, 9, 'X') + fillRow(g, 10, 80, 83, '=') + fillRow(g, 9, 80, 83, 'o') + put(g, 88, 12, 'X') + fillRow(g, 8, 91, 93, 'o') + // 桥前最后一根火棍 + 上桥高台 + put(g, 106, 9, 'X') + fillRow(g, 11, 112, 114, 'o') + column(g, 118, 1, '%') + column(g, 119, 1, '%') + column(g, 120, 1, '%') + column(g, 121, 1, '%') + // 库巴木桥(横跨大岩浆池,砍斧后整段塌落) + fillRow(g, 12, 122, 137, '-') + put(g, 128, 10, 'B') + // 斧头:桥尾地面上方一格 + put(g, 139, 12, 'A') + // 尾墙封死 + wall(g, 148, 2, 12) + wall(g, 149, 2, 12) + return { name: '1-4', theme: 'castle', grid: g, mainCols: 150, warps: [] } +} + +// 构建全部关卡(每次调用返回全新副本,供重开/复活时重置砖块状态) +export function buildLevels() { + return [buildWorld11(), buildWorld12(), buildWorld13(), buildWorld14()] +} diff --git a/src/games/engine.js b/src/games/engine.js new file mode 100644 index 0000000..7399ac7 --- /dev/null +++ b/src/games/engine.js @@ -0,0 +1,112 @@ +// 小游戏通用辅助:主循环、键盘状态、主题取色、绘图工具 +// 所有 Canvas 游戏共用,保证手感与视觉风格统一 + +// createLoop 创建 requestAnimationFrame 主循环 +// update(dt) 每帧回调,dt 为秒(上限 0.05 防止切后台后大步长) +export function createLoop(update) { + let rafId = 0 + let last = 0 + let running = false + function frame(ts) { + if (!running) return + const dt = Math.min((ts - last) / 1000, 0.05) + last = ts + update(dt) + rafId = requestAnimationFrame(frame) + } + return { + start() { + if (running) return + running = true + last = performance.now() + rafId = requestAnimationFrame(frame) + }, + stop() { + running = false + cancelAnimationFrame(rafId) + }, + get running() { + return running + }, + } +} + +// Keys 键盘状态跟踪:keys.has('ArrowLeft') 查询按住状态,onPress 注册单次按下 +export class Keys { + constructor() { + this.down = new Set() + this.pressHandlers = [] + this._kd = (e) => { + // 游戏用按键阻止页面滚动 + if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' '].includes(e.key)) { + e.preventDefault() + } + if (!this.down.has(e.key)) { + this.pressHandlers.forEach((fn) => fn(e.key)) + } + this.down.add(e.key) + } + this._ku = (e) => this.down.delete(e.key) + } + attach() { + window.addEventListener('keydown', this._kd) + window.addEventListener('keyup', this._ku) + } + detach() { + window.removeEventListener('keydown', this._kd) + window.removeEventListener('keyup', this._ku) + this.down.clear() + } + has(key) { + return this.down.has(key) + } + onPress(fn) { + this.pressHandlers.push(fn) + } +} + +// themeColor 读取当前主题的 CSS 变量颜色(游戏画面与全站主题联动) +export function themeColor(name, fallback) { + const v = getComputedStyle(document.documentElement).getPropertyValue(name).trim() + return v || fallback +} + +// palette 一次性取出游戏常用的主题色 +export function palette() { + return { + bg: themeColor('--bg-card', '#1d1f4e'), + panel: themeColor('--bg-panel', '#16173d'), + primary: themeColor('--primary', '#ff3e7f'), + primary2: themeColor('--primary-2', '#ffb300'), + accent: themeColor('--accent', '#00e5ff'), + text: themeColor('--text', '#ffffff'), + dim: themeColor('--text-dim', '#9a9ac4'), + border: themeColor('--border', '#2c2e6e'), + } +} + +// roundRect 圆角矩形路径(部分旧浏览器无原生 API 时的兜底实现) +export function roundRect(ctx, x, y, w, h, r) { + if (ctx.roundRect) { + ctx.beginPath() + ctx.roundRect(x, y, w, h, r) + return + } + ctx.beginPath() + ctx.moveTo(x + r, y) + ctx.arcTo(x + w, y, x + w, y + h, r) + ctx.arcTo(x + w, y + h, x, y + h, r) + ctx.arcTo(x, y + h, x, y, r) + ctx.arcTo(x, y, x + w, y, r) + ctx.closePath() +} + +// randInt 取 [min, max] 闭区间随机整数 +export function randInt(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min +} + +// pick 从数组随机取一个元素 +export function pick(arr) { + return arr[Math.floor(Math.random() * arr.length)] +} diff --git a/src/games/index.js b/src/games/index.js new file mode 100644 index 0000000..3eea6a6 --- /dev/null +++ b/src/games/index.js @@ -0,0 +1,210 @@ +// 游戏注册表:游戏编码 → 组件加载器 + 支持的道具 + 操作说明 +// 编码必须与数据库 games.code 一致;supports 决定游玩页道具栏显示哪些道具 +export const gameRegistry = { + snake: { + loader: () => import('./components/SnakeGame.vue'), + supports: ['double_points', 'revive'], + controls: '方向键 / WASD 控制移动方向', + }, + tetris: { + loader: () => import('./components/TetrisGame.vue'), + supports: ['double_points', 'bomb'], + controls: '←→ 移动,↑ 旋转,↓ 软降,空格 硬降', + }, + g2048: { + loader: () => import('./components/Game2048.vue'), + supports: ['double_points'], + controls: '方向键滑动合并数字', + }, + minesweeper: { + loader: () => import('./components/MinesweeperGame.vue'), + supports: ['double_points', 'hint'], + controls: '左键翻格,右键插旗', + }, + breakout: { + loader: () => import('./components/BreakoutGame.vue'), + supports: ['double_points', 'revive'], + controls: '鼠标 / ←→ 移动挡板', + }, + pong: { + loader: () => import('./components/PongGame.vue'), + supports: ['double_points'], + controls: '鼠标 / ↑↓ 移动球拍,先得 11 分获胜', + }, + flappy: { + loader: () => import('./components/FlappyGame.vue'), + supports: ['double_points', 'revive', 'shield'], + controls: '点击 / 空格 扇动翅膀', + }, + memory: { + loader: () => import('./components/MemoryGame.vue'), + supports: ['double_points', 'hint'], + controls: '点击卡片翻开,找出所有相同的一对', + }, + whack: { + loader: () => import('./components/WhackGame.vue'), + supports: ['double_points', 'time_extend'], + controls: '点击冒头的地鼠,小心炸弹', + }, + gomoku: { + loader: () => import('./components/GomokuGame.vue'), + supports: ['double_points', 'hint'], + controls: '点击棋盘落子,先五连者胜', + }, + tictactoe: { + loader: () => import('./components/TicTacToeGame.vue'), + supports: ['double_points'], + controls: '点击格子落子,三连即胜', + }, + spaceshooter: { + loader: () => import('./components/SpaceShooterGame.vue'), + supports: ['double_points', 'revive', 'bomb', 'shield'], + controls: '←→ / 鼠标移动,自动开火', + }, + dino: { + loader: () => import('./components/DinoGame.vue'), + supports: ['double_points', 'revive', 'shield'], + controls: '空格 / 点击跳跃,↓ 下蹲', + }, + down100: { + loader: () => import('./components/Down100Game.vue'), + supports: ['double_points', 'revive'], + controls: '←→ 移动,踩稳平台往下走', + }, + sudoku: { + loader: () => import('./components/SudokuGame.vue'), + supports: ['double_points', 'hint'], + controls: '点击格子后输入 1-9,填满全盘', + }, + puzzle15: { + loader: () => import('./components/Puzzle15Game.vue'), + supports: ['double_points'], + controls: '点击与空位相邻的数字滑动,按 1-15 复原', + }, + simon: { + loader: () => import('./components/SimonGame.vue'), + supports: ['double_points'], + controls: '看完灯光序列后按相同顺序点击', + }, + bubble: { + loader: () => import('./components/BubbleGame.vue'), + supports: ['double_points', 'bomb'], + controls: '鼠标瞄准点击发射,三个同色即消除', + }, + fruit: { + loader: () => import('./components/FruitGame.vue'), + supports: ['double_points', 'revive', 'time_extend'], + controls: '按住鼠标滑动切开水果,别碰炸弹', + }, + maze: { + loader: () => import('./components/MazeGame.vue'), + supports: ['double_points', 'time_extend'], + controls: '方向键 / WASD 移动,走到绿色终点', + }, + connect4: { + loader: () => import('./components/Connect4Game.vue'), + supports: ['double_points'], + controls: '点击竖列投子,先四连者胜', + }, + lightsout: { + loader: () => import('./components/LightsOutGame.vue'), + supports: ['double_points', 'hint'], + controls: '点灯会翻转自身与上下左右,关掉全部灯', + }, + typing: { + loader: () => import('./components/TypingGame.vue'), + supports: ['double_points', 'time_extend'], + controls: '单词落地前用键盘输入完成', + }, + mathrush: { + loader: () => import('./components/MathRushGame.vue'), + supports: ['double_points', 'time_extend'], + controls: '心算后点击正确答案,连对有加成', + }, + colorreact: { + loader: () => import('./components/ColorReactGame.vue'), + supports: ['double_points', 'time_extend'], + controls: '文字含义与颜色一致按 ✔,否则按 ✘', + }, + aim: { + loader: () => import('./components/AimGame.vue'), + supports: ['double_points', 'time_extend'], + controls: '点击出现的靶心,越快越准分越高', + }, + pianotiles: { + loader: () => import('./components/PianoTilesGame.vue'), + supports: ['double_points', 'revive'], + controls: '点击黑块(或按 ASDF 对应四列)', + }, + jumpjump: { + loader: () => import('./components/JumpJumpGame.vue'), + supports: ['double_points', 'revive'], + controls: '按住蓄力,松开起跳到下一个平台', + }, + tank: { + loader: () => import('./components/TankGame.vue'), + supports: ['double_points', 'revive', 'bomb', 'shield'], + controls: 'WASD / 方向键移动,空格 / 点击开炮', + }, + asteroids: { + loader: () => import('./components/AsteroidsGame.vue'), + supports: ['double_points', 'revive', 'bomb', 'shield'], + controls: '←→ 旋转,↑ 喷射推进,空格开火', + }, + klotski: { + loader: () => import('./components/KlotskiGame.vue'), + supports: ['double_points'], + controls: '拖动/点击滑块移动,让曹操走到底部出口', + }, + mario: { + loader: () => import('./components/MarioGame.vue'), + supports: ['double_points', 'revive', 'shield'], + controls: '←→ 移动,空格/↑ 跳跃,↓ 可钻特定水管;复刻原版 1-1~1-4 四关:顶?块出金币蘑菇,吃蘑菇变大能顶碎砖,旗杆抓得越高分越多,1-4 城堡关躲火棍跳过库巴摸斧头断桥', + levels: { total: 4, names: ['世界 1-1', '世界 1-2', '世界 1-3', '世界 1-4'] }, + }, + stack3d: { + loader: () => import('./components/Stack3dGame.vue'), + supports: ['double_points'], + controls: '点击 / 空格 放下移动中的方块,对齐越准塔越高', + }, + ballmaze3d: { + loader: () => import('./components/BallMaze3dGame.vue'), + supports: ['double_points', 'time_extend'], + controls: '方向键倾斜迷宫控制小球滚动,滚到金色终点', + }, + racing3d: { + loader: () => import('./components/Racing3dGame.vue'), + supports: ['double_points', 'revive', 'shield'], + controls: '←→ 变道躲避车流,速度会越来越快', + }, + pvz: { + loader: () => import('./components/PvzGame.vue'), + supports: ['double_points', 'bomb', 'shield'], + controls: '点击卡片选植物后点击草坪种植,点击阳光收集,守住五条防线', + }, + linkgame: { + loader: () => import('./components/LinkGame.vue'), + supports: ['double_points', 'hint', 'time_extend', 'bomb'], + controls: '点击两个相同图案,连线拐弯不超过两次即可消除;共 12 关,每关图块移动规则不同,后 6 关限时更紧', + levels: { + total: 12, + names: ['经典', '下落', '上浮', '左靠', '右靠', '分裂', '经典⚡', '下落⚡', '上浮⚡', '左靠⚡', '右靠⚡', '分裂⚡'], + }, + }, + match3: { + loader: () => import('./components/Match3Game.vue'), + supports: ['double_points', 'hint', 'bomb'], + controls: '拖拽(或点击)相邻两个团子交换位置,凑成三连消除;共 12 关,步数内达成目标分即过关', + levels: { total: 12 }, + }, + starve: { + loader: () => import('./components/StarveGame.vue'), + supports: ['double_points'], + saveable: true, // 云存档:每天清晨自动存,死亡删档(永久死亡);仅单人模式 + wide: true, // 宽屏游戏:游玩页解除 1200px 限宽,画布自适应铺满可视区域 + coop: true, // 组队联机:进入时可选单人冒险 / 组队联机(2~4 人共享世界) + mods: true, // Mod 装载:开局可勾选内置 mod(难度类 mod 影响得分系数) + controls: + 'WASD 移动 · 空格采集 · F 攻击 · 1~9 使用物品栏 · C 合成 · M 地图 · Esc 关面板(鼠标点击同样可用);夜晚完全黑暗会遭查理袭击,务必备火;科学机器/炼金引擎解锁高级配方,烹饪锅可炖官方料理;历经秋冬春夏四季(冬季注意保暖、提防独眼巨鹿),猪人可喂肉结盟;每活一天 +100 分并自动云存档;支持 2~4 人组队联机与 30 款人物皮肤', + }, +} diff --git a/src/games/pixelAvatars.js b/src/games/pixelAvatars.js new file mode 100644 index 0000000..cd41d02 --- /dev/null +++ b/src/games/pixelAvatars.js @@ -0,0 +1,134 @@ +// 手绘像素头像:8 个形象 × 4 套配色 = 32 个可选 +// 存储格式:'px:NN'(NN = 01~32 的两位编号),旧 emoji 数据自动降级原样显示 +// 每个形象是 8×8 字符画:`.`透明 `1`主色 `2`副色 `3`描边深色 `4`白色 `5`黑色(眼/细节) + +// ---- 8 个基础形象(8×8 字符画) ---- +export const AVATAR_SHAPES = { + // 男孩:短发 + 笑脸 + boy: [ + '..3333..', + '.311113.', + '.311113.', + '.334433.', + '.351415.', + '.311113.', + '.322223.', + '..3333..', + ], + // 女孩:长发 + 刘海 + girl: [ + '.333333.', + '3111113.', + '3133313.', + '3344333.', + '3351533.', + '3311133.', + '3311133.', + '.33333..', + ], + // 猫耳:立耳 + 胡须脸 + cat: [ + '3.333.3.', + '3311133.', + '3111113.', + '.34443..', + '.35153..', + '.31113..', + '.32223..', + '..333...', + ], + // 兔耳:双长耳 + 圆脸 + bunny: [ + '3.333.3.', + '3.313.3.', + '3311133.', + '.311113.', + '.351513.', + '.311113.', + '.322223.', + '..3333..', + ], + // 机器人:方头 + 天线 + 单眼屏 + robot: [ + '...3....', + '..323...', + '.33333..', + '3144413.', + '3144413.', + '3155133.', + '3111113.', + '.33333..', + ], + // 外星人:圆头 + 大黑眼 + alien: [ + '..333...', + '.31113..', + '3111113.', + '3155513.', + '3155513.', + '3111113.', + '.31113..', + '..3.3...', + ], + // 幽灵:飘尾 + 圆眼 + ghost: [ + '..333...', + '.31113..', + '3111113.', + '3155153.', + '3111113.', + '3111113.', + '3111113.', + '3.3.33..', + ], + // 忍者:面罩 + 露眼 + 额带 + ninja: [ + '.33333..', + '3222223.', + '3333333.', + '3.55.53.', + '3333333.', + '3111113.', + '.31113..', + '..333...', + ], +} + +// ---- 4 套配色(主色/副色/描边) ---- +export const AVATAR_PALETTES = [ + { name: '活力橙', c1: '#ff9f43', c2: '#e05657', c3: '#5d3049' }, // 橙发红衣 + { name: '海洋蓝', c1: '#54a0ff', c2: '#48dbfb', c3: '#1e2749' }, // 蓝调 + { name: '青草绿', c1: '#1dd1a1', c2: '#feca57', c3: '#0c3b2e' }, // 绿调 + { name: '梦幻紫', c1: '#a29bfe', c2: '#fd79a8', c3: '#341f97' }, // 紫调 +] + +// ---- 32 个头像清单:编号 px:01 ~ px:32 ---- +// 顺序:8 个形象各配 4 套色(boy-橙、boy-蓝、boy-绿、boy-紫、cat-橙……) +const SHAPE_ORDER = ['boy', 'girl', 'cat', 'bunny', 'robot', 'alien', 'ghost', 'ninja'] + +export const AVATAR_LIST = [] +let n = 0 +for (const shape of SHAPE_ORDER) { + AVATAR_PALETTES.forEach((pal, pi) => { + n++ + AVATAR_LIST.push({ + code: 'px:' + String(n).padStart(2, '0'), // px:01 ~ px:32 + shape, + palette: pi, + name: `${AVATAR_SHAPES[shape].label || ''}${pal.name}`, + }) + }) +} + +// isPixelAvatar 判断头像值是否为像素头像编码 +export function isPixelAvatar(v) { + return typeof v === 'string' && /^px:\d{2}$/.test(v) +} + +// decodePixelAvatar 把 'px:05' 解成 { shape, palette };非法返回 null +export function decodePixelAvatar(code) { + if (!isPixelAvatar(code)) return null + const idx = parseInt(code.slice(3), 10) - 1 + if (idx < 0 || idx >= AVATAR_LIST.length) return null + return AVATAR_LIST[idx] +} diff --git a/src/games/starve/climate.js b/src/games/starve/climate.js new file mode 100644 index 0000000..0adb511 --- /dev/null +++ b/src/games/starve/climate.js @@ -0,0 +1,71 @@ +// 饥荒气候系统:季节推算、昼夜分段、环境温度、天气(雨/雪)、体温 +import { SEASONS, YEAR_DAYS, SEG_SPLIT, SEG, DAY_LEN } from './data.js' + +// 第 day 天所处季节:{code,name,idx,dayIn(季节内第几天),len,temp,tint} +export function seasonOf(day) { + let d = ((day - 1) % YEAR_DAYS) + for (let i = 0; i < SEASONS.length; i++) { + const s = SEASONS[i] + if (d < s.days) return { ...s, idx: i, dayIn: d + 1, len: s.days } + d -= s.days + } + return { ...SEASONS[0], idx: 0, dayIn: 1, len: SEASONS[0].days } +} + +// 当季昼/昏/夜的秒数分界:[黄昏开始, 夜晚开始] +export function daySplit(seasonCode) { + const [d, k] = SEG_SPLIT[seasonCode] || SEG_SPLIT.autumn + return [d * SEG, (d + k) * SEG] +} + +// 当前时相 +export function phaseOf(seasonCode, dayTime) { + const [duskAt, nightAt] = daySplit(seasonCode) + return dayTime < duskAt ? 'day' : dayTime < nightAt ? 'dusk' : 'night' +} + +// 环境温度:季节基准 + 时相修正 + 降雨降温 +export function ambientTemp(seasonCode, phase, wxType) { + const base = SEASONS.find((s) => s.code === seasonCode)?.temp ?? 16 + const mod = phase === 'day' ? 6 : phase === 'dusk' ? 0 : -8 + return base + mod - (wxType === 'rain' ? 5 : 0) +} + +// ---- 天气 ---- +// 每天清晨掷一次天气:返回 {type:'none'|'rain'|'snow', t:剩余秒} +export function rollWeather(seasonCode) { + if (seasonCode === 'winter') return { type: 'snow', t: DAY_LEN } // 冬季整天飘雪(视觉) + const p = seasonCode === 'spring' ? 0.45 : seasonCode === 'autumn' ? 0.25 : 0.05 + if (Math.random() < p) { + return { type: 'rain', t: 40 + Math.random() * 70 } + } + return { type: 'none', t: 0 } +} +// 每帧推进天气计时(雪不倒计时,随季节整天存在) +export function tickWeather(wx, dt) { + if (wx.type === 'rain') { + wx.t -= dt + if (wx.t <= 0) { + wx.type = 'none' + wx.t = 0 + return true // 雨停了 + } + } + return false +} + +// ---- 体温 ---- +// 体温向环境漂移;篝火/火把加热;保暖装备减缓失温;保暖石托底 +// p: {temp, ...} · heat: 附近火源强度 0~1 · insul: 保暖值(冬帽 120) +export function updateBodyTemp(p, ambient, heat, insul, dt) { + if (heat > 0) { + // 火边取暖:快速升向 50 + p.temp += (50 - p.temp) * dt * 0.5 * heat + } else { + // 失温速度受保暖值抑制(升温不受影响) + const cooling = ambient < p.temp + const k = 0.05 * (cooling ? 30 / (30 + insul) : 1) + p.temp += (ambient - p.temp) * dt * k + } + p.temp = Math.max(-20, Math.min(80, p.temp)) +} diff --git a/src/games/starve/data.js b/src/games/starve/data.js new file mode 100644 index 0000000..71869f7 --- /dev/null +++ b/src/games/starve/data.js @@ -0,0 +1,252 @@ +// 饥荒数据表:三围 / 季节昼夜 / 物品 / 食物 / 配方 / 烹饪锅 / 生物 / 掉落 +// 数值尽量对齐官方 Don't Starve(威尔逊基准 150/150/200、官方食物与伤害耐久表), +// 个别 Boss 血量按小游戏节奏缩放(注释标注) +export const CAP = { hp: 150, hunger: 150, san: 200 } + +// ---- 昼夜与季节:官方一天 16 段,这里每段 10 秒(一天 160 秒) ---- +export const SEG = 10 +export const DAY_LEN = 16 * SEG // 160 +// 季节顺序与时长(天):秋 20 → 冬 15 → 春 20 → 夏 15(循环,官方冬季首临第 21 天) +export const SEASONS = [ + { code: 'autumn', name: '秋', days: 20, temp: 16, tint: '#c98548' }, + { code: 'winter', name: '冬', days: 15, temp: -12, tint: '#9fc4dd' }, + { code: 'spring', name: '春', days: 20, temp: 14, tint: '#7fb069' }, + { code: 'summer', name: '夏', days: 15, temp: 34, tint: '#e0a63c' }, +] +export const YEAR_DAYS = SEASONS.reduce((n, s) => n + s.days, 0) +// 各季节 昼/昏/夜 段数(合计 16 段,对应官方季节日长变化) +export const SEG_SPLIT = { + autumn: [9, 4, 3], + winter: [5, 4, 7], + spring: [8, 5, 3], + summer: [11, 2, 3], +} + +// ---- 全局调参(官方数值折算:官方一天 480 秒,这里 160 秒 = 3 倍速流逝) ---- +export const TUNE = { + fistDmg: 10, // 徒手伤害(官方 10) + attackGap: 0.5, // 攻击间隔秒 + hungerRate: 75 / DAY_LEN, // 官方每天消耗 75 饥饿 + starveHp: 0.6, // 饥饿归零掉血/秒 + duskSan: 0.25, // 黄昏理智流失/秒 + nightDarkSan: 2.0, // 夜晚黑暗中理智流失/秒 + nightLitSan: 0.1, // 夜晚有光时的微量流失/秒 + rainSan: 0.12, // 雨中理智流失/秒 + flowerSan: 5, // 采花回理智(官方 +5) + shadowKillSan: 15, // 杀暗影回理智(官方爬行恐惧 +15) + charlieWarn: 1.5, // 黑暗中查理低语预警秒数 + charlieHit: 5, // 黑暗中首次遭袭秒数 + charlieRe: 3, // 后续连击间隔 + charlieDmg: 100, // 查理单次伤害(官方 100) + charlieSan: 20, // 查理袭击附带理智损失 + freezeHp: 1.25, // 体温 <=0 冰冻掉血/秒(官方 1.25) + freezeAt: 0, // 冰冻阈值 + packSlots: 8, // 背包附加格数 + invSlots: 15, // 主物品栏格数 + stackMax: 40, // 堆叠上限(官方 40) + roadSpeed: 1.3, // 道路提速(官方 1.3) + pigLoyal: 80, // 喂肉结盟时长(秒) + potCookTime: 15, // 烹饪锅炖煮秒数 + // 以下为 mod 可覆盖的调参缺省值 + gatherMul: 1, // 采集动作间隔倍率(<1 更快) + houndGapMul: 1, // 猎犬波间隔倍率(<1 更频繁) + houndExtra: 0, // 每波猎犬附加数量 +} + +// ===================================================================== +// 物品表 +// food: {hunger,hp,san} 官方食物数值 · cook: 烤制产物 · perish: 保鲜期(天,缺省不腐) +// equip: hand/body/head 装备位 · dmg: 武器伤害 · uses: 耐久次数 · burn: 计时耐久(秒) +// tool: 采集工具类型 · absorb: 护甲减伤比 · armor: 护甲耐久点 · insul: 保暖值 +// meatv/sweet/mm/veg: 烹饪锅食材属性 · heal: 使用回血 · stack: 堆叠上限(缺省 40,装备 1) +// ===================================================================== +export const ITEMS = { + // ---- 材料 ---- + grass: { name: '草' }, + twig: { name: '树枝' }, + log: { name: '木头' }, + rock: { name: '石头' }, + flint: { name: '燧石' }, + gold: { name: '金块' }, + silk: { name: '蛛丝' }, + spidergland: { name: '蜘蛛腺体' }, + rope: { name: '绳子' }, + petals: { name: '花瓣', food: { hunger: 1, hp: 0, san: 0 }, perish: 6 }, + reeds: { name: '芦苇' }, + ash: { name: '灰烬' }, + beefwool: { name: '牛毛' }, + pigskin: { name: '猪皮' }, + rot: { name: '腐烂物' }, + // ---- 食物(官方数值,raw 肉类 -10 理智 / 怪物肉 -15) ---- + berry: { name: '浆果', food: { hunger: 9.4, hp: 0, san: 0 }, cook: 'cookedberry', perish: 6, veg: 0.5 }, + cookedberry: { name: '烤浆果', food: { hunger: 12.5, hp: 1, san: 0 }, perish: 10, veg: 0.5 }, + morsel: { name: '小肉', food: { hunger: 12.5, hp: 0, san: -10 }, cook: 'cookedmorsel', perish: 6, meatv: 0.5 }, + cookedmorsel: { name: '烤小肉', food: { hunger: 18.75, hp: 1, san: 0 }, perish: 10, meatv: 0.5 }, + meat: { name: '大肉', food: { hunger: 25, hp: 1, san: -10 }, cook: 'cookedmeat', perish: 6, meatv: 1 }, + cookedmeat: { name: '熟肉', food: { hunger: 25, hp: 3, san: 0 }, perish: 10, meatv: 1 }, + monstermeat: { name: '怪物肉', food: { hunger: 18.75, hp: -20, san: -15 }, cook: 'cookedmonster', perish: 6, meatv: 1, mm: 1 }, + cookedmonster: { name: '烤怪物肉', food: { hunger: 18.75, hp: -3, san: -10 }, perish: 10, meatv: 1, mm: 1 }, + froglegs: { name: '蛙腿', food: { hunger: 12.5, hp: 0, san: -10 }, cook: 'cookedfroglegs', perish: 6, meatv: 0.5 }, + cookedfroglegs: { name: '烤蛙腿', food: { hunger: 12.5, hp: 1, san: 0 }, perish: 10, meatv: 0.5 }, + honey: { name: '蜂蜜', food: { hunger: 9.4, hp: 3, san: 0 }, perish: 40, sweet: 1 }, + eyeball: { name: '巨鹿眼球', food: { hunger: 75, hp: 40, san: -15 }, perish: 20, meatv: 1 }, + // ---- 烹饪锅料理 ---- + meatballs: { name: '肉丸', food: { hunger: 62.5, hp: 3, san: 5 }, perish: 10 }, + meatystew: { name: '炖肉汤', food: { hunger: 150, hp: 12, san: 5 }, perish: 10 }, + honeyham: { name: '蜜汁火腿', food: { hunger: 75, hp: 30, san: 5 }, perish: 15 }, + honeynuggets: { name: '蜜糖块', food: { hunger: 37.5, hp: 20, san: 5 }, perish: 15 }, + taffy: { name: '太妃糖', food: { hunger: 25, hp: -3, san: 15 }, perish: 15 }, + monsterlasagna: { name: '怪物千层饼', food: { hunger: 37.5, hp: -20, san: -20 }, perish: 10 }, + wetgoop: { name: '湿哒哒糊糊', food: { hunger: 0, hp: 0, san: 0 }, perish: 10 }, + // ---- 治疗 ---- + healingsalve: { name: '治疗药膏', heal: 20, stack: 40 }, + honeypoultice: { name: '蜂蜜药膏', heal: 30, stack: 40 }, + // ---- 手部装备(官方耐久:斧 100 / 镐 33 / 铲 25 / 矛 150 / 火把计时) ---- + axe: { name: '斧头', equip: 'hand', tool: 'axe', dmg: 27, uses: 100, stack: 1 }, + pickaxe: { name: '镐子', equip: 'hand', tool: 'pick', dmg: 27, uses: 33, stack: 1 }, + shovel: { name: '铲子', equip: 'hand', tool: 'dig', dmg: 17, uses: 25, stack: 1 }, + spear: { name: '长矛', equip: 'hand', dmg: 34, uses: 150, stack: 1 }, + tentaclespike: { name: '触手尖矛', equip: 'hand', dmg: 51, uses: 100, stack: 1 }, + torch: { name: '火把', equip: 'hand', dmg: 17, burn: 60, stack: 1 }, + // ---- 身体装备 ---- + backpack: { name: '背包', equip: 'body', pack: true, stack: 1 }, + logsuit: { name: '木甲', equip: 'body', absorb: 0.8, armor: 315, stack: 1 }, + // ---- 头部装备 ---- + garland: { name: '花环', equip: 'head', sanAura: 0.08, burn: 640, stack: 1 }, + footballhelmet: { name: '猪皮帽', equip: 'head', absorb: 0.8, armor: 315, stack: 1 }, + winterhat: { name: '冬帽', equip: 'head', insul: 120, stack: 1 }, + // ---- 特殊 ---- + trap: { name: '陷阱', place: true, uses: 8, stack: 10 }, + thermalstone: { name: '保暖石', thermal: true, stack: 1 }, +} +export const ITEM_CODES = Object.keys(ITEMS) +export const ITEM_IDX = Object.fromEntries(ITEM_CODES.map((c, i) => [c, i])) +export function stackMax(code) { + return ITEMS[code]?.stack || TUNE.stackMax +} + +// ===================================================================== +// 合成表:tech 0=徒手 1=需科学机器 2=需炼金引擎(原型机制:靠近机器合成一次即永久解锁) +// ===================================================================== +export const CRAFT_TABS = [ + { code: 'tools', name: '工具', icon: 'axe' }, + { code: 'light', name: '光源', icon: 'torch' }, + { code: 'surv', name: '生存', icon: 'trap' }, + { code: 'fight', name: '战斗', icon: 'spear' }, + { code: 'food', name: '食物', icon: 'crockpot' }, + { code: 'science', name: '科学', icon: 'sciencemachine' }, + { code: 'dress', name: '装扮', icon: 'garland' }, +] +export const RECIPES = [ + { code: 'axe', name: '斧头', tab: 'tools', tech: 0, cost: { twig: 1, flint: 1 }, desc: '砍树 · 耐久 100 次' }, + { code: 'pickaxe', name: '镐子', tab: 'tools', tech: 0, cost: { twig: 2, flint: 2 }, desc: '采矿 · 耐久 33 次' }, + { code: 'shovel', name: '铲子', tab: 'tools', tech: 1, cost: { twig: 2, flint: 2 }, desc: '挖树桩得木头 · 耐久 25 次' }, + { code: 'torch', name: '火把', tab: 'light', tech: 0, cost: { grass: 2, twig: 2 }, desc: '手持光源 · 燃烧 60 秒' }, + { code: 'campfire', name: '篝火', tab: 'light', tech: 0, cost: { grass: 3, log: 2 }, desc: '原地点燃,可添柴,烧尽留灰烬' }, + { code: 'firepit', name: '火堆', tab: 'light', tech: 1, cost: { log: 2, rock: 12 }, desc: '永久石圈火堆,熄灭后可再点燃' }, + { code: 'rope', name: '绳子', tab: 'surv', tech: 0, cost: { grass: 3 }, desc: '基础合成材料' }, + { code: 'trap', name: '陷阱', tab: 'surv', tech: 0, cost: { grass: 6, twig: 2 }, desc: '放置捕兔 · 耐久 8 次' }, + { code: 'backpack', name: '背包', tab: 'surv', tech: 1, cost: { grass: 4, twig: 4 }, desc: '身体装备 · 物品栏 +8 格' }, + { code: 'healingsalve', name: '治疗药膏', tab: 'surv', tech: 1, cost: { ash: 1, rock: 1, spidergland: 2 }, desc: '使用回复 20 生命' }, + { code: 'honeypoultice', name: '蜂蜜药膏', tab: 'surv', tech: 2, cost: { reeds: 2, honey: 1 }, desc: '使用回复 30 生命' }, + { code: 'thermalstone', name: '保暖石', tab: 'surv', tech: 2, cost: { rock: 10, flint: 3, rope: 1 }, desc: '篝火旁充热,随身抵御冰冻' }, + { code: 'spear', name: '长矛', tab: 'fight', tech: 1, cost: { twig: 2, rope: 1, flint: 1 }, desc: '伤害 34 · 耐久 150 次' }, + { code: 'logsuit', name: '木甲', tab: 'fight', tech: 1, cost: { log: 8, rope: 2 }, desc: '身体护甲 · 减伤 80%' }, + { code: 'footballhelmet', name: '猪皮帽', tab: 'fight', tech: 1, cost: { pigskin: 1, rope: 1 }, desc: '头部护甲 · 减伤 80%' }, + { code: 'crockpot', name: '烹饪锅', tab: 'food', tech: 1, cost: { rock: 6, twig: 6 }, desc: '放置后投入 4 份食材炖煮料理' }, + { code: 'sciencemachine', name: '科学机器', tab: 'science', tech: 0, cost: { gold: 1, log: 4, rock: 4 }, desc: '解锁一级科技(靠近合成)' }, + { code: 'alchemyengine', name: '炼金引擎', tab: 'science', tech: 1, cost: { gold: 6, log: 4, rock: 4 }, desc: '解锁二级科技(靠近合成)' }, + { code: 'garland', name: '花环', tab: 'dress', tech: 0, cost: { petals: 12 }, desc: '头戴缓慢回复理智,会枯萎' }, + { code: 'winterhat', name: '冬帽', tab: 'dress', tech: 2, cost: { silk: 4, beefwool: 2 }, desc: '保暖 · 冬季生存必备' }, +] + +// 烹饪锅配方:按优先级匹配(meatv 肉度 / sweet 甜度 / mm 怪物肉数) +const POT_RULES = [ + { dish: 'taffy', ok: (s) => s.sweet >= 3 }, + { dish: 'honeyham', ok: (s) => s.sweet >= 1 && s.meatv >= 1.5 }, + { dish: 'honeynuggets', ok: (s) => s.sweet >= 1 && s.meatv > 0 }, + { dish: 'meatystew', ok: (s) => s.meatv >= 3 }, + { dish: 'monsterlasagna', ok: (s) => s.mm >= 2 }, + { dish: 'meatballs', ok: (s) => s.meatv >= 0.5 }, +] +// 输入 4 个食材 code,返回料理 code(不满足任何配方 = 湿哒哒糊糊) +export function cookPot(codes) { + const s = { meatv: 0, sweet: 0, mm: 0, veg: 0 } + codes.forEach((c) => { + const it = ITEMS[c] || {} + s.meatv += it.meatv || 0 + s.sweet += it.sweet || 0 + s.mm += it.mm || 0 + s.veg += it.veg || 0 + }) + const hit = POT_RULES.find((r) => r.ok(s)) + return hit ? hit.dish : 'wetgoop' +} + +// ===================================================================== +// 生物表(官方血量伤害;树精/巨鹿按小游戏节奏取官方约 1/3) +// hostile 主动敌对 · neutral 被打才反击 · friendly 友方(猪人) +// ===================================================================== +export const MON = { + spider: { hp: 100, speed: 84, dmg: 20, score: 12, name: '蜘蛛', hostile: true }, + hound: { hp: 150, speed: 195, dmg: 20, score: 20, name: '猎犬', hostile: true }, + icehound: { hp: 150, speed: 195, dmg: 20, score: 25, name: '冰猎犬', hostile: true, freeze: true }, + shadow: { hp: 100, speed: 70, dmg: 20, score: 15, name: '暗影怪', hostile: true }, + treeguard: { hp: 700, speed: 52, dmg: 50, score: 150, name: '树精', hostile: true, boss: true }, + frog: { hp: 100, speed: 110, dmg: 10, score: 8, name: '青蛙', hostile: true }, + bee: { hp: 30, speed: 150, dmg: 10, score: 5, name: '杀人蜂', hostile: true }, + pig: { hp: 250, speed: 125, dmg: 33, score: 0, name: '猪人', friendly: true }, + beefalo: { hp: 500, speed: 100, dmg: 34, score: 10, name: '皮弗娄牛', neutral: true }, + deerclops: { hp: 1600, speed: 70, dmg: 75, score: 400, name: '独眼巨鹿', hostile: true, boss: true }, +} +// 击杀掉落(落地为拾取物):数组每项 [概率, code, 数量] +export const MOB_DROPS = { + spider: [[0.5, 'monstermeat', 1], [0.25, 'silk', 1], [0.25, 'spidergland', 1]], + hound: [[1, 'monstermeat', 1]], + icehound: [[1, 'monstermeat', 1]], + shadow: [], + treeguard: [[1, 'log', 6]], + frog: [[1, 'froglegs', 1]], + bee: [[0.5, 'honey', 1]], + pig: [[0.75, 'meat', 1], [0.25, 'pigskin', 1]], + beefalo: [[1, 'meat', 2], [1, 'beefwool', 1]], + deerclops: [[1, 'meat', 3], [1, 'eyeball', 1]], +} + +// ===================================================================== +// 资源实体模板 +// hits/tool: 采集次数与所需工具 · hp: 可攻击实体血量 · regrow: 再生秒(冬季暂停) +// vanish: 采尽消失 · mobile: 会移动(兔子) +// ===================================================================== +export const RES = { + tree: { hits: 3, tool: 'axe', drops: { log: 2 }, size: 42 }, + rock: { hits: 3, tool: 'pick', drops: { rock: 2, flint: 1 }, size: 28 }, + goldrock: { hits: 4, tool: 'pick', drops: { rock: 2, gold: 2 }, size: 30 }, + grass: { hits: 1, drops: { grass: 1 }, size: 18, regrow: 240 }, + sapling: { hits: 1, drops: { twig: 1 }, size: 20, regrow: 240 }, + berry: { hits: 1, drops: { berry: 1 }, size: 22, regrow: 400 }, + reeds: { hits: 1, drops: { reeds: 1 }, size: 20, regrow: 400 }, + flower: { hits: 1, drops: { petals: 1 }, size: 14, vanish: true }, + flint: { hits: 1, drops: { flint: 1 }, size: 14, vanish: true }, + rabbit: { hp: 25, drops: { morsel: 1 }, size: 16, mobile: true }, + rabbithole: { size: 18, solid: true }, + spidernest: { hp: 200, size: 36, nest: true }, + beehive: { hp: 100, size: 30, nest: true }, + pighouse: { size: 40, struct: true }, + tentacle: { hp: 250, size: 26, lurk: true }, + sciencemachine: { size: 34, struct: true, techTier: 1 }, + alchemyengine: { size: 38, struct: true, techTier: 2 }, + crockpot: { size: 26, struct: true, pot: true }, + trap: { size: 22, placeable: true }, + loot: { size: 12, loot: true }, +} +export const DECOR = { stump: { size: 20 }, rubble: { size: 16 } } + +// 实体/交互对象显示名(动作提示条) +export const NAMES = { + tree: '树', rock: '岩石', goldrock: '金矿石', grass: '草丛', sapling: '树枝苗', berry: '浆果丛', + reeds: '芦苇', flower: '野花', flint: '燧石', rabbit: '兔子', rabbithole: '兔子洞', + spidernest: '蜘蛛巢', beehive: '蜂巢', pighouse: '猪屋', tentacle: '触手', trap: '陷阱', + sciencemachine: '科学机器', alchemyengine: '炼金引擎', crockpot: '烹饪锅', loot: '掉落物', + stump: '树桩', rubble: '碎石', +} diff --git a/src/games/starve/mobs.js b/src/games/starve/mobs.js new file mode 100644 index 0000000..b1e09e4 --- /dev/null +++ b/src/games/starve/mobs.js @@ -0,0 +1,406 @@ +// 饥荒生物 AI:单个生物的逐帧行为(主机/单人权威模拟侧调用) +// g 为游戏上下文门面(由 StarveGame 构造并逐帧刷新): +// players/monsters/entities/fires 数组引用 · phase/season/time/day 时相 +// isLand(x,y) · litAt(x,y) · nearestPlayer(x,y) · damagePlayer(p,dmg,label) +// addEntity(type,x,y,extra) · removeEntity(ent) · addLoot(x,y,code,n) +// removeMob(m) · killMob(m,credit) · smashStructure(ent) · tip(t) · ptip(p,t) · shake(amp) +import { MON } from './data.js' + +// 带陆地碰撞的位移:先整体,再退化为单轴滑动 +export function moveMob(m, dx, dy, g) { + const nx = m.x + dx + const ny = m.y + dy + if (g.isLand(nx, ny)) { + m.x = nx + m.y = ny + } else if (g.isLand(nx, m.y)) { + m.x = nx + } else if (g.isLand(m.x, ny)) { + m.y = ny + } +} +function toward(m, tx, ty, speed, dt, g) { + const d = Math.hypot(tx - m.x, ty - m.y) || 1 + moveMob(m, ((tx - m.x) / d) * speed * dt, ((ty - m.y) / d) * speed * dt, g) + m.dir = tx > m.x ? 1 : -1 + m.moving = true +} + +// 敌对生物的受害者集合:活玩家 + 猪人(暗影怪只认玩家) +function nearestVictim(m, g, playersOnly = false) { + let best = null + let bd = Infinity + g.players.forEach((p) => { + if (p.dead) return + const d = Math.hypot(p.x - m.x, p.y - m.y) + if (d < bd) { bd = d; best = { kind: 'p', ref: p } } + }) + if (!playersOnly) { + g.monsters.forEach((q) => { + if (q.kind !== 'pig' || q === m) return + const d = Math.hypot(q.x - m.x, q.y - m.y) + if (d < bd) { bd = d; best = { kind: 'm', ref: q } } + }) + } + return best ? { v: best, d: bd } : null +} +// 对受害者造成伤害(玩家走减伤管线;猪人直接扣血并反击) +function biteVictim(m, victim, dmg, label, g) { + if (victim.kind === 'p') { + g.damagePlayer(victim.ref, dmg, label) + } else { + const pig = victim.ref + pig.hp -= dmg + pig.shake = 0.22 + pig.targetMob = m // 猪人被咬会还手 + if (pig.hp <= 0) g.killMob(pig, null) + } +} +// 通用追咬:追近 → 咬一口 → 弹开 +function chaseBite(m, conf, victim, dist, dt, g, label, onBite) { + const ref = victim.ref + if (dist > 26) { + toward(m, ref.x, ref.y, conf.speed, dt, g) + } else if (m.cd <= 0) { + m.cd = 1.15 + biteVictim(m, victim, conf.dmg, label, g) + onBite?.(ref, victim.kind) + moveMob(m, ((m.x - ref.x) / (dist || 1)) * 60, ((m.y - ref.y) / (dist || 1)) * 60, g) + } +} + +// ---- 单个生物逐帧更新 ---- +export function updateMob(m, dt, g) { + const conf = (g.mon && g.mon[m.kind]) || MON[m.kind] // g.mon = mod 运行时生物表(如噩梦模式倍率) + m.moving = false + if (m.shake > 0) m.shake -= dt + if (m.cd > 0) m.cd -= dt + if (m.kind === 'shadow') updShadow(m, conf, dt, g) + else if (m.kind === 'spider') updSpider(m, conf, dt, g) + else if (m.kind === 'hound' || m.kind === 'icehound') updHound(m, conf, dt, g) + else if (m.kind === 'treeguard') updTreeguard(m, conf, dt, g) + else if (m.kind === 'pig') updPig(m, conf, dt, g) + else if (m.kind === 'beefalo') updBeefalo(m, conf, dt, g) + else if (m.kind === 'bee') updBee(m, conf, dt, g) + else if (m.kind === 'frog') updFrog(m, conf, dt, g) + else if (m.kind === 'deerclops') updDeerclops(m, conf, dt, g) + m.x = Math.max(24, Math.min(g.worldW - 24, m.x)) + m.y = Math.max(24, Math.min(g.worldH - 24, m.y)) +} + +// 暗影怪:怕光;只袭击玩家;命中附带理智损失 +function updShadow(m, conf, dt, g) { + if (g.litAt(m.x, m.y)) { + const f = g.fires.find((f2) => f2.ttl > 0) || g.players.find((p) => !p.dead) || m + const d = Math.hypot(m.x - f.x, m.y - f.y) || 1 + moveMob(m, ((m.x - f.x) / d) * 70 * dt, ((m.y - f.y) / d) * 70 * dt, g) + return + } + const near = nearestVictim(m, g, true) + if (!near) return + chaseBite(m, conf, near.v, near.d, dt, g, '被暗影咬了', (ref, kind) => { + if (kind === 'p') { + ref.san = Math.max(0, ref.san - 5) + g.ptip(ref, `被暗影咬了 · 生命 -${conf.dmg} 理智 -5`) + } + }) +} + +// 蜘蛛:巢边领地怪,白天守巢、夜晚游荡范围扩大;入侵者靠近即追击 +function updSpider(m, conf, dt, g) { + const home = m.nest && g.entities.includes(m.nest) ? m.nest : null + const ax = home ? home.x : m.x + const ay = home ? home.y : m.y + const aggroR = g.phase === 'night' ? 280 : 190 + const near = nearestVictim(m, g) + if (near && Math.hypot(near.v.ref.x - ax, near.v.ref.y - ay) < aggroR) { + chaseBite(m, conf, near.v, near.d, dt, g, '被蜘蛛咬了', (ref, kind) => { + if (kind === 'p') g.ptip(ref, `被蜘蛛咬了 · 生命 -${conf.dmg}`) + }) + return + } + m.wanderT -= dt + if (m.wanderT <= 0) { + m.wanderT = 1.6 + Math.random() * 2 + const ang = Math.random() * Math.PI * 2 + const r = Math.random() * (g.phase === 'night' ? 180 : 90) + m.tx = ax + Math.cos(ang) * r + m.ty = ay + Math.sin(ang) * r + } + if (m.tx !== undefined) { + const d = Math.hypot(m.tx - m.x, m.ty - m.y) + if (d > 6) toward(m, m.tx, m.ty, conf.speed * 0.45, dt, g) + } +} + +// 猎犬/冰猎犬:无脑扑咬最近目标;冰犬命中附带冰冻减速与失温 +function updHound(m, conf, dt, g) { + const near = nearestVictim(m, g) + if (!near) return + chaseBite(m, conf, near.v, near.d, dt, g, '被猎犬咬了', (ref, kind) => { + if (kind === 'p') { + if (conf.freeze) { + ref.freezeT = 2.2 + ref.temp = Math.max(-20, ref.temp - 12) + g.ptip(ref, `冰猎犬咬了你 · 生命 -${conf.dmg} · 寒气刺骨`) + } else { + g.ptip(ref, `猎犬咬了你一口 · 生命 -${conf.dmg}`) + } + } + }) +} + +// 树精:慢速逼近 → 抬臂前摇 → 拍击(可走位躲开)→ 硬直;远离太久重新扎根 +function updTreeguard(m, conf, dt, g) { + m.t -= dt + const near = nearestVictim(m, g, true) + if (!near) return + const tp = near.v.ref + const dist = near.d + if (dist > 340) { + m.awayT += dt + if (m.awayT > 6) { + g.removeMob(m) + g.addEntity('tree', m.x, m.y) + g.tip('树精平息了,重新扎根成一棵树') + return + } + } else { + m.awayT = 0 + } + if (m.state === 'chase') { + if (dist > 54) toward(m, tp.x, tp.y, conf.speed, dt, g) + else { + m.state = 'windup' + m.t = 0.55 + } + } else if (m.state === 'windup') { + if (m.t <= 0) { + m.state = 'smash' + m.t = 0.35 + g.shake(6) + g.players.forEach((p) => { + if (!p.dead && Math.hypot(p.x - m.x, p.y - m.y) < 70) { + g.damagePlayer(p, conf.dmg, '被树精拍中了!') + } + }) + } + } else if (m.state === 'smash') { + if (m.t <= 0) { m.state = 'recover'; m.t = 0.8 } + } else if (m.state === 'recover') { + if (m.t <= 0) m.state = 'chase' + } +} + +// 猪人:白天绕屋游荡;被喂肉结盟跟随并助战;被打会记仇反击;夜晚回屋 +function updPig(m, conf, dt, g) { + if (m.loyalT > 0) m.loyalT -= dt + if (m.angryT > 0) m.angryT -= dt + // 1) 反击惹它的玩家 + if (m.angryT > 0 && m.angryAt && !m.angryAt.dead) { + const d = Math.hypot(m.angryAt.x - m.x, m.angryAt.y - m.y) + chaseBite(m, conf, { kind: 'p', ref: m.angryAt }, d, dt, g, '被猪人揍了', (ref) => { + g.ptip(ref, `猪人还手了 · 生命 -${conf.dmg}`) + }) + return + } + // 2) 与敌对生物作战(结盟时主动搜索主人周边的敌人) + if (m.targetMob && (!g.monsters.includes(m.targetMob) || m.targetMob.hp <= 0)) m.targetMob = null + if (!m.targetMob && m.loyalT > 0) { + const owner = m.owner && !m.owner.dead ? m.owner : null + let best = null + let bd = 180 + g.monsters.forEach((q) => { + if (!MON[q.kind]?.hostile) return + const d = Math.min( + Math.hypot(q.x - m.x, q.y - m.y), + owner ? Math.hypot(q.x - owner.x, q.y - owner.y) : Infinity + ) + if (d < bd) { bd = d; best = q } + }) + m.targetMob = best + } + if (m.targetMob) { + const q = m.targetMob + const d = Math.hypot(q.x - m.x, q.y - m.y) + if (d > 30) { + toward(m, q.x, q.y, conf.speed, dt, g) + } else if (m.cd <= 0) { + m.cd = 1 + q.hp -= conf.dmg + q.shake = 0.22 + if (q.hp <= 0) { + g.killMob(q, null) + m.targetMob = null + } + } + return + } + // 3) 结盟跟随主人 + if (m.loyalT > 0 && m.owner && !m.owner.dead) { + const d = Math.hypot(m.owner.x - m.x, m.owner.y - m.y) + if (d > 70) toward(m, m.owner.x, m.owner.y, conf.speed, dt, g) + return + } + // 4) 夜晚回屋 / 白天绕屋游荡 + const hx = m.home?.x ?? m.x + const hy = m.home?.y ?? m.y + if (g.phase === 'night') { + if (Math.hypot(hx - m.x, hy - m.y) > 30) toward(m, hx, hy, conf.speed, dt, g) + return + } + m.wanderT -= dt + if (m.wanderT <= 0) { + m.wanderT = 2 + Math.random() * 3 + const ang = Math.random() * Math.PI * 2 + m.tx = hx + Math.cos(ang) * Math.random() * 120 + m.ty = hy + Math.sin(ang) * Math.random() * 120 + } + if (m.tx !== undefined && Math.hypot(m.tx - m.x, m.ty - m.y) > 8) { + toward(m, m.tx, m.ty, conf.speed * 0.4, dt, g) + } +} + +// 皮弗娄牛:herd 锚点吃草游荡;被打后全群围攻一段时间 +function updBeefalo(m, conf, dt, g) { + if (m.angryT > 0) { + m.angryT -= dt + if (m.angryAt && !m.angryAt.dead) { + const d = Math.hypot(m.angryAt.x - m.x, m.angryAt.y - m.y) + chaseBite(m, conf, { kind: 'p', ref: m.angryAt }, d, dt, g, '被牛顶飞了', (ref) => { + g.ptip(ref, `皮弗娄牛暴怒冲撞 · 生命 -${conf.dmg}`) + }) + return + } + m.angryT = 0 + } + const hx = m.home?.x ?? m.x + const hy = m.home?.y ?? m.y + m.wanderT -= dt + if (m.wanderT <= 0) { + m.wanderT = 3 + Math.random() * 4 + const ang = Math.random() * Math.PI * 2 + m.tx = hx + Math.cos(ang) * Math.random() * 160 + m.ty = hy + Math.sin(ang) * Math.random() * 160 + } + if (m.tx !== undefined && Math.hypot(m.tx - m.x, m.ty - m.y) > 10) { + toward(m, m.tx, m.ty, conf.speed * 0.35, dt, g) + } +} + +// 杀人蜂:短命防卫者,追蜇惹事者后飞散 +function updBee(m, conf, dt, g) { + m.life -= dt + if (m.life <= 0) { + g.removeMob(m) + return + } + let victim = null + if (m.angryAt && !m.angryAt.dead) victim = { kind: 'p', ref: m.angryAt } + else { + const near = nearestVictim(m, g, true) + if (near && near.d < 220) victim = near.v + } + if (!victim) { + // 无目标:飘回巢并提前散伙 + m.life -= dt * 2 + return + } + const d = Math.hypot(victim.ref.x - m.x, victim.ref.y - m.y) + if (d > 22) { + toward(m, victim.ref.x, victim.ref.y, conf.speed, dt, g) + } else if (m.cd <= 0) { + m.cd = 1.4 + biteVictim(m, victim, conf.dmg, '被蜂蜇了', (ref, kind) => { + if (kind === 'p') g.ptip(ref, `被蜂蜇了 · 生命 -${conf.dmg}`) + }) + } +} + +// 青蛙:近处玩家跳咬,其余原地蹦跶 +function updFrog(m, conf, dt, g) { + const near = nearestVictim(m, g, true) + if (near && near.d < 130) { + chaseBite(m, conf, near.v, near.d, dt, g, '被青蛙弹了', (ref, kind) => { + if (kind === 'p') g.ptip(ref, `青蛙用舌头抽了你 · 生命 -${conf.dmg}`) + }) + return + } + m.wanderT -= dt + if (m.wanderT <= 0) { + m.wanderT = 1.5 + Math.random() * 2.5 + const ang = Math.random() * Math.PI * 2 + m.tx = m.x + Math.cos(ang) * 50 + m.ty = m.y + Math.sin(ang) * 50 + } + if (m.tx !== undefined && Math.hypot(m.tx - m.x, m.ty - m.y) > 6) { + toward(m, m.tx, m.ty, conf.speed * 0.5, dt, g) + } +} + +// 独眼巨鹿:优先拆毁玩家附近建筑,其次追人;范围砸击;冬季结束自行离去 +function updDeerclops(m, conf, dt, g) { + m.t -= dt + if (g.season !== 'winter') { + g.removeMob(m) + g.tip('冬天过去了,独眼巨鹿转身离开……') + return + } + // 目标:先找 500 内建筑,否则最近的玩家 + if (!m.targetEnt || !g.entities.includes(m.targetEnt)) { + m.targetEnt = null + let bd = 500 + g.entities.forEach((e) => { + if (!(e.struct || e.pot)) return + const d = Math.hypot(e.x - m.x, e.y - m.y) + if (d < bd) { bd = d; m.targetEnt = e } + }) + } + const near = nearestVictim(m, g) + let tx + let ty + let dist + if (m.targetEnt) { + tx = m.targetEnt.x + ty = m.targetEnt.y + dist = Math.hypot(tx - m.x, ty - m.y) + } else if (near) { + tx = near.v.ref.x + ty = near.v.ref.y + dist = near.d + } else return + if (m.state === 'chase' || !m.state) { + if (dist > 80) toward(m, tx, ty, conf.speed, dt, g) + else { + m.state = 'windup' + m.t = 0.8 + } + } else if (m.state === 'windup') { + if (m.t <= 0) { + m.state = 'smash' + m.t = 0.4 + g.shake(10) + // 范围砸击:玩家/猪人受伤,建筑直接摧毁 + g.players.forEach((p) => { + if (!p.dead && Math.hypot(p.x - m.x, p.y - m.y) < 95) { + g.damagePlayer(p, conf.dmg, '被独眼巨鹿砸中了!') + } + }) + g.monsters.forEach((q) => { + if (q.kind === 'pig' && Math.hypot(q.x - m.x, q.y - m.y) < 95) { + q.hp -= conf.dmg + if (q.hp <= 0) g.killMob(q, null) + } + }) + g.entities.slice().forEach((e) => { + if ((e.struct || e.pot) && Math.hypot(e.x - m.x, e.y - m.y) < 95) { + g.smashStructure(e) + } + }) + } + } else if (m.state === 'smash') { + if (m.t <= 0) { m.state = 'recover'; m.t = 1.1 } + } else if (m.state === 'recover') { + if (m.t <= 0) m.state = 'chase' + } +} diff --git a/src/games/starve/mods/chest.js b/src/games/starve/mods/chest.js new file mode 100644 index 0000000..4a3648c --- /dev/null +++ b/src/games/starve/mods/chest.js @@ -0,0 +1,135 @@ +// 宝藏:世界各处散落 6~10 个宝箱,开箱按战利品表获得物资(每箱一次) +const TAU = Math.PI * 2 + +// 战利品表:[权重, code, 最少, 最多] +const LOOT = [ + [3, 'gold', 2, 4], + [3, 'flint', 2, 4], + [2, 'rope', 1, 2], + [2, 'log', 3, 6], + [2, 'honey', 1, 2], + [2, 'silk', 2, 3], + [1, 'healingsalve', 1, 1], + [1, 'trap', 1, 2], + [1, 'meatballs', 1, 1], +] + +function rollLoot(g) { + const total = LOOT.reduce((n, l) => n + l[0], 0) + let r = Math.random() * total + for (const [w, code, lo, hi] of LOOT) { + r -= w + if (r <= 0) return [code, g.rand(lo, hi)] + } + return ['gold', 1] +} + +export default { + id: 'chest', + name: '宝藏', + desc: '世界散落宝箱,探索开出物资', + factor: 1, + tag: 'content', + ents: { + chest: { + name: '宝箱', + conf: { size: 26 }, + init(e) { + if (!e.state) e.state = 'closed' + }, + interact(p, e, g) { + if (e.state === 'closed') { + e.state = 'open' + g.markDirty(e) + g.swing(p, 'chop') + const rolls = g.rand(2, 3) + const got = [] + for (let i = 0; i < rolls; i++) { + const [code, n] = rollLoot(g) + g.addItem(p, code, n) + got.push(`${g.items[code]?.name || code} ×${n}`) + } + g.addScore(25, p) + g.ptip(p, `打开宝箱:${got.join('、')}`) + g.shake(1.5) + return + } + g.ptip(p, '空空如也的旧箱子') + }, + draw(ctx, e, env) { + const open = e.state === 'open' + // 阴影 + ctx.fillStyle = 'rgba(0,0,0,0.25)' + ctx.beginPath() + ctx.ellipse(0, 4, 18, 6, 0, 0, TAU) + ctx.fill() + // 箱体 + ctx.fillStyle = '#7a5230' + ctx.strokeStyle = '#1d1409' + ctx.lineWidth = 2 + ctx.beginPath() + ctx.roundRect(-16, -12, 32, 18, 3) + ctx.fill() + ctx.stroke() + // 盖子:闭合盖在上方 / 打开向后仰 + ctx.fillStyle = open ? '#5d3f24' : '#8a5f38' + ctx.beginPath() + if (open) { + ctx.roundRect(-16, -26, 32, 9, 3) + ctx.fill() + ctx.stroke() + // 内部 + ctx.fillStyle = '#2a1c10' + ctx.fillRect(-13, -12, 26, 6) + } else { + ctx.roundRect(-17, -18, 34, 9, 3) + ctx.fill() + ctx.stroke() + } + // 金属包边 + 锁扣 + ctx.strokeStyle = '#d8b04a' + ctx.lineWidth = 2 + ctx.beginPath() + ctx.moveTo(-10, open ? -12 : -18) + ctx.lineTo(-10, 6) + ctx.moveTo(10, open ? -12 : -18) + ctx.lineTo(10, 6) + ctx.stroke() + if (!open) { + ctx.fillStyle = '#d8b04a' + ctx.strokeStyle = '#1d1409' + ctx.lineWidth = 1.4 + ctx.beginPath() + ctx.roundRect(-3, -12, 6, 7, 1.5) + ctx.fill() + ctx.stroke() + // 微光 + const tw = 0.5 + Math.sin((env?.time || 0) * 3 + e.id) * 0.5 + ctx.fillStyle = `rgba(255,230,150,${0.25 + tw * 0.3})` + ctx.beginPath() + ctx.arc(6, -15, 1.6 + tw, 0, TAU) + ctx.fill() + } + }, + }, + }, + hooks: { + onWorldGen(g) { + const cx = g.worldW / 2 + const cy = g.worldH / 2 + const spots = [] + const target = 6 + g.rand(0, 4) + for (let tries = 0; tries < 600 && spots.length < target; tries++) { + const x = 60 + Math.random() * (g.worldW - 120) + const y = 60 + Math.random() * (g.worldH - 120) + if (!g.isLand(x, y)) continue + if (Math.hypot(x - cx, y - cy) < 380) continue // 离出生点远一些才有探索感 + if (spots.some((s) => Math.hypot(s.x - x, s.y - y) < 280)) continue + if (g.entities.some((e) => !e.deco && Math.hypot(e.x - x, e.y - y) < 40)) continue + spots.push({ x, y }) + } + spots.forEach((s) => g.addEntity('chest', s.x, s.y)) + if (spots.length) g.tip(`听说这片荒野埋着 ${spots.length} 个宝箱……`) + }, + }, +} diff --git a/src/games/starve/mods/chester.js b/src/games/starve/mods/chester.js new file mode 100644 index 0000000..0cac050 --- /dev/null +++ b/src/games/starve/mods/chester.js @@ -0,0 +1,217 @@ +// 切斯特:荒野某处藏着眼骨,拾起后切斯特现身跟随; +// 它会自动吞掉身边的掉落物(9 格随身仓),点击它把肚子里的东西全吐出来 +const TAU = Math.PI * 2 +const STORE_SLOTS = 9 + +function ownerOf(g) { + return g.players.find((p) => !p.dead && g.countItem(p, 'eyebone') > 0) || null +} + +function stackInto(store, code, n, cap) { + let left = n + // 先叠进已有同类 + for (const s of store) { + if (!s || s.code !== code || s.n >= cap) continue + const add = Math.min(cap - s.n, left) + s.n += add + left -= add + if (!left) return 0 + } + // 再开新格 + for (let i = 0; i < store.length && left; i++) { + if (store[i]) continue + const add = Math.min(cap, left) + store[i] = { code, n: add } + left -= add + } + return left +} + +export default { + id: 'chester', + name: '切斯特', + desc: '寻获眼骨召来跟班箱子,自动吞附近掉落物', + factor: 1, + tag: 'content', + items: { + eyebone: { name: '眼骨', stack: 1 }, + }, + icons: { + eyebone: (c) => { + c.strokeStyle = '#ece7db' + c.lineWidth = 4 + c.beginPath(); c.moveTo(-4, 12); c.lineTo(4, -4); c.stroke() + c.fillStyle = '#ece7db' + c.strokeStyle = '#1d1409' + c.lineWidth = 1.6 + ;[[-7, 12], [-1, 14]].forEach(([x, y]) => { + c.beginPath(); c.arc(x, y, 3.4, 0, TAU); c.fill(); c.stroke() + }) + c.beginPath(); c.arc(5, -8, 7, 0, TAU); c.fill(); c.stroke() + c.fillStyle = '#fff' + c.beginPath(); c.arc(5, -8, 4.6, 0, TAU); c.fill() + c.fillStyle = '#4a90c2' + c.beginPath(); c.arc(5, -8, 2.6, 0, TAU); c.fill() + c.fillStyle = '#1d1409' + c.beginPath(); c.arc(5, -8, 1.2, 0, TAU); c.fill() + }, + }, + mobs: { + chester: { + conf: { hp: 300, speed: 160, dmg: 0, score: 0, name: '切斯特' }, + friendly: true, + drops: [], + update(m, dt, g) { + if (!m.store) m.store = Array(STORE_SLOTS).fill(null) + // 优先扑向附近掉落物吞掉(眼骨除外;刚吐出的东西有几秒宽限期不回吞) + const loot = m.cd <= 0 + ? g.entities.find((e) => e.loot && e.code !== 'eyebone' && !(e.shyUntil > g.time) && Math.hypot(e.x - m.x, e.y - m.y) < 150) + : null + if (loot) { + const d = Math.hypot(loot.x - m.x, loot.y - m.y) + if (d > 26) { + g.moveToward(m, loot.x, loot.y, 175, dt) + } else { + const cap = g.items[loot.code]?.stack || g.tune.stackMax + const left = stackInto(m.store, loot.code, loot.n, cap) + if (left < loot.n) { + m.cd = 0.35 + m.gulpT = 0.5 + if (left > 0) loot.n = left + else g.removeEntity(loot) + } else { + m.cd = 2 // 仓满吞不下,歇会儿别原地抽搐 + } + } + if (m.gulpT > 0) m.gulpT -= dt + return + } + const owner = ownerOf(g) + if (owner) { + const d = Math.hypot(owner.x - m.x, owner.y - m.y) + if (d > 64) { + const sprint = d > 220 ? 1.5 : 1 + g.moveToward(m, owner.x - owner.dir * 40, owner.y + 16, 160 * sprint, dt) + } + } + if (m.gulpT > 0) m.gulpT -= dt + }, + interact(p, m, g) { + const has = (m.store || []).filter(Boolean) + if (!has.length) { + g.ptip(p, '切斯特打了个哈欠(肚子是空的)') + return + } + has.forEach((s) => { + const l = g.dropLoot(m.x + g.rand(-24, 24), m.y + g.rand(10, 30), s.code, s.n) + if (l) l.shyUntil = g.time + 6 + }) + m.store = Array(STORE_SLOTS).fill(null) + m.gulpT = 0.5 + g.ptip(p, `切斯特吐出了 ${has.length} 样东西`) + }, + draw(ctx, m, time) { + const hop = m.moving ? Math.abs(Math.sin(time * 9 + m.bob)) * 6 : Math.sin(time * 2.2 + m.bob) * 1.5 + const squash = m.gulpT > 0 ? 1.15 : 1 + ctx.save() + ctx.translate(0, -hop) + ctx.scale(m.dir, 1) + // 阴影 + ctx.restore() + ctx.fillStyle = 'rgba(0,0,0,0.25)' + ctx.beginPath() + ctx.ellipse(0, 2, 16, 5, 0, 0, TAU) + ctx.fill() + ctx.save() + ctx.translate(0, -hop) + ctx.scale(m.dir * squash, 1 / squash) + // 小短腿 + ctx.strokeStyle = '#4a2c1a' + ctx.lineWidth = 4 + const leg = m.moving ? Math.sin(time * 12) * 3 : 0 + ctx.beginPath(); ctx.moveTo(-8, -4); ctx.lineTo(-8 + leg, 2); ctx.moveTo(8, -4); ctx.lineTo(8 - leg, 2); ctx.stroke() + // 圆滚滚的身体 + ctx.fillStyle = '#a85a28' + ctx.strokeStyle = '#1d1409' + ctx.lineWidth = 2 + ctx.beginPath() + ctx.ellipse(0, -16, 15, 14, 0, 0, TAU) + ctx.fill() + ctx.stroke() + // 大嘴(张合) + const mouth = m.gulpT > 0 ? 5 : 2 + Math.sin(time * 3 + m.bob) * 1 + ctx.fillStyle = '#5d2a12' + ctx.beginPath() + ctx.ellipse(4, -12, 9, mouth, 0.15, 0, TAU) + ctx.fill() + // 牙 + ctx.fillStyle = '#ece7db' + ctx.beginPath(); ctx.moveTo(-2, -14); ctx.lineTo(0, -10); ctx.lineTo(2, -14); ctx.closePath(); ctx.fill() + ctx.beginPath(); ctx.moveTo(7, -14); ctx.lineTo(9, -10); ctx.lineTo(11, -14); ctx.closePath(); ctx.fill() + // 眼睛 + ctx.fillStyle = '#fff' + ctx.strokeStyle = '#1d1409' + ctx.lineWidth = 1.4 + ctx.beginPath(); ctx.arc(-2, -24, 3.4, 0, TAU); ctx.fill(); ctx.stroke() + ctx.beginPath(); ctx.arc(7, -23, 3, 0, TAU); ctx.fill(); ctx.stroke() + ctx.fillStyle = '#1d1409' + ctx.beginPath(); ctx.arc(-1.4, -23.6, 1.4, 0, TAU); ctx.fill() + ctx.beginPath(); ctx.arc(7.6, -22.6, 1.2, 0, TAU); ctx.fill() + // 角耳朵 + ctx.strokeStyle = '#7a3f1c' + ctx.lineWidth = 3.4 + ctx.beginPath(); ctx.moveTo(-10, -27); ctx.quadraticCurveTo(-14, -34, -10, -38); ctx.stroke() + ctx.beginPath(); ctx.moveTo(4, -28); ctx.quadraticCurveTo(8, -35, 4, -39); ctx.stroke() + ctx.restore() + }, + }, + }, + hooks: { + onWorldGen(g) { + // 眼骨落在远离出生点的荒野 + const cx = g.worldW / 2 + const cy = g.worldH / 2 + let best = null + for (let tries = 0; tries < 300; tries++) { + const x = 80 + Math.random() * (g.worldW - 160) + const y = 80 + Math.random() * (g.worldH - 160) + if (!g.isLand(x, y)) continue + const d = Math.hypot(x - cx, y - cy) + if (d > 480 && d < 1100) { best = { x, y }; break } + } + if (!best) best = g.findLand(cx + 500, cy, 0, 200) + g.addEntity('loot', best.x, best.y, { code: 'eyebone', n: 1, fresh: 1 }) + g.tip('传说荒野某处躺着一根奇怪的眼骨……') + }, + onTick(g) { + const owner = ownerOf(g) + if (!owner) return + if (g.monsters.some((m) => m.kind === 'chester')) return + const spot = g.findLand(owner.x + 60, owner.y + 40, 20, 60) + g.spawnMob('chester', spot.x, spot.y) + g.tip('切斯特闻着眼骨的味道蹦了出来!(它会吞附近的掉落物,点它取回)') + }, + onUseItem(g, p, s) { + if (s.code !== 'eyebone') return + const ch = g.monsters.find((m) => m.kind === 'chester') + if (ch) { + const n = (ch.store || []).filter(Boolean).length + g.ptip(p, n ? `切斯特肚子里有 ${n} 样东西(点击它取回)` : '切斯特跟在你身边(会自动吞掉落物)') + } else { + g.ptip(p, '眼骨微微颤动着……切斯特正在赶来') + } + return true + }, + // 切斯特与仓内物品随存档走(生物本身不入档) + save(g) { + const ch = g.monsters.find((m) => m.kind === 'chester') + if (!ch) return null + return { x: Math.round(ch.x), y: Math.round(ch.y), store: (ch.store || []).map((s) => (s ? [s.code, s.n] : 0)) } + }, + load(g, d) { + const m = g.spawnMob('chester', d.x, d.y) + m.store = (d.store || []).map((v) => (v ? { code: v[0], n: v[1] } : null)) + while (m.store.length < STORE_SLOTS) m.store.push(null) + }, + }, +} diff --git a/src/games/starve/mods/farm.js b/src/games/starve/mods/farm.js new file mode 100644 index 0000000..c1a3af9 --- /dev/null +++ b/src/games/starve/mods/farm.js @@ -0,0 +1,246 @@ +// 农场:锄头开垦农田,播下种子经三阶段生长收获作物 +// - 锄头(0 级科技合成,耐久 20):点击使用在面前开垦一块农田 +// - 种子:采草 15% 掉胡萝卜种子,采浆果丛 25% 掉浆果种子;点击种子播到身边空农田 +// - 生长:2 天(320 秒)三阶段,冬季暂停;成熟后点击收获 2~3 个作物 +const TAU = Math.PI * 2 +const GROW_TOTAL = 320 // 两天 + +function stageOf(t) { + if (t >= GROW_TOTAL) return 'ready' + if (t >= 214) return 'grow2' + if (t >= 107) return 'grow1' + return 'grow0' +} + +function plant(p, plot, seedCode, g) { + plot.mx = { seed: seedCode === 'berryseeds' ? 'berry' : 'carrot', t: 0 } + plot.state = 'grow0' + g.markDirty(plot) + g.swing(p, 'dig') + g.ptip(p, `种下了${seedCode === 'berryseeds' ? '浆果' : '胡萝卜'}种子`) +} + +export default { + id: 'farm', + name: '农场', + desc: '锄头开垦农田,种植浆果与胡萝卜', + factor: 1, + tag: 'content', + items: { + hoe: { name: '锄头', stack: 1, uses: 20 }, + carrotseeds: { name: '胡萝卜种子', stack: 40 }, + berryseeds: { name: '浆果种子', stack: 40 }, + carrot: { name: '胡萝卜', food: { hunger: 12.5, hp: 1, san: 0 }, cook: 'cookedcarrot', perish: 10, veg: 1 }, + cookedcarrot: { name: '烤胡萝卜', food: { hunger: 12.5, hp: 3, san: 0 }, perish: 10, veg: 1 }, + }, + recipes: [ + { code: 'hoe', name: '锄头', tab: 'tools', tech: 0, cost: { twig: 2, flint: 2 }, desc: '开垦农田 · 耐久 20 次' }, + ], + icons: { + hoe: (c) => { + c.strokeStyle = '#7a5b33' + c.lineWidth = 3 + c.beginPath(); c.moveTo(-8, 12); c.lineTo(6, -10); c.stroke() + c.fillStyle = '#8d9097' + c.strokeStyle = '#1d1409' + c.lineWidth = 1.6 + c.beginPath(); c.moveTo(4, -12); c.lineTo(12, -8); c.lineTo(10, -2); c.lineTo(3, -6); c.closePath() + c.fill(); c.stroke() + }, + carrotseeds: (c) => { + c.fillStyle = '#c9a36b' + c.strokeStyle = '#1d1409' + c.lineWidth = 1.4 + ;[[-5, 2], [3, -3], [6, 6], [-3, 8], [0, -7]].forEach(([x, y]) => { + c.beginPath(); c.ellipse(x, y, 3, 4.2, 0.4, 0, TAU); c.fill(); c.stroke() + }) + c.fillStyle = '#e08a3c' + c.beginPath(); c.ellipse(0, -1, 2, 2.6, 0, 0, TAU); c.fill() + }, + berryseeds: (c) => { + c.fillStyle = '#b6d29b' + c.strokeStyle = '#1d1409' + c.lineWidth = 1.4 + ;[[-5, 2], [3, -3], [6, 6], [-3, 8], [0, -7]].forEach(([x, y]) => { + c.beginPath(); c.ellipse(x, y, 3, 4.2, 0.4, 0, TAU); c.fill(); c.stroke() + }) + c.fillStyle = '#c0455a' + c.beginPath(); c.ellipse(0, -1, 2.4, 2.4, 0, 0, TAU); c.fill() + }, + carrot: (c) => { + c.fillStyle = '#e08a3c' + c.strokeStyle = '#1d1409' + c.lineWidth = 1.8 + c.beginPath(); c.moveTo(-6, -4); c.quadraticCurveTo(0, -9, 6, -4); c.lineTo(1, 12); c.closePath() + c.fill(); c.stroke() + c.strokeStyle = '#8f9a3e' + c.lineWidth = 2.4 + c.beginPath(); c.moveTo(-3, -6); c.lineTo(-6, -13); c.moveTo(0, -7); c.lineTo(0, -14); c.moveTo(3, -6); c.lineTo(6, -13); c.stroke() + }, + cookedcarrot: (c) => { + c.fillStyle = '#c0703a' + c.strokeStyle = '#1d1409' + c.lineWidth = 1.8 + c.beginPath(); c.moveTo(-6, -3); c.quadraticCurveTo(0, -8, 6, -3); c.lineTo(1, 12); c.closePath() + c.fill(); c.stroke() + c.strokeStyle = '#5a4a3a' + c.lineWidth = 1.2 + c.beginPath(); c.moveTo(-3, 0); c.lineTo(2, 1); c.moveTo(-2, 5); c.lineTo(2, 6); c.stroke() + }, + }, + ents: { + farmplot: { + name: '农田', + conf: { size: 26 }, + init(e) { + if (!e.state) e.state = 'empty' + }, + update(e, dt, g) { + if (!e.mx || e.state === 'empty' || e.state === 'ready') return + if (g.season === 'winter') return // 冬季停止生长(与官方一致) + e.mx.t += dt + const st = stageOf(e.mx.t) + if (st !== e.state) { + e.state = st + g.markDirty(e) + if (st === 'ready') g.tip('农田里的作物成熟了!') + } + }, + interact(p, e, g) { + if (e.state === 'ready') { + const crop = e.mx?.seed || 'carrot' + const n = g.rand(2, 3) + g.addItem(p, crop, n) + g.addScore(12, p) + g.swing(p, 'chop') + g.ptip(p, `收获${crop === 'berry' ? '浆果' : '胡萝卜'} ×${n}`) + e.state = 'empty' + e.mx = null + g.markDirty(e) + return + } + if (e.state === 'empty') { + // 身上有种子就直接播种 + const seed = ['berryseeds', 'carrotseeds'].find((code) => g.countItem(p, code) > 0) + if (seed) { + g.payCost(p, { [seed]: 1 }) + plant(p, e, seed, g) + } else { + g.ptip(p, '空农田:需要种子(采草丛/浆果丛概率获得)') + } + return + } + g.ptip(p, '作物生长中……(冬季会暂停)') + }, + draw(ctx, e) { + // 垄起的土地 + ctx.fillStyle = '#6b4a2e' + ctx.strokeStyle = '#1d1409' + ctx.lineWidth = 2 + ctx.beginPath() + ctx.ellipse(0, 0, 24, 14, 0, 0, TAU) + ctx.fill() + ctx.stroke() + ctx.strokeStyle = '#553a24' + ctx.lineWidth = 2 + ;[-8, 0, 8].forEach((y) => { + ctx.beginPath() + ctx.moveTo(-16 + Math.abs(y), y * 0.6) + ctx.lineTo(16 - Math.abs(y), y * 0.6) + ctx.stroke() + }) + const st = e.state + if (st === 'empty' || !st) return + const berry = e.mx?.seed === 'berry' + if (st === 'grow0') { + ctx.strokeStyle = '#7fb069' + ctx.lineWidth = 2 + ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(0, -7); ctx.stroke() + ctx.fillStyle = '#8f9a3e' + ctx.beginPath(); ctx.ellipse(2, -7, 3, 1.8, -0.4, 0, TAU); ctx.fill() + } else if (st === 'grow1') { + ctx.strokeStyle = '#6f9a52' + ctx.lineWidth = 2.4 + ;[-6, 0, 6].forEach((x) => { + ctx.beginPath(); ctx.moveTo(x, 2); ctx.lineTo(x * 1.3, -11); ctx.stroke() + }) + } else if (st === 'grow2') { + ctx.strokeStyle = '#5d8a45' + ctx.lineWidth = 2.8 + ;[-8, 0, 8].forEach((x) => { + ctx.beginPath(); ctx.moveTo(x, 2); ctx.quadraticCurveTo(x * 1.4, -10, x * 1.1, -18); ctx.stroke() + }) + ctx.fillStyle = berry ? '#7a9a52' : '#8f9a3e' + ctx.beginPath(); ctx.ellipse(0, -16, 9, 5, 0, 0, TAU); ctx.fill() + } else { + // ready:结出作物 + ctx.strokeStyle = '#5d8a45' + ctx.lineWidth = 2.8 + ;[-9, 0, 9].forEach((x) => { + ctx.beginPath(); ctx.moveTo(x, 2); ctx.quadraticCurveTo(x * 1.4, -12, x * 1.1, -20); ctx.stroke() + }) + if (berry) { + ctx.fillStyle = '#c0455a' + ctx.strokeStyle = '#1d1409' + ctx.lineWidth = 1.4 + ;[[-8, -14], [0, -19], [8, -13]].forEach(([x, y]) => { + ctx.beginPath(); ctx.arc(x, y, 3.6, 0, TAU); ctx.fill(); ctx.stroke() + }) + } else { + ctx.fillStyle = '#e08a3c' + ctx.strokeStyle = '#1d1409' + ctx.lineWidth = 1.4 + ;[[-8, -4], [0, -5], [8, -4]].forEach(([x, y]) => { + ctx.beginPath() + ctx.moveTo(x - 3, y); ctx.quadraticCurveTo(x, y - 3, x + 3, y); ctx.lineTo(x, y + 7); ctx.closePath() + ctx.fill(); ctx.stroke() + }) + } + } + }, + }, + }, + hooks: { + onUseItem(g, p, s, area, i) { + if (s.code === 'hoe') { + const spot = g.findLand(p.x + p.dir * 46, p.y + 8, 0, 24) + const crowded = g.entities.some((e) => !e.deco && !e.loot && Math.hypot(e.x - spot.x, e.y - spot.y) < 34) + if (crowded) { + g.ptip(p, '这里太挤了,换块空地开垦') + return true + } + g.addEntity('farmplot', spot.x, spot.y) + g.swing(p, 'dig') + s.dur = (s.dur ?? g.items.hoe.uses) - 1 + if (s.dur <= 0) { + g.consumeAt(p, area, i) + g.ptip(p, '开垦出农田 · 锄头用坏了') + } else { + g.ptip(p, '开垦出一块农田(种子点击播种)') + } + g.invalidate() + return true + } + if (s.code === 'berryseeds' || s.code === 'carrotseeds') { + const plot = g.entities.find((e) => e.type === 'farmplot' && e.state === 'empty' && Math.hypot(e.x - p.x, e.y - p.y) < 80) + if (!plot) { + g.ptip(p, '需要站在空农田旁播种(先用锄头开垦)') + return true + } + const code = s.code + g.consumeAt(p, area, i) + plant(p, plot, code, g) + return true + } + }, + onGather(g, p, e) { + if (e.type === 'grass' && Math.random() < 0.15) { + g.addItem(p, 'carrotseeds', 1) + g.ptip(p, '草丛里翻出了胡萝卜种子!') + } else if (e.type === 'berry' && Math.random() < 0.25) { + g.addItem(p, 'berryseeds', 1) + g.ptip(p, '摘到了一把浆果种子') + } + }, + }, +} diff --git a/src/games/starve/mods/index.js b/src/games/starve/mods/index.js new file mode 100644 index 0000000..a03623e --- /dev/null +++ b/src/games/starve/mods/index.js @@ -0,0 +1,11 @@ +// 内置 mod 列表:声明顺序即合并顺序(主客机保持一致,勿随意调换) +import qol from './qol.js' +import farm from './farm.js' +import chest from './chest.js' +import chester from './chester.js' +import revive from './revive.js' +import peaceful from './peaceful.js' +import nightmare from './nightmare.js' +import magic from './magic.js' + +export const MODS = [qol, farm, chest, chester, revive, peaceful, nightmare, magic] diff --git a/src/games/starve/mods/magic.js b/src/games/starve/mods/magic.js new file mode 100644 index 0000000..ce87d3c --- /dev/null +++ b/src/games/starve/mods/magic.js @@ -0,0 +1,214 @@ +// 魔法武器:火魔杖(对最近敌人范围灼烧)+ 回旋镖(直线飞行命中后折返) +const TAU = Math.PI * 2 +const STAFF_RANGE = 280 // 施法距离 +const STAFF_AOE = 85 // 爆燃半径 +const STAFF_DMG = 35 +const BOOM_SPEED = 340 +const BOOM_RANGE = 270 +const BOOM_DMG = 20 + +function hostileNear(g, x, y, r) { + let best = null + let bd = r + g.monsters.forEach((m) => { + if (!g.mon[m.kind]?.hostile) return + const d = Math.hypot(m.x - x, m.y - y) + if (d < bd) { bd = d; best = m } + }) + return best +} + +function useDur(g, p, s, area, i, brokeText) { + s.dur = (s.dur ?? g.items[s.code].uses) - 1 + if (s.dur <= 0) { + g.consumeAt(p, area, i) + g.ptip(p, brokeText) + } + g.invalidate() +} + +export default { + id: 'magic', + name: '魔法武器', + desc: '火魔杖范围灼烧 · 回旋镖飞掷折返', + factor: 1, + tag: 'content', + items: { + firestaff: { name: '火魔杖', stack: 1, uses: 20 }, + boomerang: { name: '回旋镖', stack: 1, uses: 10 }, + }, + recipes: [ + { code: 'firestaff', name: '火魔杖', tab: 'fight', tech: 2, cost: { twig: 2, gold: 2, rope: 1 }, desc: `灼烧最近敌人及周围(${STAFF_DMG} 伤害)· 20 次` }, + { code: 'boomerang', name: '回旋镖', tab: 'fight', tech: 1, cost: { twig: 2, flint: 1, rope: 1 }, desc: `飞掷命中沿途敌人后折返(${BOOM_DMG} 伤害)· 10 次` }, + ], + icons: { + firestaff: (c) => { + c.strokeStyle = '#7a5b33' + c.lineWidth = 3 + c.beginPath(); c.moveTo(-9, 13); c.lineTo(5, -6); c.stroke() + c.fillStyle = '#e0662e' + c.strokeStyle = '#1d1409' + c.lineWidth = 1.6 + c.beginPath() + c.moveTo(7, -14); c.quadraticCurveTo(12, -8, 7, -2); c.quadraticCurveTo(4, -5, 3, -8); c.quadraticCurveTo(4, -12, 7, -14) + c.closePath(); c.fill(); c.stroke() + c.fillStyle = '#f0c04a' + c.beginPath(); c.ellipse(6.6, -8, 2, 3.4, 0.3, 0, TAU); c.fill() + }, + boomerang: (c) => { + c.fillStyle = '#c9a36b' + c.strokeStyle = '#1d1409' + c.lineWidth = 1.8 + c.beginPath() + c.moveTo(-10, -10) + c.quadraticCurveTo(2, -12, 10, -4) + c.quadraticCurveTo(12, 2, 8, 10) + c.quadraticCurveTo(4, 0, -4, -3) + c.quadraticCurveTo(-9, -5, -10, -10) + c.closePath() + c.fill(); c.stroke() + c.strokeStyle = '#8a6238' + c.lineWidth = 1.2 + c.beginPath(); c.moveTo(-6, -8); c.quadraticCurveTo(3, -8, 8, 6); c.stroke() + }, + }, + ents: { + // 爆燃特效(短寿命,纯视觉) + flamefx: { + conf: { size: 10 }, + init(e) { + e.deco = true + if (!e.mx) e.mx = { t: 0 } + }, + update(e, dt, g) { + e.mx.t += dt + if (e.mx.t > 0.55) g.removeEntity(e) + }, + draw(ctx, e) { + const k = Math.min(1, (e.mx?.t || 0) / 0.55) + const r = STAFF_AOE * (0.35 + k * 0.65) + ctx.globalAlpha = 1 - k + ctx.strokeStyle = '#f0862e' + ctx.lineWidth = 5 - k * 3 + ctx.beginPath(); ctx.arc(0, 0, r, 0, TAU); ctx.stroke() + ctx.fillStyle = 'rgba(240,140,50,0.35)' + ctx.beginPath(); ctx.arc(0, 0, r * 0.75, 0, TAU); ctx.fill() + for (let i = 0; i < 6; i++) { + const a = (i / 6) * TAU + k * 2 + ctx.fillStyle = i % 2 ? '#f0c04a' : '#e0662e' + ctx.beginPath() + ctx.arc(Math.cos(a) * r * 0.8, Math.sin(a) * r * 0.55 - k * 14, 4 * (1 - k) + 1, 0, TAU) + ctx.fill() + } + ctx.globalAlpha = 1 + }, + }, + // 回旋镖投射物:直线飞出 → 命中或到程折返 → 回到投掷者手里 + boomerang: { + conf: { size: 10 }, + init(e) { + e.deco = true + }, + update(e, dt, g) { + const m = e.mx + if (!m) { g.removeEntity(e); return } + const owner = g.players.find((p) => p.seat === m.seat) + if (!owner || owner.dead) { g.removeEntity(e); return } + if (m.phase === 'out') { + e.x += m.vx * dt + e.y += m.vy * dt + m.dist += Math.hypot(m.vx, m.vy) * dt + const hit = g.monsters.find((mo) => !g.mon[mo.kind]?.friendly && mo.kind !== 'chester' && Math.hypot(mo.x - e.x, mo.y - e.y) < 26) + if (hit) { + g.damageMob(hit, BOOM_DMG, owner) + m.phase = 'back' + } else if (m.dist >= BOOM_RANGE) { + m.phase = 'back' + } + } else { + const d = Math.hypot(owner.x - e.x, owner.y - e.y) || 1 + e.x += ((owner.x - e.x) / d) * BOOM_SPEED * dt + e.y += ((owner.y - e.y) / d) * BOOM_SPEED * dt + if (d < 26) { + g.removeEntity(e) + g.ptip(owner, '接住了回旋镖') + return + } + } + // 位置镜像进 mx,随增量同步给客机(快照的实体更新行不含坐标) + m.px = Math.round(e.x) + m.py = Math.round(e.y) + g.markDirty(e) + }, + draw(ctx, e, env) { + // 客机上 e.x/e.y 停留在投出点,用 mx 里的实时坐标纠偏 + const dx = e.mx?.px != null ? e.mx.px - e.x : 0 + const dy = e.mx?.py != null ? e.mx.py - e.y : 0 + ctx.save() + ctx.translate(dx, dy - 14) + ctx.rotate(((env?.time || 0) * 14) % TAU) + ctx.fillStyle = '#c9a36b' + ctx.strokeStyle = '#1d1409' + ctx.lineWidth = 1.6 + ctx.beginPath() + ctx.moveTo(-9, -9) + ctx.quadraticCurveTo(2, -10, 9, -3) + ctx.quadraticCurveTo(10, 2, 7, 9) + ctx.quadraticCurveTo(3, 0, -4, -3) + ctx.closePath() + ctx.fill() + ctx.stroke() + ctx.restore() + ctx.fillStyle = 'rgba(0,0,0,0.2)' + ctx.beginPath() + ctx.ellipse(dx, dy + 2, 7, 2.6, 0, 0, TAU) + ctx.fill() + }, + }, + }, + hooks: { + onUseItem(g, p, s, area, i) { + if (s.code === 'firestaff') { + const target = hostileNear(g, p.x, p.y, STAFF_RANGE) + if (!target) { + g.ptip(p, '附近没有可灼烧的敌人') + return true + } + g.swing(p, 'attack') + p.dir = target.x >= p.x ? 1 : -1 + g.addEntity('flamefx', target.x, target.y) + let hits = 0 + g.monsters.slice().forEach((m) => { + if (!g.mon[m.kind]?.hostile) return + if (Math.hypot(m.x - target.x, m.y - target.y) > STAFF_AOE) return + hits++ + g.damageMob(m, STAFF_DMG, p) + }) + g.shake(2) + g.ptip(p, `烈焰爆燃!灼烧了 ${hits} 个敌人`) + useDur(g, p, s, area, i, '火魔杖燃尽碎裂了') + return true + } + if (s.code === 'boomerang') { + // 朝最近敌人方向掷出;没有敌人就朝面向 + if (g.entities.some((e) => e.type === 'boomerang' && e.mx?.seat === p.seat)) { + g.ptip(p, '回旋镖还在飞行中……') + return true + } + const target = hostileNear(g, p.x, p.y, BOOM_RANGE + 60) + let vx = p.dir * BOOM_SPEED + let vy = 0 + if (target) { + const d = Math.hypot(target.x - p.x, target.y - p.y) || 1 + vx = ((target.x - p.x) / d) * BOOM_SPEED + vy = ((target.y - p.y) / d) * BOOM_SPEED + p.dir = target.x >= p.x ? 1 : -1 + } + g.swing(p, 'attack') + g.addEntity('boomerang', p.x + p.dir * 14, p.y - 4, { mx: { vx, vy, phase: 'out', dist: 0, seat: p.seat, px: Math.round(p.x), py: Math.round(p.y) } }) + useDur(g, p, s, area, i, '回旋镖散架了') + return true + } + }, + }, +} diff --git a/src/games/starve/mods/nightmare.js b/src/games/starve/mods/nightmare.js new file mode 100644 index 0000000..449c6bc --- /dev/null +++ b/src/games/starve/mods/nightmare.js @@ -0,0 +1,10 @@ +// 噩梦模式:全体怪物血量伤害 x1.5,猎犬波周期缩短且每波 +1 +export default { + id: 'nightmare', + name: '噩梦模式', + desc: '怪物血量伤害 ×1.5 · 猎犬波更频繁更多', + factor: 1.3, + tag: 'rule', + monScale: { hp: 1.5, dmg: 1.5 }, + tune: { houndGapMul: 0.7, houndExtra: 1 }, +} diff --git a/src/games/starve/mods/peaceful.js b/src/games/starve/mods/peaceful.js new file mode 100644 index 0000000..f712b1a --- /dev/null +++ b/src/games/starve/mods/peaceful.js @@ -0,0 +1,13 @@ +// 和平模式:取消猎犬波与独眼巨鹿的袭击排程(其余生物照常) +export default { + id: 'peaceful', + name: '和平模式', + desc: '不再有猎犬波与独眼巨鹿', + factor: 0.7, + tag: 'rule', + hooks: { + onSpawnWave(g, kind) { + if (kind === 'hound' || kind === 'deerclops') return false + }, + }, +} diff --git a/src/games/starve/mods/qol.js b/src/games/starve/mods/qol.js new file mode 100644 index 0000000..c65fa81 --- /dev/null +++ b/src/games/starve/mods/qol.js @@ -0,0 +1,10 @@ +// 便利包:全图显示(无战争迷雾)+ 快速采集 + 堆叠上限 99 +export default { + id: 'qol', + name: '便利包', + desc: '全图显示 · 采集加速 · 堆叠上限 99', + factor: 0.7, + tag: 'rule', + tune: { stackMax: 99, gatherMul: 0.5 }, + flags: { noFog: true }, +} diff --git a/src/games/starve/mods/registry.js b/src/games/starve/mods/registry.js new file mode 100644 index 0000000..9b7c65c --- /dev/null +++ b/src/games/starve/mods/registry.js @@ -0,0 +1,99 @@ +// Mod 注册表:与游戏核心解耦的加载器 +// - mod 定义 = 纯数据(物品/配方/实体/生物/图标/调参)+ 钩子函数集 +// - activateMods() 把选中的 mod 合并成运行时注册表(不改动 data.js 基表) +// - 核心只依赖本文件与 index.js;mod 只依赖传入的 g(游戏受控上下文),互不感知实现 +import { + ITEMS, RECIPES, TUNE, MON, MOB_DROPS, RES, NAMES, +} from '../data.js' +import { MODS } from './index.js' + +// 给开局 UI 用的元数据(不激活任何逻辑) +export function modMeta() { + return MODS.map(({ id, name, desc, factor, tag }) => ({ id, name, desc, factor: factor || 1, tag: tag || 'content' })) +} + +// 激活一组 mod:返回运行时注册表(rt)。合并顺序 = MODS 声明顺序,保证主客机一致 +export function activateMods(ids = []) { + const act = MODS.filter((m) => ids.includes(m.id)) + const rt = { + ids: act.map((m) => m.id), + metas: act.map(({ id, name, desc, factor }) => ({ id, name, desc, factor: factor || 1 })), + items: { ...ITEMS }, + recipes: [...RECIPES], + tune: { ...TUNE }, + mon: { ...MON }, + mobDrops: { ...MOB_DROPS }, + res: { ...RES }, + names: { ...NAMES }, + entKinds: {}, // type -> { conf, name, init, update, interact, draw } + mobKinds: {}, // kind -> { conf, friendly, update, interact, draw, drops } + iconPainters: {}, // code -> (ctx) => void(40x40 画布,原点居中) + hooks: {}, // name -> [{ id, fn }] + flags: {}, + scoreFactor: 1, + } + let factor = 1 + act.forEach((mod) => { + Object.entries(mod.items || {}).forEach(([code, def]) => { + rt.items[code] = def + rt.names[code] = def.name + }) + ;(mod.recipes || []).forEach((r) => rt.recipes.push(r)) + Object.assign(rt.tune, mod.tune || {}) + Object.assign(rt.flags, mod.flags || {}) + Object.entries(mod.ents || {}).forEach(([type, def]) => { + rt.entKinds[type] = def + rt.res[type] = def.conf || { size: 20 } + if (def.name) rt.names[type] = def.name + }) + Object.entries(mod.mobs || {}).forEach(([kind, def]) => { + rt.mobKinds[kind] = def + rt.mon[kind] = def.conf + rt.mobDrops[kind] = def.drops || [] + }) + Object.assign(rt.iconPainters, mod.icons || {}) + Object.entries(mod.hooks || {}).forEach(([name, fn]) => { + ;(rt.hooks[name] = rt.hooks[name] || []).push({ id: mod.id, fn }) + }) + // 怪物表整体缩放(如噩梦模式):对当前已合并的所有生物生效 + if (mod.monScale) { + Object.keys(rt.mon).forEach((k) => { + const c = rt.mon[k] + rt.mon[k] = { ...c, hp: Math.round(c.hp * (mod.monScale.hp || 1)), dmg: Math.round(c.dmg * (mod.monScale.dmg || 1)) } + }) + } + factor *= mod.factor || 1 + }) + rt.scoreFactor = Math.round(Math.max(0.5, Math.min(1.5, factor)) * 100) / 100 + rt.itemCodes = Object.keys(rt.items) + rt.itemIdx = Object.fromEntries(rt.itemCodes.map((c, i) => [c, i])) + return rt +} + +// 通知型钩子:全部调用 +export function callHook(rt, name, ...args) { + ;(rt.hooks[name] || []).forEach((h) => h.fn(...args)) +} +// 处理型钩子:任一返回 true 即视为已处理(短路) +export function callHandled(rt, name, ...args) { + return (rt.hooks[name] || []).some((h) => h.fn(...args) === true) +} +// 否决型钩子:任一返回 false 即取消 +export function callAllows(rt, name, ...args) { + return !(rt.hooks[name] || []).some((h) => h.fn(...args) === false) +} +// 存档收集/恢复:每个 mod 的自定义状态挂在 modData[modId] +export function collectModSaves(rt, g) { + const out = {} + ;(rt.hooks.save || []).forEach((h) => { + const d = h.fn(g) + if (d != null) out[h.id] = d + }) + return Object.keys(out).length ? out : undefined +} +export function applyModSaves(rt, g, data) { + if (!data) return + ;(rt.hooks.load || []).forEach((h) => { + if (data[h.id] != null) h.fn(g, data[h.id]) + }) +} diff --git a/src/games/starve/mods/revive.js b/src/games/starve/mods/revive.js new file mode 100644 index 0000000..bb7d342 --- /dev/null +++ b/src/games/starve/mods/revive.js @@ -0,0 +1,77 @@ +// 复活雕像:二级科技建造石像;任何玩家死亡时自动消耗一座,原地复活(半血半饥) +// 单人模式下等于多一条命,保住"死亡删档"前的存档 +const TAU = Math.PI * 2 + +export default { + id: 'revive', + name: '复活雕像', + desc: '建造石像,死亡时消耗一座原地复活', + factor: 1, + tag: 'content', + recipes: [ + { code: 'revivestatue', name: '复活雕像', tab: 'science', tech: 2, cost: { rock: 12, gold: 4 }, build: true, desc: '死亡时自动消耗,原地复活(半血)' }, + ], + icons: { + revivestatue: (c) => { + c.fillStyle = '#8d9097' + c.strokeStyle = '#1d1409' + c.lineWidth = 1.8 + c.beginPath(); c.roundRect(-11, 8, 22, 6, 2); c.fill(); c.stroke() + c.beginPath(); c.moveTo(-7, 8); c.lineTo(-5, -4); c.lineTo(5, -4); c.lineTo(7, 8); c.closePath(); c.fill(); c.stroke() + c.beginPath(); c.arc(0, -9, 5.5, 0, TAU); c.fill(); c.stroke() + c.fillStyle = '#b0b4bc' + c.beginPath(); c.arc(-2, -10.5, 1.6, 0, TAU); c.fill() + }, + }, + ents: { + revivestatue: { + name: '复活雕像', + conf: { size: 34, struct: true }, + interact(p, e, g) { + g.ptip(p, '守望雕像静静伫立(死亡时将消耗它复活)') + }, + draw(ctx, e, env) { + // 底座 + ctx.fillStyle = 'rgba(0,0,0,0.25)' + ctx.beginPath(); ctx.ellipse(0, 4, 20, 7, 0, 0, TAU); ctx.fill() + ctx.fillStyle = '#7d8087' + ctx.strokeStyle = '#1d1409' + ctx.lineWidth = 2 + ctx.beginPath(); ctx.roundRect(-17, -4, 34, 10, 3); ctx.fill(); ctx.stroke() + // 身躯(斗篷状) + ctx.fillStyle = '#8d9097' + ctx.beginPath() + ctx.moveTo(-11, -4); ctx.quadraticCurveTo(-13, -26, -6, -32) + ctx.lineTo(6, -32); ctx.quadraticCurveTo(13, -26, 11, -4) + ctx.closePath() + ctx.fill(); ctx.stroke() + // 头 + ctx.beginPath(); ctx.arc(0, -38, 8, 0, TAU); ctx.fill(); ctx.stroke() + // 高光 + ctx.fillStyle = '#b0b4bc' + ctx.beginPath(); ctx.arc(-3, -40, 2.4, 0, TAU); ctx.fill() + ctx.beginPath(); ctx.ellipse(-7, -18, 2.4, 8, 0.2, 0, TAU); ctx.fill() + // 怀中微光(生命之火) + const tw = 0.5 + Math.sin((env?.time || 0) * 2.4 + e.id) * 0.5 + ctx.fillStyle = `rgba(120,220,160,${0.35 + tw * 0.35})` + ctx.beginPath(); ctx.arc(0, -16, 3.4 + tw * 1.2, 0, TAU); ctx.fill() + }, + }, + }, + hooks: { + onPlayerDeath(g, p) { + const statue = g.entities.find((e) => e.type === 'revivestatue') + if (!statue) return + g.removeEntity(statue) + p.hp = 75 + p.hunger = 75 + p.san = 100 + p.temp = Math.max(p.temp, 12) + p.darkT = 0 + g.ptip(p, '远处的复活雕像碎裂了——你重获新生!') + g.tip(`${p.name || '玩家'}被复活雕像救了回来`) + g.shake(5) + return true + }, + }, +} diff --git a/src/games/starve/paint.js b/src/games/starve/paint.js new file mode 100644 index 0000000..814414b --- /dev/null +++ b/src/games/starve/paint.js @@ -0,0 +1,1418 @@ +// 饥荒手绘美术:物品图标离屏渲染 + 静态实体 sprite 缓存 + 生物程序动画画师 +// 约定:实体/生物画师在"已 translate 到脚底原点"的 ctx 上绘制;静态实体先离屏缓存再 blit, +// 摇摆动画通过整体旋转缓存图完成(性能远优于逐帧矢量重绘);生物保持逐帧矢量(动作丰富) +import { ITEMS, ITEM_CODES } from './data.js' + +const TAU = Math.PI * 2 + +function fs(c, fill, stroke = '#1d1409', lw = 2) { + if (fill) { c.fillStyle = fill; c.fill() } + if (stroke) { c.strokeStyle = stroke; c.lineWidth = lw; c.stroke() } +} +function shadow(c, x, y, rx, ry = null) { + c.fillStyle = 'rgba(20,16,8,0.3)' + c.beginPath() + c.ellipse(x, y, rx, ry ?? rx * 0.35, 0, 0, TAU) + c.fill() +} + +// ===================================================================== +// 物品图标(40px 画布 / 28px 内容,以 (0,0) 为中心) +// ===================================================================== +function iconMeat(c, col, small = false, smoke = false) { + const s = small ? 0.72 : 1 + c.strokeStyle = '#e8e2d4' + c.lineWidth = 3.4 * s + c.beginPath(); c.moveTo(-9 * s, 9 * s); c.lineTo(8 * s, -8 * s); c.stroke() + c.fillStyle = '#e8e2d4' + c.beginPath(); c.arc(-9 * s, 9 * s, 3 * s, 0, TAU); c.arc(8 * s, -8 * s, 3 * s, 0, TAU); c.fill() + c.beginPath(); c.ellipse(1 * s, 0, 8 * s, 6 * s, -0.8, 0, TAU); fs(c, col) + if (smoke) { + c.strokeStyle = '#d8c49c'; c.lineWidth = 1.6 + c.beginPath(); c.moveTo(-2, -10); c.quadraticCurveTo(-4, -13, -2, -15); c.stroke() + } +} +function iconBowl(c, foodCol, blobs = 3) { + c.beginPath(); c.ellipse(0, 3, 11, 6.5, 0, 0, Math.PI); fs(c, '#b98a52') + c.beginPath(); c.ellipse(0, 3, 11, 4.2, 0, Math.PI, 0); fs(c, '#d8b478') + c.fillStyle = foodCol + for (let i = 0; i < blobs; i++) { + c.beginPath() + c.arc(-6 + i * 6, 0.5 - (i % 2) * 2.4, 3.6, 0, TAU) + c.fill() + } + c.strokeStyle = '#241812'; c.lineWidth = 1.4 + c.beginPath(); c.ellipse(0, 3, 11, 6.5, 0, 0.15, Math.PI - 0.15); c.stroke() +} +function iconTool(c, headPaint) { + c.strokeStyle = '#7a5b33'; c.lineWidth = 3.2 + c.beginPath(); c.moveTo(-8, 11); c.lineTo(7, -7); c.stroke() + headPaint() +} + +export function drawItemIcon(c, code) { + c.lineJoin = 'round' + c.lineCap = 'round' + if (code === 'grass') { + c.strokeStyle = '#8f9a3e'; c.lineWidth = 2.4 + ;[[-7, 10, -10, -8], [-2, 11, -3, -11], [3, 11, 2, -9], [8, 10, 10, -7]].forEach(([x1, y1, x2, y2]) => { + c.beginPath(); c.moveTo(x1, y1); c.quadraticCurveTo((x1 + x2) / 2 + 2, 0, x2, y2); c.stroke() + }) + c.strokeStyle = '#6b5a2a'; c.lineWidth = 3 + c.beginPath(); c.moveTo(-8, 5); c.lineTo(8, 5); c.stroke() + } else if (code === 'twig') { + c.strokeStyle = '#7a5b33'; c.lineWidth = 2.8 + c.beginPath(); c.moveTo(-9, 11); c.lineTo(6, -10); c.stroke() + c.beginPath(); c.moveTo(0, -1); c.lineTo(9, -6); c.stroke() + c.fillStyle = '#8f9a3e' + c.beginPath(); c.ellipse(8, -9, 3.4, 2, -0.5, 0, TAU); c.fill() + } else if (code === 'log') { + c.fillStyle = '#8a6238'; c.beginPath(); c.rect(-11, -7, 18, 14); c.fill() + c.strokeStyle = '#241812'; c.lineWidth = 1.6; c.strokeRect(-11, -7, 18, 14) + c.beginPath(); c.ellipse(8, 0, 4.5, 7, 0, 0, TAU); fs(c, '#c9a36b') + c.beginPath(); c.ellipse(8, 0, 2, 3.4, 0, 0, TAU); c.strokeStyle = '#8a6238'; c.lineWidth = 1.2; c.stroke() + c.strokeStyle = '#6b4a28'; c.lineWidth = 1 + c.beginPath(); c.moveTo(-9, -3); c.lineTo(3, -3); c.moveTo(-9, 2); c.lineTo(3, 2); c.stroke() + } else if (code === 'rock') { + c.beginPath(); c.moveTo(-10, 8); c.lineTo(-11, -2); c.lineTo(-3, -9); c.lineTo(7, -7); c.lineTo(11, 3); c.lineTo(6, 9); c.closePath(); fs(c, '#8d9097') + c.beginPath(); c.moveTo(-3, -9); c.lineTo(0, -1); c.lineTo(-11, -2); c.closePath(); fs(c, '#b0b4bc', null) + } else if (code === 'flint') { + c.beginPath(); c.moveTo(-9, 6); c.lineTo(-2, -10); c.lineTo(4, 2); c.closePath(); fs(c, '#c07a3e') + c.beginPath(); c.moveTo(1, 9); c.lineTo(7, -5); c.lineTo(10, 7); c.closePath(); fs(c, '#8d9097') + } else if (code === 'gold') { + c.beginPath(); c.moveTo(-10, 7); c.lineTo(-8, -4); c.lineTo(-1, -9); c.lineTo(8, -5); c.lineTo(10, 5); c.lineTo(3, 9); c.closePath(); fs(c, '#e0b83a', '#8e6a14', 1.8) + c.fillStyle = '#f4dc8a' + c.beginPath(); c.moveTo(-8, -4); c.lineTo(-1, -9); c.lineTo(0, -2); c.closePath(); c.fill() + } else if (code === 'silk') { + c.strokeStyle = '#e8e2d4'; c.lineWidth = 2.2 + c.beginPath(); c.arc(0, 0, 9, 0.3, 5.6); c.stroke() + c.beginPath(); c.arc(1, 1, 5.5, 2.2, 8); c.stroke() + c.beginPath(); c.arc(-1, 0, 2.6, 0, TAU); c.stroke() + c.strokeStyle = '#241812'; c.lineWidth = 1 + c.beginPath(); c.arc(0, 0, 10.6, 0, TAU); c.stroke() + } else if (code === 'spidergland') { + c.beginPath(); c.ellipse(0, 1, 8, 9, 0, 0, TAU); fs(c, '#c98aa8') + c.beginPath(); c.ellipse(-2, -2, 3, 4, 0.3, 0, TAU); fs(c, '#e8c0d4', null) + c.strokeStyle = '#8e5a78'; c.lineWidth = 1.6 + c.beginPath(); c.moveTo(0, -8); c.quadraticCurveTo(3, -12, 6, -11); c.stroke() + } else if (code === 'rope') { + c.strokeStyle = '#a8894c'; c.lineWidth = 5 + c.beginPath(); c.arc(0, 0, 8, 0, TAU); c.stroke() + c.strokeStyle = '#7c6234'; c.lineWidth = 1.4 + for (let i = 0; i < 8; i++) { + const a = (i / 8) * TAU + c.beginPath() + c.moveTo(Math.cos(a) * 5.6, Math.sin(a) * 5.6) + c.lineTo(Math.cos(a + 0.5) * 10.4, Math.sin(a + 0.5) * 10.4) + c.stroke() + } + } else if (code === 'petals') { + ;[[-6, -2], [5, -4], [0, 6], [8, 5], [-8, 7]].forEach(([x, y], i) => { + c.beginPath() + c.ellipse(x, y, 4.4, 3, i * 0.7, 0, TAU) + fs(c, i % 2 ? '#e08a94' : '#d8b04a', '#241812', 1.2) + }) + } else if (code === 'reeds') { + c.strokeStyle = '#7c9a52'; c.lineWidth = 2.4 + ;[[-6, 11, -8, -9], [0, 12, 0, -12], [6, 11, 8, -8]].forEach(([x1, y1, x2, y2]) => { + c.beginPath(); c.moveTo(x1, y1); c.quadraticCurveTo(x2 * 0.4, 0, x2, y2); c.stroke() + }) + c.fillStyle = '#8a6a3a' + c.beginPath(); c.ellipse(0, -9, 2.6, 6, 0, 0, TAU); c.fill() + } else if (code === 'ash') { + c.fillStyle = '#9a968c' + c.beginPath(); c.moveTo(-9, 8); c.quadraticCurveTo(-4, -2, 0, -6); c.quadraticCurveTo(5, -2, 9, 8); c.closePath(); fs(c, '#9a968c') + c.fillStyle = '#c9c4b8' + ;[[-3, 2], [3, 4], [0, -2]].forEach(([x, y]) => { c.beginPath(); c.arc(x, y, 1.6, 0, TAU); c.fill() }) + } else if (code === 'beefwool') { + ;[[-5, -2, 5], [4, -3, 5.4], [0, 4, 6], [-8, 4, 4.4], [8, 4, 4.4]].forEach(([x, y, r]) => { + c.beginPath(); c.arc(x, y, r, 0, TAU); fs(c, '#6b5340', '#241812', 1.2) + }) + } else if (code === 'pigskin') { + c.beginPath(); c.moveTo(-9, -6); c.quadraticCurveTo(0, -12, 9, -6); c.lineTo(7, 8); c.quadraticCurveTo(0, 12, -7, 8); c.closePath(); fs(c, '#e8a8a0') + c.fillStyle = '#d88a80' + c.beginPath(); c.ellipse(0, 6, 4, 3, 0, 0, TAU); c.fill() + c.strokeStyle = '#241812'; c.lineWidth = 1.2 + c.beginPath(); c.moveTo(-2, 5); c.lineTo(-2, 7); c.moveTo(2, 5); c.lineTo(2, 7); c.stroke() + } else if (code === 'rot') { + c.beginPath(); c.moveTo(-8, 8); c.quadraticCurveTo(-10, -4, -2, -8); c.quadraticCurveTo(8, -10, 9, 0); c.quadraticCurveTo(10, 8, 0, 9); c.closePath(); fs(c, '#5a6b3a') + c.fillStyle = '#3d4d28' + ;[[-3, 0], [4, 2], [0, -4]].forEach(([x, y]) => { c.beginPath(); c.arc(x, y, 2, 0, TAU); c.fill() }) + c.strokeStyle = '#8a9a5a'; c.lineWidth = 1.2 + c.beginPath(); c.moveTo(2, -9); c.quadraticCurveTo(4, -13, 7, -12); c.stroke() + } else if (code === 'berry' || code === 'cookedberry') { + const col = code === 'berry' ? '#c94f4f' : '#7e3550' + ;[[-5, 2], [4, 0], [-1, 8]].forEach(([x, y]) => { c.beginPath(); c.arc(x, y, 4.6, 0, TAU); fs(c, col) }) + c.fillStyle = '#5d7a35' + c.beginPath(); c.ellipse(-2, -7, 5, 2.6, -0.4, 0, TAU); c.fill() + if (code === 'cookedberry') { + c.strokeStyle = '#d8c49c'; c.lineWidth = 1.6 + c.beginPath(); c.moveTo(-4, -11); c.quadraticCurveTo(-6, -14, -4, -16); c.moveTo(2, -11); c.quadraticCurveTo(0, -14, 2, -16); c.stroke() + } + } else if (code === 'meat') { iconMeat(c, '#b5484e') } + else if (code === 'cookedmeat') { iconMeat(c, '#96603a', false, true) } + else if (code === 'morsel') { iconMeat(c, '#c05a60', true) } + else if (code === 'cookedmorsel') { iconMeat(c, '#a06a42', true, true) } + else if (code === 'monstermeat') { iconMeat(c, '#7e3a8e'); c.fillStyle = '#5a2468'; c.beginPath(); c.arc(2, 1, 2.2, 0, TAU); c.arc(-2, -2, 1.6, 0, TAU); c.fill() } + else if (code === 'cookedmonster') { iconMeat(c, '#6b4a3a', false, true) } + else if (code === 'froglegs' || code === 'cookedfroglegs') { + const col = code === 'froglegs' ? '#8aa848' : '#a08048' + c.strokeStyle = col; c.lineWidth = 4.4 + c.beginPath(); c.moveTo(-6, -8); c.quadraticCurveTo(-2, 2, -8, 8); c.stroke() + c.beginPath(); c.moveTo(6, -8); c.quadraticCurveTo(2, 2, 8, 8); c.stroke() + c.fillStyle = col + c.beginPath(); c.ellipse(0, -8, 7, 4.4, 0, 0, TAU); fs(c, col, '#241812', 1.4) + } else if (code === 'honey') { + c.beginPath(); c.moveTo(-6, -9); c.lineTo(6, -9); c.lineTo(6, -5); c.lineTo(8, -3); c.lineTo(8, 8); c.quadraticCurveTo(0, 11, -8, 8); c.lineTo(-8, -3); c.lineTo(-6, -5); c.closePath(); fs(c, '#e8b83a') + c.fillStyle = '#f4d878' + c.beginPath(); c.ellipse(-2, 2, 3, 5, 0.2, 0, TAU); c.fill() + c.strokeStyle = '#241812'; c.lineWidth = 1.4 + c.beginPath(); c.moveTo(-6, -9); c.lineTo(6, -9); c.stroke() + } else if (code === 'eyeball') { + c.beginPath(); c.arc(0, 0, 10, 0, TAU); fs(c, '#ece8dc') + c.beginPath(); c.arc(1, 1, 5, 0, TAU); fs(c, '#7fa8d8', '#241812', 1.4) + c.fillStyle = '#241812' + c.beginPath(); c.arc(1, 1, 2.2, 0, TAU); c.fill() + c.strokeStyle = '#c05a50'; c.lineWidth = 1 + c.beginPath(); c.moveTo(-8, -4); c.lineTo(-4, -1); c.moveTo(-7, 5); c.lineTo(-3, 3); c.stroke() + } else if (code === 'meatballs') { iconBowl(c, '#b5605a', 3) } + else if (code === 'meatystew') { iconBowl(c, '#8e4a3a', 4) } + else if (code === 'honeyham') { + iconMeat(c, '#d88a5a') + c.fillStyle = 'rgba(244,216,120,0.85)' + c.beginPath(); c.ellipse(1, -1, 6, 4, -0.8, 0, TAU); c.fill() + } else if (code === 'honeynuggets') { iconBowl(c, '#e0b458', 3) } + else if (code === 'taffy') { + c.beginPath(); c.ellipse(0, 0, 8, 5.4, -0.3, 0, TAU); fs(c, '#e89ab8') + ;[-1, 1].forEach((s) => { + c.beginPath(); c.moveTo(s * 7, -2); c.lineTo(s * 13, -5); c.lineTo(s * 12, 3); c.closePath(); fs(c, '#d87ea0', '#241812', 1.4) + }) + } else if (code === 'monsterlasagna') { iconBowl(c, '#7e3a8e', 3) } + else if (code === 'wetgoop') { iconBowl(c, '#8a9a8a', 2) } + else if (code === 'healingsalve') { + c.beginPath(); c.arc(0, 2, 8, 0, TAU); fs(c, '#c9c0ac') + c.beginPath(); c.ellipse(0, -5, 8, 3, 0, 0, TAU); fs(c, '#ece7db') + c.strokeStyle = '#5d7a35'; c.lineWidth = 2.2 + c.beginPath(); c.moveTo(-3, 2); c.lineTo(3, 2); c.moveTo(0, -1); c.lineTo(0, 5); c.stroke() + } else if (code === 'honeypoultice') { + c.fillStyle = '#ece7db' + c.beginPath(); c.rect(-9, -6, 18, 12); c.fill() + c.strokeStyle = '#241812'; c.lineWidth = 1.6; c.strokeRect(-9, -6, 18, 12) + c.fillStyle = 'rgba(232,184,58,0.9)' + c.beginPath(); c.ellipse(0, 0, 5, 3.4, 0, 0, TAU); c.fill() + c.strokeStyle = '#c9c0ac'; c.lineWidth = 1.2 + c.beginPath(); c.moveTo(-4, -6); c.lineTo(-4, 6); c.moveTo(4, -6); c.lineTo(4, 6); c.stroke() + } else if (code === 'axe') { + iconTool(c, () => { + c.beginPath(); c.moveTo(3, -11); c.lineTo(12, -4); c.lineTo(6, 2); c.lineTo(1, -3); c.closePath(); fs(c, '#9aa0a8') + }) + } else if (code === 'pickaxe') { + c.strokeStyle = '#7a5b33'; c.lineWidth = 3.2 + c.beginPath(); c.moveTo(-2, 13); c.lineTo(-2, -6); c.stroke() + c.beginPath(); c.moveTo(-12, -4); c.quadraticCurveTo(-2, -14, 9, -4); c.quadraticCurveTo(-2, -9, -12, -4); fs(c, '#9aa0a8') + } else if (code === 'shovel') { + c.strokeStyle = '#7a5b33'; c.lineWidth = 3.2 + c.beginPath(); c.moveTo(-6, -12); c.lineTo(3, 2); c.stroke() + c.beginPath(); c.moveTo(1, 0); c.quadraticCurveTo(10, 2, 8, 10); c.quadraticCurveTo(2, 12, -2, 5); c.closePath(); fs(c, '#9aa0a8') + } else if (code === 'spear') { + c.strokeStyle = '#7a5b33'; c.lineWidth = 3 + c.beginPath(); c.moveTo(-10, 12); c.lineTo(6, -6); c.stroke() + c.beginPath(); c.moveTo(4, -4); c.lineTo(11, -11); c.lineTo(8, -1); c.closePath(); fs(c, '#b0b4bc') + c.strokeStyle = '#a8894c'; c.lineWidth = 1.6 + c.beginPath(); c.moveTo(2, -1); c.lineTo(6, -5); c.stroke() + } else if (code === 'tentaclespike') { + c.strokeStyle = '#6b4a7e'; c.lineWidth = 4 + c.beginPath(); c.moveTo(-9, 11); c.quadraticCurveTo(0, 2, 8, -9); c.stroke() + c.fillStyle = '#e8e2d4' + ;[[-2, 2], [2, -2], [6, -6]].forEach(([x, y]) => { + c.beginPath(); c.moveTo(x, y); c.lineTo(x + 4, y - 1); c.lineTo(x + 1, y - 4); c.closePath(); c.fill() + }) + } else if (code === 'torch') { + c.strokeStyle = '#7a5b33'; c.lineWidth = 3.2 + c.beginPath(); c.moveTo(-6, 12); c.lineTo(3, -3); c.stroke() + c.beginPath(); c.moveTo(6, -13); c.quadraticCurveTo(11, -6, 4, -2); c.quadraticCurveTo(-1, -5, 6, -13); fs(c, '#f0923c', '#b5541e', 1.4) + c.beginPath(); c.ellipse(4.6, -6.5, 2, 3, 0.3, 0, TAU); fs(c, '#ffd76e', null) + } else if (code === 'backpack') { + c.beginPath(); c.moveTo(-8, -8); c.quadraticCurveTo(0, -13, 8, -8); c.lineTo(8, 9); c.quadraticCurveTo(0, 12, -8, 9); c.closePath(); fs(c, '#b08a54') + c.beginPath(); c.rect(-5, -2, 10, 8); fs(c, '#8e6c3e', '#241812', 1.4) + c.strokeStyle = '#241812'; c.lineWidth = 1.4 + c.beginPath(); c.moveTo(-8, -6); c.quadraticCurveTo(-12, 0, -8, 6); c.moveTo(8, -6); c.quadraticCurveTo(12, 0, 8, 6); c.stroke() + } else if (code === 'logsuit') { + c.beginPath(); c.moveTo(-9, -9); c.lineTo(9, -9); c.lineTo(7, 10); c.lineTo(-7, 10); c.closePath(); fs(c, '#8a6238') + c.strokeStyle = '#5b4128'; c.lineWidth = 2 + ;[-4, 0, 4].forEach((x) => { c.beginPath(); c.moveTo(x, -9); c.lineTo(x * 0.8, 10); c.stroke() }) + c.strokeStyle = '#a8894c'; c.lineWidth = 1.6 + c.beginPath(); c.moveTo(-9, -3); c.lineTo(9, -3); c.moveTo(-8, 4); c.lineTo(8, 4); c.stroke() + } else if (code === 'garland') { + c.strokeStyle = '#5d7a35'; c.lineWidth = 3 + c.beginPath(); c.arc(0, 0, 8.5, 0, TAU); c.stroke() + ;[0, 1, 2, 3, 4, 5].forEach((i) => { + const a = (i / 6) * TAU + c.beginPath(); c.arc(Math.cos(a) * 8.5, Math.sin(a) * 8.5, 2.8, 0, TAU) + fs(c, i % 2 ? '#e08a94' : '#d8b04a', '#241812', 1) + }) + } else if (code === 'footballhelmet') { + c.beginPath(); c.arc(0, 0, 9, Math.PI * 0.95, Math.PI * 0.05); c.quadraticCurveTo(9, 8, 5, 8); c.lineTo(-5, 8); c.quadraticCurveTo(-9, 8, -9, 0); c.closePath(); fs(c, '#e8a8a0') + c.strokeStyle = '#b87870'; c.lineWidth = 2 + c.beginPath(); c.moveTo(-9, 2); c.lineTo(9, 2); c.stroke() + c.strokeStyle = '#241812'; c.lineWidth = 1.4 + c.beginPath(); c.moveTo(0, -9); c.lineTo(0, -4); c.stroke() + } else if (code === 'winterhat') { + c.beginPath(); c.moveTo(-9, 6); c.quadraticCurveTo(-8, -8, 0, -10); c.quadraticCurveTo(8, -8, 9, 6); c.closePath(); fs(c, '#4a668c') + c.fillStyle = '#dfe6f0' + c.beginPath(); c.rect(-10, 4, 20, 5); c.fill() + c.strokeStyle = '#241812'; c.lineWidth = 1.6; c.strokeRect(-10, 4, 20, 5) + c.beginPath(); c.arc(0, -11, 2.6, 0, TAU); fs(c, '#dfe6f0', '#241812', 1.2) + } else if (code === 'trap') { + c.strokeStyle = '#a8894c'; c.lineWidth = 2.2 + c.beginPath(); c.arc(0, 6, 10, Math.PI, 0); c.stroke() + c.beginPath(); c.arc(0, 6, 6.6, Math.PI, 0); c.stroke() + c.beginPath(); c.moveTo(0, -4); c.lineTo(0, 6); c.stroke() + c.strokeStyle = '#6b5a2a'; c.lineWidth = 2.6 + c.beginPath(); c.moveTo(-11, 6); c.lineTo(11, 6); c.stroke() + } else if (code === 'thermalstone') { + c.beginPath(); c.moveTo(-9, 5); c.lineTo(-7, -6); c.lineTo(0, -9); c.lineTo(8, -5); c.lineTo(9, 4); c.lineTo(2, 9); c.closePath(); fs(c, '#8d9097') + c.fillStyle = 'rgba(240,146,60,0.75)' + c.beginPath(); c.arc(0, 0, 4.4, 0, TAU); c.fill() + } else if (code === 'campfire') { + c.strokeStyle = '#7a5b33'; c.lineWidth = 3 + c.beginPath(); c.moveTo(-9, 10); c.lineTo(9, 5); c.moveTo(-9, 5); c.lineTo(9, 10); c.stroke() + c.beginPath(); c.moveTo(0, -13); c.quadraticCurveTo(8, -4, 0, 4); c.quadraticCurveTo(-8, -4, 0, -13); fs(c, '#f0923c', '#b5541e', 1.4) + c.beginPath(); c.ellipse(0, -2, 2.6, 4.4, 0, 0, TAU); fs(c, '#ffd76e', null) + } else if (code === 'firepit') { + c.fillStyle = '#7e8187' + ;[-9, -3, 3, 9].forEach((x, i) => { c.beginPath(); c.arc(x, 8 - (i % 2), 3, 0, TAU); c.fill() }) + c.beginPath(); c.moveTo(0, -12); c.quadraticCurveTo(7, -4, 0, 3); c.quadraticCurveTo(-7, -4, 0, -12); fs(c, '#f0923c', '#b5541e', 1.4) + } else if (code === 'sciencemachine') { + c.beginPath(); c.rect(-8, -4, 16, 14); fs(c, '#8a6238') + c.beginPath(); c.arc(0, -7, 5.5, 0, TAU); fs(c, '#9aa0a8') + c.strokeStyle = '#241812'; c.lineWidth = 1.4 + for (let i = 0; i < 6; i++) { + const a = (i / 6) * TAU + c.beginPath(); c.moveTo(Math.cos(a) * 5.5, -7 + Math.sin(a) * 5.5); c.lineTo(Math.cos(a) * 8, -7 + Math.sin(a) * 8); c.stroke() + } + c.beginPath(); c.arc(-3, 3, 2.2, 0, TAU); fs(c, '#e0b83a', '#241812', 1.2) + } else if (code === 'alchemyengine') { + c.beginPath(); c.rect(-8, -2, 16, 12); fs(c, '#6e5a3a') + c.beginPath(); c.moveTo(-6, -2); c.lineTo(0, -13); c.lineTo(6, -2); c.closePath(); fs(c, '#8a6238') + c.beginPath(); c.arc(0, 4, 3.4, 0, TAU); fs(c, '#57a486', '#241812', 1.4) + } else if (code === 'crockpot') { + c.beginPath(); c.ellipse(0, 2, 9.5, 7.5, 0, 0, TAU); fs(c, '#4a4a52') + c.beginPath(); c.ellipse(0, -4, 8, 3, 0, 0, TAU); fs(c, '#33333a') + c.strokeStyle = '#241812'; c.lineWidth = 2 + c.beginPath(); c.moveTo(-11, 0); c.lineTo(-9, 3); c.moveTo(11, 0); c.lineTo(9, 3); c.stroke() + } +} + +// 生成全部物品图标:返回 {url: dataURL 表(DOM 用), cv: 画布表(画布内绘制用)} +export function buildIcons() { + const url = {} + const cv = {} + const extra = ['campfire', 'firepit', 'sciencemachine', 'alchemyengine', 'crockpot'] + ;[...ITEM_CODES, ...extra.filter((e) => !ITEMS[e])].forEach((code) => { + const canvas = document.createElement('canvas') + canvas.width = 40 + canvas.height = 40 + const c = canvas.getContext('2d') + c.translate(20, 20) + drawItemIcon(c, code) + cv[code] = canvas + url[code] = canvas.toDataURL() + }) + return { url, cv } +} + +// ===================================================================== +// 静态实体 sprite 缓存 +// ===================================================================== +const CACHE = {} +function sprite(key, w, h, ox, oy, paint) { + let s = CACHE[key] + if (!s) { + const canvas = document.createElement('canvas') + canvas.width = w + canvas.height = h + const c = canvas.getContext('2d') + c.translate(ox, oy) + c.lineJoin = 'round' + c.lineCap = 'round' + paint(c) + s = CACHE[key] = { cv: canvas, ox, oy } + } + return s +} +function blit(ctx, s, rot = 0) { + if (rot) { + ctx.save() + ctx.rotate(rot) + ctx.drawImage(s.cv, -s.ox, -s.oy) + ctx.restore() + } else { + ctx.drawImage(s.cv, -s.ox, -s.oy) + } +} +// 冬季给顶部盖一层雪的通用画法 +function snowCap(c, pts) { + c.fillStyle = 'rgba(238,246,252,0.9)' + pts.forEach(([x, y, rx, ry]) => { + c.beginPath() + c.ellipse(x, y, rx, ry, 0, 0, TAU) + c.fill() + }) +} + +function paintTree(c, winter) { + shadow(c, 0, 4, 20, 7) + c.beginPath() + c.moveTo(-6, 4); c.lineTo(-4, -26); c.lineTo(4, -26); c.lineTo(6, 4) + c.closePath() + fs(c, '#5b4128') + const layer = (yTop, w, col) => { + c.beginPath() + c.moveTo(-w, yTop + 22) + for (let i = 0; i <= 4; i++) { + c.lineTo(-w + (i * 2 * w) / 4, yTop + 22 - (i % 2 === 0 ? 0 : 9)) + } + c.lineTo(0, yTop) + c.closePath() + fs(c, col) + } + layer(-78, 26, winter ? '#3d5a48' : '#2c4a28') + layer(-58, 32, winter ? '#35503e' : '#27411f') + layer(-38, 38, winter ? '#2c4434' : '#1f3619') + if (winter) snowCap(c, [[0, -76, 12, 4], [-14, -40, 12, 4], [15, -42, 11, 3.6], [0, -57, 14, 4]]) +} +function paintRock(c, gold, winter) { + shadow(c, 0, 8, 20, 6) + c.beginPath() + c.moveTo(-18, 8); c.lineTo(-16, -6); c.lineTo(-5, -15); c.lineTo(10, -12); c.lineTo(18, 2); c.lineTo(12, 8) + c.closePath() + fs(c, '#8d9097') + c.beginPath() + c.moveTo(-5, -15); c.lineTo(1, -3); c.lineTo(-16, -6) + c.closePath() + c.fillStyle = '#b0b4bc' + c.fill() + c.fillStyle = gold ? '#e0b83a' : '#c07a3e' + ;[[6, -2, 2.6], [-4, 3, 2], [1, -8, 1.8]].forEach(([x, y, r]) => { + c.beginPath(); c.arc(x, y, r, 0, TAU); c.fill() + }) + if (gold) { + c.strokeStyle = '#8e6a14' + c.lineWidth = 1 + ;[[6, -2, 2.6], [-4, 3, 2]].forEach(([x, y, r]) => { c.beginPath(); c.arc(x, y, r, 0, TAU); c.stroke() }) + } + if (winter) snowCap(c, [[-4, -12, 10, 3.4], [8, -8, 7, 2.6]]) +} +function paintGrass(c, stub) { + shadow(c, 0, 6, 12, 4) + const blades = stub ? [[-4, -8], [0, -9], [4, -7]] : [[-9, -18], [-5, -24], [0, -27], [5, -23], [9, -17], [-2, -20]] + ;[[stub ? '#7c7a3a' : '#9aa34a', 3], ['#6b7231', 1]].forEach(([col, lw]) => { + c.strokeStyle = col + c.lineWidth = lw + blades.forEach(([bx, by]) => { + c.beginPath() + c.moveTo(bx * 0.4, 5) + c.quadraticCurveTo(bx * 0.8, by * 0.5, bx, by) + c.stroke() + }) + }) +} +function paintSapling(c, stub) { + shadow(c, 0, 5, 9, 3.4) + c.strokeStyle = '#7a5b33' + c.lineWidth = 2.6 + c.beginPath(); c.moveTo(0, 5); c.quadraticCurveTo(1, -10, 0, -22); c.stroke() + c.lineWidth = 2 + c.beginPath(); c.moveTo(0, -8); c.lineTo(7, -16); c.moveTo(0, -13); c.lineTo(-6, -20); c.stroke() + if (!stub) { + c.fillStyle = '#8f9a3e' + ;[[0, -24], [8, -18], [-7, -22]].forEach(([lx, ly]) => { + c.beginPath(); c.ellipse(lx, ly, 4, 2.4, -0.4, 0, TAU); c.fill() + }) + } +} +function paintBerry(c, stub, winter) { + shadow(c, 0, 7, 15, 5) + const col = stub ? '#3d4d2d' : winter ? '#44543c' : '#4d6234' + ;[[-8, -8, 10], [8, -9, 10], [0, -16, 11], [0, -6, 12]].forEach(([bx, by, r]) => { + c.beginPath(); c.arc(bx, by, r, 0, TAU); fs(c, col, '#1d1409', 1.6) + }) + if (!stub && !winter) { + c.fillStyle = '#c94f4f' + ;[[-8, -12], [3, -18], [9, -6], [-3, -4], [6, -13], [-11, -4]].forEach(([bx, by]) => { + c.beginPath(); c.arc(bx, by, 2.8, 0, TAU); c.fill() + }) + } + if (winter) snowCap(c, [[0, -20, 10, 3.4], [-8, -12, 6, 2.4]]) +} +function paintReeds(c, stub) { + shadow(c, 0, 6, 12, 4) + c.strokeStyle = stub ? '#5a6b48' : '#7c9a52' + c.lineWidth = 2.6 + const stalks = stub ? [[-5, -8], [3, -9]] : [[-8, -22], [-3, -27], [3, -25], [8, -20]] + stalks.forEach(([bx, by]) => { + c.beginPath() + c.moveTo(bx * 0.4, 5) + c.quadraticCurveTo(bx * 0.8, by * 0.5, bx, by) + c.stroke() + }) + if (!stub) { + c.fillStyle = '#8a6a3a' + ;[[-3, -24], [3, -22]].forEach(([x, y]) => { + c.beginPath(); c.ellipse(x, y, 2.2, 5, 0, 0, TAU); c.fill() + }) + } +} +function paintFlower(c, hue) { + shadow(c, 0, 4, 8, 3) + c.strokeStyle = '#5d7a35' + c.lineWidth = 2 + c.beginPath(); c.moveTo(0, 4); c.quadraticCurveTo(1, -3, 0, -8); c.stroke() + const col = hue < 0.5 ? '#d8b04a' : '#c86a70' + for (let i = 0; i < 5; i++) { + const a = (i / 5) * TAU + c.beginPath() + c.ellipse(Math.cos(a) * 4.4, -10 + Math.sin(a) * 4.4, 3.2, 3.2, 0, 0, TAU) + fs(c, col, '#1d1409', 1) + } + c.beginPath(); c.arc(0, -10, 2.4, 0, TAU); fs(c, '#7a5a20', '#1d1409', 1) +} +function paintFlint(c) { + shadow(c, 0, 5, 10, 3.4) + c.beginPath(); c.moveTo(-8, 4); c.lineTo(-2, -8); c.lineTo(3, 1); c.closePath(); fs(c, '#c07a3e', '#1d1409', 1.6) + c.beginPath(); c.moveTo(0, 5); c.lineTo(6, -5); c.lineTo(9, 4); c.closePath(); fs(c, '#8d9097', '#1d1409', 1.6) +} +function paintStump(c, winter) { + shadow(c, 0, 5, 14, 5) + c.beginPath() + c.moveTo(-9, 4); c.lineTo(-8, -10); c.lineTo(8, -10); c.lineTo(9, 4) + c.closePath() + fs(c, '#5b4128') + c.beginPath(); c.ellipse(0, -10, 8, 3.4, 0, 0, TAU); fs(c, '#c9a36b', '#1d1409', 1.6) + c.strokeStyle = '#8a6238' + c.lineWidth = 1 + c.beginPath(); c.ellipse(0, -10, 4.4, 1.8, 0, 0, TAU); c.stroke() + if (winter) snowCap(c, [[0, -12, 8, 2.6]]) +} +function paintRubble(c) { + shadow(c, 0, 6, 14, 4) + ;[[-8, 2, 6], [4, 4, 5], [-1, -2, 4]].forEach(([px, py, r]) => { + c.beginPath() + c.moveTo(px - r, py + r * 0.7); c.lineTo(px - r * 0.6, py - r); c.lineTo(px + r, py - r * 0.4); c.lineTo(px + r * 0.7, py + r * 0.8) + c.closePath() + fs(c, '#7e8187', '#1d1409', 1.4) + }) +} +function paintRabbithole(c) { + shadow(c, 0, 4, 16, 5) + c.beginPath(); c.ellipse(0, 0, 16, 8, 0, Math.PI, 0); fs(c, '#8a6a48') + c.beginPath(); c.ellipse(0, 0, 10, 5, 0, 0, TAU); fs(c, '#2a1e14', '#1d1409', 1.6) + c.fillStyle = '#9a7a54' + ;[[-13, -3], [12, -2], [0, -8]].forEach(([x, y]) => { + c.beginPath(); c.arc(x, y, 2, 0, TAU); c.fill() + }) +} +function paintSpiderNest(c, winter) { + shadow(c, 0, 10, 26, 8) + c.beginPath() + c.moveTo(-24, 8) + c.quadraticCurveTo(-26, -18, -8, -30) + c.quadraticCurveTo(0, -36, 8, -30) + c.quadraticCurveTo(26, -18, 24, 8) + c.closePath() + fs(c, '#ddd5c2') + c.strokeStyle = '#b9ad92' + c.lineWidth = 1.6 + ;[[-18, -4, 18, -8], [-14, -16, 12, -18], [-20, 4, 20, 2]].forEach(([x1, y1, x2, y2]) => { + c.beginPath() + c.moveTo(x1, y1) + c.quadraticCurveTo(0, (y1 + y2) / 2 + 5, x2, y2) + c.stroke() + }) + c.beginPath(); c.ellipse(0, 2, 8, 6, 0, 0, TAU); fs(c, '#211a14', '#1d1409', 1.6) + if (winter) snowCap(c, [[0, -30, 14, 4]]) +} +function paintBeehive(c) { + shadow(c, 0, 8, 20, 6) + ;[[0, -6, 17, 13], [0, -16, 13, 9], [0, -24, 8, 6]].forEach(([x, y, rx, ry]) => { + c.beginPath(); c.ellipse(x, y, rx, ry, 0, 0, TAU); fs(c, '#d8a437') + }) + c.strokeStyle = '#a87c1e' + c.lineWidth = 1.6 + ;[-12, -4, 4].forEach((y) => { + c.beginPath(); c.moveTo(-14, y); c.quadraticCurveTo(0, y + 4, 14, y); c.stroke() + }) + c.beginPath(); c.ellipse(0, -2, 4.4, 5.4, 0, 0, TAU); fs(c, '#3a2c14', '#1d1409', 1.4) +} +function paintPighouse(c, winter) { + shadow(c, 0, 10, 30, 9) + c.beginPath(); c.rect(-24, -26, 48, 34); fs(c, '#b08a54') + c.strokeStyle = '#8e6c3e' + c.lineWidth = 2 + ;[-12, 0, 12].forEach((x) => { c.beginPath(); c.moveTo(x, -26); c.lineTo(x, 8); c.stroke() }) + c.beginPath() + c.moveTo(-30, -24); c.lineTo(0, -46); c.lineTo(30, -24) + c.closePath() + fs(c, winter ? '#d8e2ea' : '#a84c38') + c.beginPath(); c.ellipse(0, -2, 8, 10, 0, 0, TAU); fs(c, '#3a2415', '#1d1409', 1.8) + c.beginPath(); c.arc(14, -16, 4.4, 0, TAU); fs(c, '#e8d8b0', '#1d1409', 1.6) + if (winter) snowCap(c, [[0, -44, 10, 3.4], [-16, -32, 10, 3], [16, -32, 10, 3]]) +} +function paintTentacleMound(c) { + shadow(c, 0, 4, 15, 5) + c.beginPath(); c.ellipse(0, 0, 14, 6, 0, Math.PI, 0); fs(c, '#4c5646', '#2a3024', 1.8) + c.fillStyle = '#3a4234' + ;[[-6, -3], [5, -2], [0, -5]].forEach(([x, y]) => { + c.beginPath(); c.arc(x, y, 2, 0, TAU); c.fill() + }) +} +function paintScience(c, winter) { + shadow(c, 0, 8, 24, 8) + c.beginPath(); c.rect(-18, -22, 36, 30); fs(c, '#8a6238') + c.strokeStyle = '#5b4128' + c.lineWidth = 1.6 + c.beginPath(); c.moveTo(-18, -8); c.lineTo(18, -8); c.stroke() + // 顶部齿轮 + c.beginPath(); c.arc(0, -30, 9, 0, TAU); fs(c, '#9aa0a8') + c.strokeStyle = '#241812' + c.lineWidth = 2.4 + for (let i = 0; i < 8; i++) { + const a = (i / 8) * TAU + c.beginPath() + c.moveTo(Math.cos(a) * 9, -30 + Math.sin(a) * 9) + c.lineTo(Math.cos(a) * 13, -30 + Math.sin(a) * 13) + c.stroke() + } + c.beginPath(); c.arc(0, -30, 3.4, 0, TAU); fs(c, '#6e7278', '#241812', 1.4) + // 仪表与摇柄 + c.beginPath(); c.arc(-8, 0, 4, 0, TAU); fs(c, '#e0b83a', '#241812', 1.4) + c.strokeStyle = '#241812' + c.beginPath(); c.moveTo(-8, 0); c.lineTo(-6, -3); c.stroke() + c.beginPath(); c.rect(4, -4, 10, 8); fs(c, '#6e5a3a', '#241812', 1.4) + if (winter) snowCap(c, [[0, -38, 12, 3.6]]) +} +function paintAlchemy(c, winter) { + shadow(c, 0, 8, 26, 8) + c.beginPath(); c.rect(-20, -18, 40, 28); fs(c, '#6e5a3a') + c.beginPath() + c.moveTo(-16, -18); c.lineTo(0, -44); c.lineTo(16, -18) + c.closePath() + fs(c, '#8a6238') + c.beginPath(); c.arc(0, -26, 5, 0, TAU); fs(c, '#57a486', '#241812', 1.6) + c.beginPath(); c.rect(-12, -8, 9, 12); fs(c, '#57707e', '#241812', 1.4) + c.beginPath(); c.arc(8, -2, 4.4, 0, TAU); fs(c, '#c98aa8', '#241812', 1.4) + c.strokeStyle = '#241812' + c.lineWidth = 1.6 + c.beginPath(); c.moveTo(4, 10); c.lineTo(12, 10); c.stroke() + if (winter) snowCap(c, [[0, -42, 9, 3.2]]) +} + +// ===================================================================== +// 实体绘制分发(ctx 已平移到实体脚底) +// env: {winter, time, icons} +// ===================================================================== +export function drawEnt(ctx, ent, env) { + const { winter, time, icons } = env + const sway = Math.sin(time * 1.1 + (ent.id % 13)) * 0.02 + if (ent.type === 'tree') { + blit(ctx, sprite(winter ? 'tree_w' : 'tree', 100, 124, 50, 112, (c) => paintTree(c, winter)), sway) + } else if (ent.type === 'rock') { + blit(ctx, sprite(winter ? 'rock_w' : 'rock', 48, 40, 24, 28, (c) => paintRock(c, false, winter))) + } else if (ent.type === 'goldrock') { + blit(ctx, sprite(winter ? 'goldrock_w' : 'goldrock', 48, 40, 24, 28, (c) => paintRock(c, true, winter))) + } else if (ent.type === 'grass') { + blit(ctx, sprite(ent.stub ? 'grass_s' : 'grass', 48, 44, 24, 36, (c) => paintGrass(c, ent.stub)), sway * 1.6) + } else if (ent.type === 'sapling') { + blit(ctx, sprite(ent.stub ? 'sapling_s' : 'sapling', 44, 48, 22, 40, (c) => paintSapling(c, ent.stub)), sway * 1.4) + } else if (ent.type === 'berry') { + blit(ctx, sprite(`berry${ent.stub ? '_s' : ''}${winter ? '_w' : ''}`, 52, 50, 26, 40, (c) => paintBerry(c, ent.stub, winter))) + } else if (ent.type === 'reeds') { + blit(ctx, sprite(ent.stub ? 'reeds_s' : 'reeds', 44, 52, 22, 42, (c) => paintReeds(c, ent.stub)), sway * 1.5) + } else if (ent.type === 'flower') { + blit(ctx, sprite(ent.id % 2 ? 'flower_a' : 'flower_b', 30, 32, 15, 24, (c) => paintFlower(c, ent.id % 2 ? 0.3 : 0.7))) + } else if (ent.type === 'flint') { + blit(ctx, sprite('flint', 30, 26, 15, 18, paintFlint)) + } else if (ent.type === 'stump') { + blit(ctx, sprite(winter ? 'stump_w' : 'stump', 40, 40, 20, 30, (c) => paintStump(c, winter))) + } else if (ent.type === 'rubble') { + blit(ctx, sprite('rubble', 40, 30, 20, 22, paintRubble)) + } else if (ent.type === 'rabbithole') { + blit(ctx, sprite('rabbithole', 44, 30, 22, 18, paintRabbithole)) + } else if (ent.type === 'spidernest') { + blit(ctx, sprite(winter ? 'nest_w' : 'nest', 64, 60, 32, 48, (c) => paintSpiderNest(c, winter))) + } else if (ent.type === 'beehive') { + blit(ctx, sprite('beehive', 48, 52, 24, 42, paintBeehive)) + // 环绕巢的活蜂点缀 + for (let i = 0; i < 2; i++) { + const a = time * (1.4 + i * 0.5) + i * 3 + ctx.fillStyle = '#e0b83a' + ctx.beginPath() + ctx.arc(Math.cos(a) * 22, -24 + Math.sin(a * 1.7) * 8, 2, 0, TAU) + ctx.fill() + } + } else if (ent.type === 'pighouse') { + blit(ctx, sprite(winter ? 'pighouse_w' : 'pighouse', 68, 72, 34, 60, (c) => paintPighouse(c, winter))) + } else if (ent.type === 'tentacle') { + drawTentacle(ctx, ent, time) + } else if (ent.type === 'sciencemachine') { + blit(ctx, sprite(winter ? 'science_w' : 'science', 56, 68, 28, 56, (c) => paintScience(c, winter))) + } else if (ent.type === 'alchemyengine') { + blit(ctx, sprite(winter ? 'alchemy_w' : 'alchemy', 60, 76, 30, 62, (c) => paintAlchemy(c, winter))) + } else if (ent.type === 'crockpot') { + drawCrockpot(ctx, ent, time) + } else if (ent.type === 'trap') { + drawTrap(ctx, ent, time) + } else if (ent.type === 'rabbit') { + drawRabbit(ctx, ent, time) + } else if (ent.type === 'loot') { + // 掉落物:物品图标微缩 + 底部阴影 + 轻微浮动 + const bob = Math.sin(time * 2.4 + ent.id) * 1.4 + shadow(ctx, 0, 3, 9, 3) + const ic = icons?.[ent.code] + if (ic) ctx.drawImage(ic, -13, -22 + bob, 26, 26) + } +} + +// 陷阱:armed 张开支起 / caught 扣地晃动 +function drawTrap(ctx, ent, time) { + const wob = ent.state === 'caught' && ent.shake > 0 ? Math.sin(time * 40) * 2 : 0 + shadow(ctx, 0, 5, 13, 4) + ctx.save() + ctx.translate(wob, 0) + ctx.strokeStyle = '#a8894c' + ctx.lineWidth = 2.4 + if (ent.state === 'armed') { + ctx.save() + ctx.rotate(-0.5) + ;[12, 8.5].forEach((r) => { + ctx.beginPath(); ctx.arc(0, 0, r, Math.PI, 0); ctx.stroke() + }) + ctx.restore() + ctx.strokeStyle = '#6b5a2a' + ctx.beginPath(); ctx.moveTo(7, 3); ctx.lineTo(10, -8); ctx.stroke() + } else { + ;[13, 9, 5].forEach((r) => { + ctx.beginPath(); ctx.arc(0, 2, r, Math.PI, 0); ctx.stroke() + }) + ctx.strokeStyle = '#6b5a2a' + ctx.beginPath(); ctx.moveTo(-13, 2); ctx.lineTo(13, 2); ctx.stroke() + } + ctx.restore() +} + +// 兔子:蹲跳挤压拉伸,逃跑时耳朵后倒 +function drawRabbit(ctx, ent, time) { + const moving = ent.flee || Math.abs(ent.vx) + Math.abs(ent.vy) > 4 + const hopPhase = moving ? Math.abs(Math.sin(time * 9 + ent.hop)) : 0 + const lift = hopPhase * 7 + const squash = 1 - hopPhase * 0.18 + ctx.save() + ctx.scale(ent.dir || 1, 1) + shadow(ctx, 0, 7, 11 * (1 + hopPhase * 0.15), 3.6) + ctx.translate(0, -lift) + ctx.scale(1 / squash, squash) + ctx.beginPath(); ctx.ellipse(0, -6, 10, 8, 0, 0, TAU); fsx(ctx, '#eceadf') + ctx.beginPath(); ctx.arc(7, -12, 5.6, 0, TAU); fsx(ctx, '#eceadf') + const earAng = ent.flee ? -1.9 : -0.5 + ;[0, 0.5].forEach((off) => { + ctx.save() + ctx.translate(6, -16) + ctx.rotate(earAng + off) + ctx.beginPath(); ctx.ellipse(0, -7, 2.4, 7.5, 0, 0, TAU); fsx(ctx, '#eceadf', '#1d1409', 1.6) + ctx.beginPath(); ctx.ellipse(0, -6.4, 1.1, 4.8, 0, 0, TAU) + ctx.fillStyle = '#d8a8a8' + ctx.fill() + ctx.restore() + }) + ctx.beginPath(); ctx.arc(-9, -5, 3.2, 0, TAU); fsx(ctx, '#f6f4ea', '#1d1409', 1.4) + ctx.fillStyle = '#1d1409' + ctx.beginPath(); ctx.arc(9, -13, 1.2, 0, TAU); ctx.fill() + ctx.restore() +} +// ctx 版填充描边(实体/生物画师用) +function fsx(ctx, fill, stroke = '#1d1409', lw = 2) { + if (fill) { ctx.fillStyle = fill; ctx.fill() } + if (stroke) { ctx.strokeStyle = stroke; ctx.lineWidth = lw; ctx.stroke() } +} + +// 触手:潜伏=泥丘;现身=紫色尖刺触手甩击(atkT 1→0 为一次甩击) +function drawTentacle(ctx, ent, time) { + if (ent.state !== 'up') { + blit(ctx, sprite('tentmound', 36, 22, 18, 14, paintTentacleMound)) + // 潜伏微动涟漪 + const p = (time * 0.7 + ent.id) % 1 + ctx.strokeStyle = `rgba(120,134,110,${0.5 - p * 0.5})` + ctx.lineWidth = 1.6 + ctx.beginPath() + ctx.ellipse(0, 0, 10 + p * 14, 4 + p * 5, 0, 0, TAU) + ctx.stroke() + return + } + shadow(ctx, 0, 4, 14, 5) + const whip = ent.atkT > 0 ? Math.sin((1 - ent.atkT) * Math.PI) : 0 + const sway = Math.sin(time * 2.6 + ent.id) * 0.14 + ctx.save() + ctx.scale(ent.dir || 1, 1) + ctx.rotate(sway + whip * 0.9) + ctx.beginPath() + ctx.moveTo(-9, 2) + ctx.bezierCurveTo(-11, -22, -3, -34, 4, -46) + ctx.quadraticCurveTo(8, -52, 12, -50) + ctx.quadraticCurveTo(10, -44, 7, -38) + ctx.bezierCurveTo(4, -26, 9, -14, 9, 2) + ctx.closePath() + fsx(ctx, '#6b4a7e', '#2a1834', 2.2) + // 白色尖刺 + ctx.fillStyle = '#e8e2d4' + ;[[-4, -14, -0.6], [0, -26, -0.4], [4, -38, -0.2]].forEach(([x, y, a]) => { + ctx.save() + ctx.translate(x, y) + ctx.rotate(a) + ctx.beginPath() + ctx.moveTo(0, 0); ctx.lineTo(7, -2); ctx.lineTo(1, -6) + ctx.closePath() + ctx.fill() + ctx.restore() + }) + ctx.restore() +} + +// 烹饪锅:idle 空锅 / cook 火苗+冒泡 / done 满锅料理 +function drawCrockpot(ctx, ent, time) { + shadow(ctx, 0, 6, 18, 6) + // 石支脚 + ctx.fillStyle = '#7e8187' + ;[-10, 0, 10].forEach((x) => { + ctx.beginPath(); ctx.arc(x, 2, 3.4, 0, TAU); ctx.fill() + }) + // 锅体 + ctx.beginPath(); ctx.ellipse(0, -12, 16, 12, 0, 0, TAU); fsx(ctx, '#4a4a52') + ctx.beginPath(); ctx.ellipse(0, -20, 12, 4.4, 0, 0, TAU); fsx(ctx, '#33333a') + const st = ent.state + if (st === 'cook') { + // 火苗 + const fl = Math.sin(time * 8) * 2 + ctx.beginPath() + ctx.moveTo(0, -2 - 10 - fl) + ctx.quadraticCurveTo(6, -6, 0, 1) + ctx.quadraticCurveTo(-6, -6, 0, -12 - fl) + fsx(ctx, 'rgba(240,146,60,0.9)', null) + // 冒泡 + ctx.fillStyle = '#c9d0a8' + const b = (time * 1.4 + ent.id) % 1 + ctx.beginPath() + ctx.arc(-4 + b * 6, -22 - b * 8, 2 + b * 1.5, 0, TAU) + ctx.fill() + } else if (st === 'done') { + ctx.beginPath(); ctx.ellipse(0, -20, 10, 3.4, 0, 0, TAU); fsx(ctx, '#c98a4a', null) + // 香气 + ctx.strokeStyle = 'rgba(232,226,212,0.65)' + ctx.lineWidth = 1.6 + const w = Math.sin(time * 2.2) * 2 + ctx.beginPath() + ctx.moveTo(-3, -26) + ctx.quadraticCurveTo(-5 + w, -32, -3, -38) + ctx.moveTo(3, -26) + ctx.quadraticCurveTo(5 - w, -32, 3, -38) + ctx.stroke() + } + // 投料格指示点 + const ing = ent.ing || [] + for (let i = 0; i < 4; i++) { + ctx.fillStyle = i < ing.length ? '#e8c168' : 'rgba(255,255,255,0.22)' + ctx.beginPath() + ctx.arc(-9 + i * 6, -34, 2.2, 0, TAU) + ctx.fill() + } +} + +// 篝火/火堆:石圈 + 交叉柴 + 三层贝塞尔火焰(大小随余量;火堆熄灭仍保留石圈炭堆) +export function drawFirePainter(ctx, f, time) { + ctx.lineJoin = 'round' + ctx.lineCap = 'round' + if (f.pit) { + ctx.fillStyle = '#7e8187' + for (let i = 0; i < 8; i++) { + const a = (i / 8) * TAU + ctx.beginPath() + ctx.arc(Math.cos(a) * 17, 4 + Math.sin(a) * 8, 4, 0, TAU) + ctx.fill() + ctx.strokeStyle = '#54565c' + ctx.lineWidth = 1.2 + ctx.stroke() + } + ctx.fillStyle = '#2a2018' + ctx.beginPath() + ctx.ellipse(0, 4, 12, 5.4, 0, 0, TAU) + ctx.fill() + } else { + ctx.fillStyle = '#7e8187' + ;[-14, -7, 0, 7, 14].forEach((sx, i) => { + ctx.beginPath() + ctx.arc(sx, 6 + (i % 2 ? 2 : 0), 3.4, 0, TAU) + ctx.fill() + }) + } + if (f.ttl <= 0) return + ctx.strokeStyle = '#5b4128' + ctx.lineWidth = 4 + ctx.beginPath() + ctx.moveTo(-9, 5); ctx.lineTo(9, 0); ctx.moveTo(-9, 0); ctx.lineTo(9, 5) + ctx.stroke() + const vigor = 0.45 + 0.55 * Math.min(1, f.ttl / 45) + const flick = Math.sin(time * 9) * 2.5 * vigor + const scale = f.pit ? 1.2 : 1 + const flame = (h, w, col) => { + ctx.beginPath() + ctx.moveTo(0, (-h + flick * 0.4) * scale) + ctx.quadraticCurveTo(w * scale, -h * 0.45 * scale, 0, 2) + ctx.quadraticCurveTo(-w * scale, -h * 0.45 * scale, 0, (-h + flick * 0.4) * scale) + ctx.fillStyle = col + ctx.fill() + } + flame(30 * vigor + flick, 13 * vigor, 'rgba(240,120,40,0.88)') + flame(21 * vigor + flick * 0.7, 9 * vigor, 'rgba(255,185,60,0.9)') + flame(12 * vigor + flick * 0.4, 5.4 * vigor, 'rgba(255,238,160,0.95)') +} + +// ===================================================================== +// 生物绘制(ctx 已平移到脚底) +// ===================================================================== +export function drawMob(ctx, m, time) { + if (m.kind === 'shadow') drawShadow(ctx, m, time) + else if (m.kind === 'spider') drawSpider(ctx, m, time) + else if (m.kind === 'hound') drawHoundFig(ctx, m, time, false) + else if (m.kind === 'icehound') drawHoundFig(ctx, m, time, true) + else if (m.kind === 'treeguard') drawTreeguard(ctx, m, time) + else if (m.kind === 'pig') drawPig(ctx, m, time) + else if (m.kind === 'beefalo') drawBeefalo(ctx, m, time) + else if (m.kind === 'bee') drawBee(ctx, m, time) + else if (m.kind === 'frog') drawFrog(ctx, m, time) + else if (m.kind === 'deerclops') drawDeerclops(ctx, m, time) +} + +function drawShadow(ctx, m, time) { + const wob = Math.sin(time * 2.2 + m.bob) + ctx.save() + ctx.translate(0, wob * 4) + ctx.globalAlpha = 0.85 + ctx.beginPath() + ctx.moveTo(0, -30) + ctx.bezierCurveTo(14 + wob * 3, -26, 17, -6, 12, 6) + ctx.bezierCurveTo(9, 14, -9, 14, -12, 6) + ctx.bezierCurveTo(-17, -6, -14 - wob * 3, -26, 0, -30) + ctx.fillStyle = '#120a1e' + ctx.fill() + ctx.strokeStyle = '#120a1e' + ctx.lineWidth = 4 + ;[-8, 0, 8].forEach((tx, i) => { + const sw = Math.sin(time * 3 + i * 2 + m.bob) * 4 + ctx.beginPath() + ctx.moveTo(tx, 8) + ctx.quadraticCurveTo(tx + sw, 16, tx + sw * 1.4, 22) + ctx.stroke() + }) + ctx.strokeStyle = '#e8e5f2' + ctx.lineWidth = 2.4 + ctx.beginPath() + ctx.moveTo(-8, -18); ctx.lineTo(-3, -14) + ctx.moveTo(8, -18); ctx.lineTo(3, -14) + ctx.stroke() + ctx.globalAlpha = 1 + ctx.restore() +} + +function drawSpider(ctx, m, time) { + ctx.save() + ctx.scale(m.dir || 1, 1) + shadow(ctx, 0, 8, 14, 4.6) + const crawl = Math.sin(time * 10 + m.bob) + ctx.strokeStyle = '#17151c' + ctx.lineWidth = 2.2 + for (let s = -1; s <= 1; s += 2) { + for (let i = 0; i < 4; i++) { + const lift = Math.sin(time * 10 + m.bob + i * 1.6 + (s > 0 ? 0 : Math.PI)) * 2.4 + const bx = s * (6 + i * 1.2) + const by = -4 + i * 2.4 + ctx.beginPath() + ctx.moveTo(s * 3, -4) + ctx.lineTo(bx + s * 4, by - 4 + lift) + ctx.lineTo(bx + s * 8, 7 + lift * 0.4) + ctx.stroke() + } + } + ctx.beginPath(); ctx.ellipse(-4, -6, 10, 8, 0, 0, TAU); fsx(ctx, '#211f28') + ctx.beginPath(); ctx.arc(8, -5 + crawl * 0.6, 5.6, 0, TAU); fsx(ctx, '#2b2934') + ctx.strokeStyle = '#3a3744' + ctx.lineWidth = 1.2 + ;[[-10, -12], [-5, -14], [0, -13]].forEach(([hx, hy]) => { + ctx.beginPath(); ctx.moveTo(hx, hy); ctx.lineTo(hx - 1, hy - 3); ctx.stroke() + }) + ctx.fillStyle = '#e04840' + ;[[6.5, -7], [9.5, -7], [7, -4.4], [10, -4.6]].forEach(([ex, ey]) => { + ctx.beginPath(); ctx.arc(ex, ey + crawl * 0.6, 1.1, 0, TAU); ctx.fill() + }) + ctx.restore() +} + +function drawHoundFig(ctx, m, time, ice) { + const body = ice ? '#7fa8c8' : '#33313a' + const dark = ice ? '#5a86a8' : '#26242a' + const chest = ice ? '#dfe8f0' : '#8f8c96' + const eye = ice ? '#7fd8f0' : '#e04840' + ctx.save() + ctx.scale(m.dir || 1, 1) + shadow(ctx, 0, 9, 16, 5) + const run = time * 14 + m.bob + ctx.strokeStyle = dark + ctx.lineWidth = 3.4 + ;[[-9, 0], [-5, Math.PI], [7, Math.PI * 0.9], [11, Math.PI * 1.9]].forEach(([lx, ph]) => { + const sw = Math.sin(run + ph) * 5 + ctx.beginPath() + ctx.moveTo(lx, -6) + ctx.lineTo(lx + sw, 8) + ctx.stroke() + }) + ctx.beginPath(); ctx.ellipse(0, -9, 14, 7.4, -0.08, 0, TAU); fsx(ctx, body) + ctx.beginPath(); ctx.ellipse(6, -6, 4.4, 3, -0.3, 0, TAU) + ctx.fillStyle = chest + ctx.fill() + ctx.strokeStyle = dark + ctx.lineWidth = 2.6 + ctx.beginPath() + ctx.moveTo(-13, -11) + ctx.quadraticCurveTo(-19, -15 + Math.sin(run) * 1.4, -21, -11) + ctx.stroke() + const jaw = (Math.sin(run * 0.7) + 1) * 0.16 + ctx.save() + ctx.translate(13, -13) + ctx.beginPath(); ctx.arc(0, 0, 6.2, 0, TAU); fsx(ctx, body) + ctx.beginPath() + ctx.moveTo(-4, -4); ctx.lineTo(-2, -10); ctx.lineTo(1, -5) + ctx.closePath() + fsx(ctx, dark, '#1d1409', 1.4) + ctx.save() + ctx.rotate(-jaw) + ctx.beginPath() + ctx.moveTo(2, -2); ctx.lineTo(13, 0); ctx.lineTo(3, 2.6) + ctx.closePath() + fsx(ctx, body, '#1d1409', 1.4) + ctx.fillStyle = '#e8e5da' + ctx.beginPath() + ctx.moveTo(8, 0.4); ctx.lineTo(9.6, 2.8); ctx.lineTo(11, 0.6) + ctx.closePath() + ctx.fill() + ctx.restore() + ctx.save() + ctx.rotate(jaw) + ctx.beginPath() + ctx.moveTo(2, 2); ctx.lineTo(11, 4.4); ctx.lineTo(3, 6) + ctx.closePath() + fsx(ctx, dark, '#1d1409', 1.4) + ctx.restore() + ctx.fillStyle = eye + ctx.beginPath(); ctx.arc(1, -2.6, 1.4, 0, TAU); ctx.fill() + ctx.restore() + // 冰犬霜气 + if (ice) { + ctx.fillStyle = 'rgba(200,236,248,0.5)' + const p = (time * 1.2 + m.bob) % 1 + ctx.beginPath() + ctx.arc(m.dir * 16, -18 - p * 8, 2.4 * (1 - p), 0, TAU) + ctx.fill() + } + ctx.restore() +} + +function drawTreeguard(ctx, m, time) { + ctx.save() + ctx.scale(m.dir || 1, 1) + shadow(ctx, 0, 6, 30, 9) + const breathe = Math.sin(time * 1.6) * 1.5 + ctx.strokeStyle = '#4a3421' + ctx.lineWidth = 5 + ;[[-12, -4], [-4, 2], [6, 0], [13, -3]].forEach(([rx, ry]) => { + ctx.beginPath() + ctx.moveTo(rx * 0.5, -14) + ctx.quadraticCurveTo(rx, ry - 6, rx + (rx > 0 ? 4 : -4), 4) + ctx.stroke() + }) + ctx.beginPath() + ctx.moveTo(-17, -12); ctx.lineTo(-14, -74 + breathe); ctx.lineTo(14, -74 + breathe); ctx.lineTo(17, -12) + ctx.closePath() + fsx(ctx, '#5b4128', '#1d1409', 2.6) + ctx.strokeStyle = '#43301d' + ctx.lineWidth = 2 + ;[[-8, -20, -10, -58], [0, -16, 2, -66], [9, -22, 8, -54]].forEach(([x1, y1, x2, y2]) => { + ctx.beginPath() + ctx.moveTo(x1, y1) + ctx.quadraticCurveTo((x1 + x2) / 2 + 3, (y1 + y2) / 2, x2, y2 + breathe) + ctx.stroke() + }) + const crown = (yTop, w, col) => { + ctx.beginPath() + ctx.moveTo(-w, yTop + 14) + for (let i = 0; i <= 4; i++) { + ctx.lineTo(-w + (i * 2 * w) / 4, yTop + 14 - (i % 2 === 0 ? 0 : 7)) + } + ctx.lineTo(0, yTop) + ctx.closePath() + fsx(ctx, col, '#1d1409', 2) + } + crown(-102 + breathe, 22, '#27411f') + crown(-88 + breathe, 27, '#1f3619') + const armAng = m.state === 'windup' ? -2.2 : m.state === 'smash' ? 0.7 : -0.5 + Math.sin(time * 1.6) * 0.12 + ;[1, -1].forEach((side) => { + ctx.save() + ctx.translate(side * 15, -58 + breathe) + ctx.rotate(side === 1 ? armAng : -armAng * 0.55 - 0.4) + ctx.strokeStyle = '#5b4128' + ctx.lineWidth = 7 + ctx.beginPath() + ctx.moveTo(0, 0) + ctx.quadraticCurveTo(side * 16, 10, side * 26, 26) + ctx.stroke() + ctx.lineWidth = 4 + ctx.beginPath() + ctx.moveTo(side * 26, 26); ctx.lineTo(side * 34, 34) + ctx.moveTo(side * 26, 26); ctx.lineTo(side * 22, 38) + ctx.stroke() + ctx.restore() + }) + const glow = m.state === 'windup' ? 1 : 0.6 + Math.sin(time * 4) * 0.2 + ctx.fillStyle = `rgba(224,64,48,${glow})` + ctx.beginPath() + ctx.arc(-6, -62 + breathe, 3, 0, TAU) + ctx.arc(7, -62 + breathe, 3, 0, TAU) + ctx.fill() + ctx.restore() +} + +// 猪人:粉色双足胖子,步行摆腿;结盟时眼神朝向主人 +function drawPig(ctx, m, time) { + ctx.save() + ctx.scale(m.dir || 1, 1) + shadow(ctx, 0, 8, 14, 5) + const walkSwing = m.moving ? Math.sin(time * 9 + m.bob) : 0 + // 腿 + ctx.strokeStyle = '#c98a80' + ctx.lineWidth = 5 + ctx.beginPath() + ctx.moveTo(-5, -6); ctx.lineTo(-5 + walkSwing * 4, 7) + ctx.moveTo(5, -6); ctx.lineTo(5 - walkSwing * 4, 7) + ctx.stroke() + // 圆身 + ctx.beginPath(); ctx.ellipse(0, -16, 13, 12, 0, 0, TAU); fsx(ctx, '#e8a8a0') + ctx.beginPath(); ctx.ellipse(0, -11, 8, 6, 0, 0, TAU) + ctx.fillStyle = '#f2c4bc' + ctx.fill() + // 手 + ctx.strokeStyle = '#e8a8a0' + ctx.lineWidth = 4 + ctx.beginPath() + ctx.moveTo(-10, -18); ctx.lineTo(-14 - walkSwing * 2, -10) + ctx.moveTo(10, -18); ctx.lineTo(14 + walkSwing * 2, -10) + ctx.stroke() + // 头 + ctx.beginPath(); ctx.arc(2, -32, 9, 0, TAU); fsx(ctx, '#e8a8a0') + // 耳朵 + ;[-1, 1].forEach((s) => { + ctx.beginPath() + ctx.moveTo(2 + s * 6, -39) + ctx.lineTo(2 + s * 10, -45) + ctx.lineTo(2 + s * 12, -38) + ctx.closePath() + fsx(ctx, '#d88a80', '#1d1409', 1.4) + }) + // 猪鼻 + 眼 + ctx.beginPath(); ctx.ellipse(8, -30, 4.4, 3.4, 0, 0, TAU); fsx(ctx, '#d88a80', '#1d1409', 1.6) + ctx.fillStyle = '#1d1409' + ctx.beginPath() + ctx.arc(7, -30.5, 0.8, 0, TAU) + ctx.arc(9.5, -30.5, 0.8, 0, TAU) + ctx.arc(4, -35, 1.4, 0, TAU) + ctx.fill() + // 结盟标记:头顶小红心 + if (m.loyalT > 0) { + ctx.fillStyle = 'rgba(220,80,90,0.9)' + const s = 1 + Math.sin(time * 4) * 0.12 + ctx.save() + ctx.translate(2, -50) + ctx.scale(s, s) + ctx.beginPath() + ctx.moveTo(0, 3) + ctx.bezierCurveTo(-5, -2, -2, -6, 0, -3) + ctx.bezierCurveTo(2, -6, 5, -2, 0, 3) + ctx.fill() + ctx.restore() + } + ctx.restore() +} + +// 皮弗娄牛:棕色蓬毛大块头 + 弯角,低头吃草/走动 +function drawBeefalo(ctx, m, time) { + ctx.save() + ctx.scale(m.dir || 1, 1) + shadow(ctx, 0, 10, 24, 7) + const walk = m.moving ? Math.sin(time * 6 + m.bob) : 0 + const graze = m.moving ? 0 : Math.sin(time * 0.8 + m.bob) * 2 + // 四腿 + ctx.strokeStyle = '#4a3828' + ctx.lineWidth = 5 + ;[[-14, 0], [-7, Math.PI], [8, Math.PI * 0.8], [15, Math.PI * 1.8]].forEach(([lx, ph]) => { + const sw = walk * Math.sin(ph + 1) * 4 + ctx.beginPath() + ctx.moveTo(lx, -10) + ctx.lineTo(lx + sw, 9) + ctx.stroke() + }) + // 蓬毛躯干(几团圆叠出蓬松感) + ;[[0, -20, 21, 15], [-12, -16, 13, 11], [12, -18, 12, 10]].forEach(([x, y, rx, ry]) => { + ctx.beginPath() + ctx.ellipse(x, y, rx, ry, 0, 0, TAU) + fsx(ctx, '#6b5340') + }) + // 毛发笔触 + ctx.strokeStyle = '#54402e' + ctx.lineWidth = 1.6 + ;[[-16, -8], [-6, -6], [6, -7], [14, -9]].forEach(([x, y]) => { + ctx.beginPath() + ctx.moveTo(x, y) + ctx.lineTo(x - 2, y + 6) + ctx.stroke() + }) + // 头(低垂) + ctx.save() + ctx.translate(20, -14 + graze) + ctx.beginPath(); ctx.ellipse(2, 0, 9, 7.4, 0.2, 0, TAU); fsx(ctx, '#5c4634') + // 弯角 + ctx.strokeStyle = '#d8c8a8' + ctx.lineWidth = 3.4 + ctx.beginPath() + ctx.moveTo(-2, -6) + ctx.quadraticCurveTo(-8, -12, -4, -16) + ctx.stroke() + ctx.beginPath() + ctx.moveTo(6, -5) + ctx.quadraticCurveTo(12, -10, 9, -15) + ctx.stroke() + // 鼻孔与眼 + ctx.fillStyle = '#1d1409' + ctx.beginPath() + ctx.arc(8, 2, 1.2, 0, TAU) + ctx.arc(1, -3, 1.4, 0, TAU) + ctx.fill() + ctx.restore() + // 尾巴 + ctx.strokeStyle = '#4a3828' + ctx.lineWidth = 2.4 + ctx.beginPath() + ctx.moveTo(-20, -20) + ctx.quadraticCurveTo(-26, -14 + Math.sin(time * 2 + m.bob) * 2, -24, -8) + ctx.stroke() + ctx.restore() +} + +// 杀人蜂:黄黑条纹小球 + 高频振翅 +function drawBee(ctx, m, time) { + const hover = Math.sin(time * 6 + m.bob) * 3 + ctx.save() + ctx.scale(m.dir || 1, 1) + shadow(ctx, 0, 4, 7, 2.6) + ctx.translate(0, -18 + hover) + // 翅膀 + const wing = Math.sin(time * 40) * 0.5 + ctx.fillStyle = 'rgba(230,240,248,0.75)' + ;[-1, 1].forEach((s) => { + ctx.save() + ctx.rotate(s * (0.5 + wing)) + ctx.beginPath() + ctx.ellipse(0, -7, 3, 6.5, 0, 0, TAU) + ctx.fill() + ctx.restore() + }) + // 身体条纹 + ctx.beginPath(); ctx.ellipse(0, 0, 7.5, 5.6, 0, 0, TAU); fsx(ctx, '#e0b83a') + ctx.save() + ctx.beginPath() + ctx.ellipse(0, 0, 7.5, 5.6, 0, 0, TAU) + ctx.clip() + ctx.fillStyle = '#2a2418' + ctx.fillRect(-2.4, -6, 2.6, 12) + ctx.fillRect(2.6, -6, 2.4, 12) + ctx.restore() + ctx.strokeStyle = '#1d1409' + ctx.lineWidth = 1.4 + ctx.beginPath() + ctx.ellipse(0, 0, 7.5, 5.6, 0, 0, TAU) + ctx.stroke() + // 蜇针 + 眼 + ctx.beginPath() + ctx.moveTo(-7, 1); ctx.lineTo(-11, 2.4); ctx.lineTo(-7, 3.4) + ctx.closePath() + fsx(ctx, '#e8e2d4', '#1d1409', 1) + ctx.fillStyle = '#1d1409' + ctx.beginPath(); ctx.arc(5.4, -1.6, 1.2, 0, TAU); ctx.fill() + ctx.restore() +} + +// 青蛙:绿团蹲跳 + 大嘴 +function drawFrog(ctx, m, time) { + const hop = m.moving ? Math.abs(Math.sin(time * 8 + m.bob)) : 0 + ctx.save() + ctx.scale(m.dir || 1, 1) + shadow(ctx, 0, 6, 11, 3.6) + ctx.translate(0, -hop * 8) + ctx.scale(1 + hop * 0.1, 1 - hop * 0.12) + // 后腿 + ctx.strokeStyle = '#5c7a34' + ctx.lineWidth = 4 + ctx.beginPath() + ctx.moveTo(-7, -3) + ctx.quadraticCurveTo(-12, 2, -9, 6) + ctx.stroke() + // 身体 + ctx.beginPath(); ctx.ellipse(0, -7, 10, 7.4, 0, 0, TAU); fsx(ctx, '#7a9a44') + ctx.beginPath(); ctx.ellipse(2, -4, 6, 4, 0, 0, TAU) + ctx.fillStyle = '#a8c070' + ctx.fill() + // 前脚 + ctx.strokeStyle = '#5c7a34' + ctx.lineWidth = 3 + ctx.beginPath() + ctx.moveTo(5, -3); ctx.lineTo(7, 5) + ctx.moveTo(1, -2); ctx.lineTo(1, 6) + ctx.stroke() + // 眼睛鼓包 + ;[[3, -14], [8, -13]].forEach(([x, y]) => { + ctx.beginPath(); ctx.arc(x, y, 3, 0, TAU); fsx(ctx, '#7a9a44', '#1d1409', 1.4) + ctx.fillStyle = '#1d1409' + ctx.beginPath(); ctx.arc(x + 1, y, 1.1, 0, TAU); ctx.fill() + }) + // 嘴线 + ctx.strokeStyle = '#1d1409' + ctx.lineWidth = 1.4 + ctx.beginPath() + ctx.moveTo(4, -8) + ctx.quadraticCurveTo(9, -7, 11, -9) + ctx.stroke() + ctx.restore() +} + +// 独眼巨鹿:极巨蓝灰毛怪 + 鹿角 + 单眼,windup 抬双臂 / smash 砸地 +function drawDeerclops(ctx, m, time) { + ctx.save() + ctx.scale(m.dir || 1, 1) + shadow(ctx, 0, 8, 40, 12) + const step = m.moving ? Math.sin(time * 4 + m.bob) : 0 + const breathe = Math.sin(time * 1.4) * 2 + // 粗腿 + ctx.strokeStyle = '#3a4450' + ctx.lineWidth = 10 + ctx.beginPath() + ctx.moveTo(-14, -34); ctx.lineTo(-16 + step * 5, 4) + ctx.moveTo(14, -34); ctx.lineTo(16 - step * 5, 4) + ctx.stroke() + // 蓬毛巨躯 + ;[[0, -62 + breathe, 30, 34], [-18, -50 + breathe, 16, 20], [18, -50 + breathe, 16, 20]].forEach(([x, y, rx, ry]) => { + ctx.beginPath() + ctx.ellipse(x, y, rx, ry, 0, 0, TAU) + fsx(ctx, '#4c5866', '#1d1420', 2.4) + }) + // 毛发笔触 + ctx.strokeStyle = '#3a4450' + ctx.lineWidth = 2 + ;[[-20, -40], [-8, -34], [6, -36], [18, -42]].forEach(([x, y]) => { + ctx.beginPath() + ctx.moveTo(x, y + breathe) + ctx.lineTo(x - 3, y + 10 + breathe) + ctx.stroke() + }) + // 手臂:windup 高举 / smash 砸地 + const armAng = m.state === 'windup' ? -2.4 : m.state === 'smash' ? 0.8 : -0.4 + Math.sin(time * 1.4) * 0.1 + ;[1, -1].forEach((side) => { + ctx.save() + ctx.translate(side * 26, -72 + breathe) + ctx.rotate(side === 1 ? armAng : -armAng * 0.6 - 0.3) + ctx.strokeStyle = '#46525e' + ctx.lineWidth = 9 + ctx.beginPath() + ctx.moveTo(0, 0) + ctx.quadraticCurveTo(side * 18, 14, side * 28, 34) + ctx.stroke() + // 爪 + ctx.strokeStyle = '#dfe6f0' + ctx.lineWidth = 3 + ctx.beginPath() + ctx.moveTo(side * 28, 34); ctx.lineTo(side * 34, 40) + ctx.moveTo(side * 28, 34); ctx.lineTo(side * 24, 42) + ctx.stroke() + ctx.restore() + }) + // 头 + 鹿角 + 单眼 + ctx.save() + ctx.translate(0, -92 + breathe) + ctx.beginPath(); ctx.ellipse(0, 0, 18, 15, 0, 0, TAU); fsx(ctx, '#4c5866', '#1d1420', 2.4) + // 鹿角 + ctx.strokeStyle = '#c9b891' + ctx.lineWidth = 4 + ;[-1, 1].forEach((s) => { + ctx.beginPath() + ctx.moveTo(s * 10, -10) + ctx.quadraticCurveTo(s * 22, -22, s * 18, -32) + ctx.moveTo(s * 16, -22) + ctx.lineTo(s * 24, -26) + ctx.stroke() + }) + // 单眼(愤怒发光) + const glow = m.state === 'windup' ? 1 : 0.7 + Math.sin(time * 5) * 0.15 + ctx.beginPath(); ctx.arc(2, -2, 7.4, 0, TAU); fsx(ctx, '#ece8dc', '#1d1420', 2) + ctx.fillStyle = `rgba(224,64,48,${glow})` + ctx.beginPath(); ctx.arc(2, -2, 4, 0, TAU); ctx.fill() + ctx.fillStyle = '#1d1420' + ctx.beginPath(); ctx.arc(2, -2, 1.6, 0, TAU); ctx.fill() + // 嘴獠牙 + ctx.fillStyle = '#dfe6f0' + ;[[-8, 10], [-2, 12], [5, 11]].forEach(([x, y]) => { + ctx.beginPath() + ctx.moveTo(x, y); ctx.lineTo(x + 3, y + 5); ctx.lineTo(x + 6, y) + ctx.closePath() + ctx.fill() + }) + ctx.restore() + ctx.restore() +} + +// 装饰点缀:草叶丛 / 碎石 / 蘑菇 +export function drawDecor(ctx, d, x, y) { + if (d.kind === 'tuft') { + ctx.strokeStyle = 'rgba(150,168,90,0.6)' + ctx.lineWidth = 1.6 + ;[-3, 0, 3].forEach((ox) => { + ctx.beginPath() + ctx.moveTo(x + ox, y + 3) + ctx.lineTo(x + ox * 1.4, y - 4) + ctx.stroke() + }) + } else if (d.kind === 'pebble') { + ctx.fillStyle = 'rgba(120,116,104,0.75)' + ctx.beginPath() + ctx.ellipse(x, y, 4.5, 3, 0.4, 0, TAU) + ctx.fill() + ctx.beginPath() + ctx.ellipse(x + 6, y + 2, 3, 2.2, -0.3, 0, TAU) + ctx.fill() + } else { + ctx.fillStyle = '#c9c2ae' + ctx.fillRect(x - 1.5, y - 2, 3, 5) + ctx.fillStyle = d.hue < 0.5 ? '#a84c38' : '#b07c34' + ctx.beginPath() + ctx.ellipse(x, y - 3, 5.5, 3.4, 0, Math.PI, 0) + ctx.fill() + ctx.fillStyle = 'rgba(255,255,255,0.8)' + ctx.beginPath() + ctx.arc(x - 2, y - 4, 1, 0, TAU) + ctx.fill() + } +} diff --git a/src/games/starve/world.js b/src/games/starve/world.js new file mode 100644 index 0000000..0c3d2e3 --- /dev/null +++ b/src/games/starve/world.js @@ -0,0 +1,448 @@ +// 饥荒世界生成:径向噪声岛屿 + 群系 Voronoi + 道路 + 实体铺设 + turf 瓦片图集 +// 瓦片网格 80px:0=海洋 1=出生草原 2=热带草原 3=草地 4=森林 5=岩石地 6=沼泽 +// (不依赖 engine 模块,方便 Node 环境独立跑生成测试) +function randInt(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min +} + +export const TILE = 80 +export const GW = 40 +export const GH = 30 +export const WORLD = { w: GW * TILE, h: GH * TILE } // 3200 x 2400 + +export const BIOMES = { + 1: { code: 'meadow', name: '出生草原', color: '#4a5d33', map: '#5d7a3d' }, + 2: { code: 'savanna', name: '热带草原', color: '#8a8a3e', map: '#a8a34e' }, + 3: { code: 'grassland', name: '草地', color: '#55703a', map: '#6d9048' }, + 4: { code: 'forest', name: '森林', color: '#33492c', map: '#3d5c36' }, + 5: { code: 'rocky', name: '岩石地', color: '#6e6a5e', map: '#8a8578' }, + 6: { code: 'marsh', name: '沼泽', color: '#3d4438', map: '#4c5646' }, +} + +export function tileIdx(x, y) { + const tx = Math.floor(x / TILE) + const ty = Math.floor(y / TILE) + if (tx < 0 || tx >= GW || ty < 0 || ty >= GH) return -1 + return ty * GW + tx +} +export function isLand(tiles, x, y) { + const i = tileIdx(x, y) + return i >= 0 && tiles[i] > 0 +} +export function biomeAt(tiles, x, y) { + const i = tileIdx(x, y) + return i >= 0 ? tiles[i] : 0 +} +export function onRoad(road, x, y) { + const i = tileIdx(x, y) + return i >= 0 && road[i] === 1 +} + +// 在 (x,y) 附近 rMin~rMax 环带内找一块陆地(怪物/掉落安全落点) +export function findLandNear(tiles, x, y, rMin, rMax, tries = 24) { + for (let i = 0; i < tries; i++) { + const a = Math.random() * Math.PI * 2 + const r = rMin + Math.random() * (rMax - rMin) + const nx = x + Math.cos(a) * r + const ny = y + Math.sin(a) * r + if (isLand(tiles, nx, ny)) return { x: nx, y: ny } + } + return { x, y } +} + +// ===================================================================== +// 岛屿 + 群系 + 道路生成 +// ===================================================================== +export function genIsland() { + const tiles = new Uint8Array(GW * GH) + const road = new Uint8Array(GW * GH) + const cx = GW / 2 + const cy = GH / 2 + // 径向噪声轮廓:三组正弦叠加出不规则海岸线 + const ph = [Math.random() * 7, Math.random() * 7, Math.random() * 7] + const landAt = (tx, ty) => { + const dx = (tx + 0.5 - cx) / (GW * 0.46) + const dy = (ty + 0.5 - cy) / (GH * 0.46) + const rad = Math.hypot(dx, dy) + const ang = Math.atan2(dy, dx) + const edge = 1 + 0.15 * Math.sin(ang * 3 + ph[0]) + 0.11 * Math.sin(ang * 5 + ph[1]) + 0.07 * Math.sin(ang * 8 + ph[2]) + return rad < edge * 0.9 + } + for (let ty = 0; ty < GH; ty++) { + for (let tx = 0; tx < GW; tx++) { + if (landAt(tx, ty)) tiles[ty * GW + tx] = 1 + } + } + // 出生点周围强制陆地 + for (let ty = Math.floor(cy) - 3; ty <= Math.floor(cy) + 3; ty++) { + for (let tx = Math.floor(cx) - 3; tx <= Math.floor(cx) + 3; tx++) { + tiles[ty * GW + tx] = 1 + } + } + // 群系簇心:出生草原在中心,其余类型在远处陆地上挑选(相互保持间距) + const centers = [{ biome: 1, tx: cx, ty: cy }] + const wantTypes = [2, 2, 3, 3, 4, 4, 4, 5, 5, 6, 6] // 热带草原x2 草地x2 森林x3 岩石x2 沼泽x2 + wantTypes.forEach((biome) => { + for (let retry = 0; retry < 60; retry++) { + const tx = randInt(2, GW - 3) + const ty = randInt(2, GH - 3) + if (!tiles[ty * GW + tx]) continue + if (Math.hypot(tx - cx, ty - cy) < 6) continue + if (centers.some((c) => Math.hypot(c.tx - tx, c.ty - ty) < 5.5)) continue + centers.push({ biome, tx, ty }) + break + } + }) + // Voronoi 归属:陆地瓦片染成最近簇心的群系(出生草原簇心加权更近,保证出生区域够大) + for (let ty = 0; ty < GH; ty++) { + for (let tx = 0; tx < GW; tx++) { + const i = ty * GW + tx + if (!tiles[i]) continue + let best = 1 + let bd = Infinity + centers.forEach((c) => { + let d = Math.hypot(c.tx - tx, c.ty - ty) + if (c.biome === 1) d *= 0.6 + if (d < bd) { bd = d; best = c.biome } + }) + tiles[i] = best + } + } + // 道路:从出生点向每个远方簇心铺一条(只在陆地上落笔) + centers.slice(1).forEach((c) => { + const steps = Math.ceil(Math.hypot(c.tx - cx, c.ty - cy)) * 2 + for (let s = 0; s <= steps; s++) { + const t = s / steps + // 轻微弯曲的路径 + const bend = Math.sin(t * Math.PI) * 1.6 * (c.tx % 2 === 0 ? 1 : -1) + const fx = cx + (c.tx - cx) * t + bend * ((c.ty - cy) / (Math.hypot(c.tx - cx, c.ty - cy) || 1)) + const fy = cy + (c.ty - cy) * t - bend * ((c.tx - cx) / (Math.hypot(c.tx - cx, c.ty - cy) || 1)) + const tx = Math.round(fx) + const ty = Math.round(fy) + if (tx < 0 || tx >= GW || ty < 0 || ty >= GH) continue + const i = ty * GW + tx + if (tiles[i]) road[i] = 1 + } + }) + return { tiles, road, centers } +} + +// ===================================================================== +// 实体铺设:按群系密度表逐瓦片掷点(带最小间距),返回放置清单与生物出生点 +// ===================================================================== +const DENSITY = { + 1: { grass: 0.2, sapling: 0.16, flint: 0.06, berry: 0.05, tree: 0.05, flower: 0.1, rabbithole: 0.03 }, + 2: { grass: 0.3, rabbithole: 0.07, sapling: 0.06, flint: 0.03, tree: 0.02, flower: 0.03 }, + 3: { berry: 0.13, flower: 0.15, grass: 0.1, sapling: 0.08, beehive: 0.02, tree: 0.05 }, + 4: { tree: 0.36, sapling: 0.06, grass: 0.04, spidernest: 0.016, flint: 0.03, flower: 0.03 }, + 5: { rock: 0.2, goldrock: 0.07, flint: 0.09, tree: 0.02 }, + 6: { reeds: 0.16, tentacle: 0.045, tree: 0.03, flint: 0.02 }, +} + +export function planEntities(tiles, road) { + const placed = [] // {type,x,y} + const grid = new Map() // 空间散列:瓦片 idx -> 该瓦片内放置点 + const minDist = (x, y, type) => { + const tx = Math.floor(x / TILE) + const ty = Math.floor(y / TILE) + for (let oy = -1; oy <= 1; oy++) { + for (let ox = -1; ox <= 1; ox++) { + const list = grid.get((ty + oy) * GW + tx + ox) + if (!list) continue + for (const p of list) { + const need = p.type === type ? 30 : 26 + if (Math.hypot(p.x - x, p.y - y) < need) return false + } + } + } + return true + } + const put = (type, x, y) => { + if (!isLand(tiles, x, y)) return false + if (!minDist(x, y, type)) return false + const p = { type, x, y } + placed.push(p) + const i = tileIdx(x, y) + if (!grid.has(i)) grid.set(i, []) + grid.get(i).push(p) + return true + } + const cx = WORLD.w / 2 + const cy = WORLD.h / 2 + // 1) 逐瓦片按群系密度掷点(道路上与出生点近处留空) + for (let ty = 0; ty < GH; ty++) { + for (let tx = 0; tx < GW; tx++) { + const i = ty * GW + tx + const biome = tiles[i] + if (!biome || road[i]) continue + const wx = tx * TILE + const wy = ty * TILE + if (Math.hypot(wx + 40 - cx, wy + 40 - cy) < 130) continue + const table = DENSITY[biome] + Object.entries(table).forEach(([type, p]) => { + if (Math.random() >= p) return + for (let retry = 0; retry < 4; retry++) { + const x = wx + 8 + Math.random() * (TILE - 16) + const y = wy + 8 + Math.random() * (TILE - 16) + if (put(type, x, y)) break + } + }) + } + } + // 2) 出生保底物资(第一晚一定造得出火把与斧头) + ;[['grass', -90, -40], ['grass', 80, 60], ['grass', -30, 95], ['grass', 110, -30], + ['sapling', 60, -80], ['sapling', -105, 40], ['sapling', 20, 120], + ['flint', -60, 80], ['flint', 95, 20], ['berry', -120, -70], ['berry', 130, 90], + ['tree', -160, 30], ['tree', 150, -100]] + .forEach(([type, dx, dy]) => put(type, cx + dx, cy + dy)) + // 3) 保证关键实体的下限数量 + const ensure = (type, min, biomes) => { + let n = placed.filter((p) => p.type === type).length + for (let retry = 0; retry < 300 && n < min; retry++) { + const tx = randInt(1, GW - 2) + const ty = randInt(1, GH - 2) + const i = ty * GW + tx + if (!biomes.includes(tiles[i]) || road[i]) continue + if (put(type, tx * TILE + 20 + Math.random() * 40, ty * TILE + 20 + Math.random() * 40)) n++ + } + } + ensure('spidernest', 2, [4]) + ensure('beehive', 2, [3]) + ensure('goldrock', 4, [5]) + ensure('rabbithole', 5, [1, 2]) + ensure('tentacle', 4, [6]) + ensure('reeds', 6, [6]) + // 4) 猪人村:草地/森林挑 2 处,每处 3 座猪屋围成一圈 + const villages = [] + for (let retry = 0; retry < 120 && villages.length < 2; retry++) { + const tx = randInt(2, GW - 3) + const ty = randInt(2, GH - 3) + const i = ty * GW + tx + if (tiles[i] !== 3 && tiles[i] !== 4) continue + const wx = tx * TILE + 40 + const wy = ty * TILE + 40 + if (Math.hypot(wx - cx, wy - cy) < 500) continue + if (villages.some((v) => Math.hypot(v.x - wx, v.y - wy) < 900)) continue + villages.push({ x: wx, y: wy }) + for (let k = 0; k < 3; k++) { + const a = (k / 3) * Math.PI * 2 + 0.5 + put('pighouse', wx + Math.cos(a) * 70, wy + Math.sin(a) * 70) + } + } + ensure('pighouse', 3, [3, 4]) // 村落选点/落位失败时的下限保底 + // 5) 牛群与青蛙出生点(生物由游戏侧生成) + const herds = [] + for (let retry = 0; retry < 120 && herds.length < 2; retry++) { + const tx = randInt(2, GW - 3) + const ty = randInt(2, GH - 3) + if (tiles[ty * GW + tx] !== 2) continue + const wx = tx * TILE + 40 + const wy = ty * TILE + 40 + if (herds.some((h) => Math.hypot(h.x - wx, h.y - wy) < 800)) continue + herds.push({ x: wx, y: wy }) + } + const frogs = [] + for (let retry = 0; retry < 200 && frogs.length < 8; retry++) { + const tx = randInt(1, GW - 2) + const ty = randInt(1, GH - 2) + if (tiles[ty * GW + tx] !== 6) continue + frogs.push({ x: tx * TILE + 10 + Math.random() * 60, y: ty * TILE + 10 + Math.random() * 60 }) + } + return { placed, herds, frogs } +} + +// 纯装饰点缀(碎石/蘑菇/草叶),按群系倾向,不可交互 +export function genDecor(tiles) { + const spots = [] + for (let i = 0; i < 110; i++) { + const x = Math.random() * WORLD.w + const y = Math.random() * WORLD.h + const b = biomeAt(tiles, x, y) + if (!b) continue + let kind = 'tuft' + if (b === 4 || b === 6) kind = Math.random() < 0.55 ? 'mushroom' : 'pebble' + else if (b === 5) kind = 'pebble' + else if (Math.random() < 0.3) kind = 'pebble' + spots.push({ x, y, kind, hue: Math.random() }) + } + return spots +} + +// ===================================================================== +// turf 瓦片图集:每群系一张 80px 纹理(含冬季雪化变体)+ 道路 +// ===================================================================== +function makeTileCv(paint) { + const cv = document.createElement('canvas') + cv.width = TILE + cv.height = TILE + const c = cv.getContext('2d') + paint(c) + return cv +} +// 伪随机(瓦片纹理内部使用,避免每次生成不一致) +function lcg(seed) { + let s = seed + return () => { + s = (s * 1664525 + 1013904223) % 4294967296 + return s / 4294967296 + } +} +export function buildTilePatterns() { + const pats = { norm: {}, winter: {} } + const speck = (c, rnd, n, colors, rMin = 2, rMax = 7) => { + for (let i = 0; i < n; i++) { + c.fillStyle = colors[Math.floor(rnd() * colors.length)] + c.beginPath() + const r = rMin + rnd() * (rMax - rMin) + c.ellipse(rnd() * TILE, rnd() * TILE, r, r * 0.6, rnd() * 3, 0, 7) + c.fill() + } + } + const defs = { + 1: (c, rnd) => { + c.fillStyle = '#4a5d33' + c.fillRect(0, 0, TILE, TILE) + speck(c, rnd, 12, ['rgba(96,120,62,0.5)', 'rgba(58,76,40,0.6)']) + }, + 2: (c, rnd) => { + c.fillStyle = '#8a8a3e' + c.fillRect(0, 0, TILE, TILE) + speck(c, rnd, 10, ['rgba(168,163,78,0.55)', 'rgba(122,118,46,0.6)']) + // 干草细笔 + c.strokeStyle = 'rgba(200,190,110,0.4)' + c.lineWidth = 1.4 + for (let i = 0; i < 8; i++) { + const x = rnd() * TILE + const y = rnd() * TILE + c.beginPath() + c.moveTo(x, y) + c.lineTo(x + 2 - rnd() * 4, y - 5 - rnd() * 4) + c.stroke() + } + }, + 3: (c, rnd) => { + c.fillStyle = '#55703a' + c.fillRect(0, 0, TILE, TILE) + speck(c, rnd, 12, ['rgba(120,150,80,0.5)', 'rgba(70,95,48,0.6)']) + }, + 4: (c, rnd) => { + c.fillStyle = '#33492c' + c.fillRect(0, 0, TILE, TILE) + speck(c, rnd, 13, ['rgba(38,62,34,0.65)', 'rgba(60,86,48,0.5)']) + }, + 5: (c, rnd) => { + c.fillStyle = '#6e6a5e' + c.fillRect(0, 0, TILE, TILE) + speck(c, rnd, 10, ['rgba(130,126,112,0.55)', 'rgba(90,86,76,0.6)']) + // 石纹裂缝 + c.strokeStyle = 'rgba(60,56,50,0.5)' + c.lineWidth = 1.2 + for (let i = 0; i < 4; i++) { + const x = rnd() * TILE + const y = rnd() * TILE + c.beginPath() + c.moveTo(x, y) + c.lineTo(x + 8 + rnd() * 10, y + 4 - rnd() * 8) + c.stroke() + } + }, + 6: (c, rnd) => { + c.fillStyle = '#3d4438' + c.fillRect(0, 0, TILE, TILE) + speck(c, rnd, 12, ['rgba(76,86,70,0.6)', 'rgba(44,50,40,0.7)']) + // 泥水洼 + c.fillStyle = 'rgba(52,64,66,0.55)' + for (let i = 0; i < 3; i++) { + c.beginPath() + c.ellipse(rnd() * TILE, rnd() * TILE, 6 + rnd() * 8, 3 + rnd() * 4, 0, 0, 7) + c.fill() + } + }, + } + Object.entries(defs).forEach(([id, paint]) => { + pats.norm[id] = makeTileCv((c) => paint(c, lcg(+id * 977))) + // 冬季变体:同底纹 + 雪覆盖 + pats.winter[id] = makeTileCv((c) => { + paint(c, lcg(+id * 977)) + c.fillStyle = 'rgba(224,232,240,0.55)' + c.fillRect(0, 0, TILE, TILE) + const rnd = lcg(+id * 1237) + c.fillStyle = 'rgba(244,250,255,0.7)' + for (let i = 0; i < 10; i++) { + c.beginPath() + c.ellipse(rnd() * TILE, rnd() * TILE, 4 + rnd() * 8, 2.5 + rnd() * 5, 0, 0, 7) + c.fill() + } + }) + }) + // 道路(叠加在陆地之上,半透明泥土带碎石) + pats.road = makeTileCv((c) => { + const rnd = lcg(4242) + c.fillStyle = 'rgba(146,120,84,0.85)' + c.fillRect(0, 0, TILE, TILE) + c.fillStyle = 'rgba(110,90,62,0.8)' + for (let i = 0; i < 8; i++) { + c.beginPath() + c.ellipse(rnd() * TILE, rnd() * TILE, 3 + rnd() * 5, 2 + rnd() * 3, 0, 0, 7) + c.fill() + } + c.fillStyle = 'rgba(180,158,120,0.6)' + for (let i = 0; i < 6; i++) { + c.beginPath() + c.arc(rnd() * TILE, rnd() * TILE, 1.5 + rnd() * 2, 0, 7) + c.fill() + } + }) + return pats +} + +// 绘制可视范围内的地面瓦片(海洋底色 + 陆地 turf + 道路 + 岸线 + 近岸波浪) +export function drawTiles(ctx, tiles, road, pats, camX, camY, W, H, time, winter) { + ctx.fillStyle = winter ? '#26404e' : '#20404c' + ctx.fillRect(0, 0, W, H) + const set = winter ? pats.winter : pats.norm + const tx0 = Math.max(0, Math.floor(camX / TILE)) + const ty0 = Math.max(0, Math.floor(camY / TILE)) + const tx1 = Math.min(GW - 1, Math.floor((camX + W) / TILE)) + const ty1 = Math.min(GH - 1, Math.floor((camY + H) / TILE)) + for (let ty = ty0; ty <= ty1; ty++) { + for (let tx = tx0; tx <= tx1; tx++) { + const i = ty * GW + tx + const b = tiles[i] + const sx = tx * TILE - camX + const sy = ty * TILE - camY + if (!b) { + // 近岸波浪:与陆地相邻的海洋瓦片画一两道流动弧线 + const nearLand = + (tx > 0 && tiles[i - 1]) || (tx < GW - 1 && tiles[i + 1]) || + (ty > 0 && tiles[i - GW]) || (ty < GH - 1 && tiles[i + GW]) + if (nearLand) { + const wob = Math.sin(time * 1.4 + tx * 1.7 + ty * 2.3) + ctx.strokeStyle = 'rgba(190,220,230,0.35)' + ctx.lineWidth = 2 + ctx.beginPath() + ctx.arc(sx + 24 + wob * 5, sy + 30, 9, Math.PI * 0.15, Math.PI * 0.85) + ctx.stroke() + if (wob > 0.2) { + ctx.beginPath() + ctx.arc(sx + 55 - wob * 4, sy + 58, 7, Math.PI * 0.15, Math.PI * 0.85) + ctx.stroke() + } + } + continue + } + ctx.drawImage(set[b] || set[1], sx, sy) + if (road[i]) { + ctx.globalAlpha = 0.9 + ctx.drawImage(pats.road, sx, sy) + ctx.globalAlpha = 1 + } + // 岸线:陆地瓦片贴海一侧描沙边 + ctx.fillStyle = winter ? 'rgba(226,234,240,0.9)' : 'rgba(196,178,128,0.9)' + if (tx > 0 && !tiles[i - 1]) ctx.fillRect(sx, sy, 4, TILE) + if (tx < GW - 1 && !tiles[i + 1]) ctx.fillRect(sx + TILE - 4, sy, 4, TILE) + if (ty > 0 && !tiles[i - GW]) ctx.fillRect(sx, sy, TILE, 4) + if (ty < GH - 1 && !tiles[i + GW]) ctx.fillRect(sx, sy, TILE, 4) + } + } +} diff --git a/src/games/starveSkins.js b/src/games/starveSkins.js new file mode 100644 index 0000000..12e87f4 --- /dev/null +++ b/src/games/starveSkins.js @@ -0,0 +1,328 @@ +// 饥荒人物皮肤模块:30 款皮肤绘制参数 + 通用人物矢量绘制 + 离屏立绘预览 +// code 与后端 game_skins 种子数据一一对应;StarveGame(本人/队友)与 GamePlay 更衣室共用 +// hair: spiky=尖刺 flat=平刘海 long=披肩长发 curly=蓬卷 crown=短发+金冠 +export const SKINS = [ + { code: 'wilson', name: '经典威尔逊', hair: 'spiky', hairColor: '#15100c', skinTone: '#f3dfc2', vestColor: '#a33d3d', shirtColor: '#e8e2d4' }, + { code: 'willow', name: '篝火少女', hair: 'long', hairColor: '#8a2f22', skinTone: '#f3dfc2', vestColor: '#43506e', shirtColor: '#dfd8c6' }, + { code: 'wendy', name: '暮色少女', hair: 'long', hairColor: '#e8d8a8', skinTone: '#f0e4d0', vestColor: '#3d3a52', shirtColor: '#d8d4e2' }, + { code: 'flame', name: '火焰行者', hair: 'spiky', hairColor: '#d8501e', skinTone: '#f3dfc2', vestColor: '#e0662a', shirtColor: '#f4e3c2' }, + { code: 'ocean', name: '海风水手', hair: 'flat', hairColor: '#2a3648', skinTone: '#f0dcc0', vestColor: '#2f6d8e', shirtColor: '#dfe8ea' }, + { code: 'forest', name: '密林猎手', hair: 'curly', hairColor: '#3f2c18', skinTone: '#e8ceac', vestColor: '#3f6b35', shirtColor: '#d8e0c2' }, + { code: 'honey', name: '蜜糖甜心', hair: 'curly', hairColor: '#c98a2e', skinTone: '#f6e6cc', vestColor: '#d8a437', shirtColor: '#f4ead0' }, + { code: 'dusk', name: '暮紫魔术师', hair: 'flat', hairColor: '#3a2a52', skinTone: '#ecd8c4', vestColor: '#6a4a9e', shirtColor: '#d8cce8' }, + { code: 'mint', name: '薄荷学者', hair: 'flat', hairColor: '#3f5548', skinTone: '#f3dfc2', vestColor: '#57a486', shirtColor: '#e6f4ec' }, + { code: 'ash', name: '灰烬流浪者', hair: 'spiky', hairColor: '#6a6a72', skinTone: '#e4d2bc', vestColor: '#7a7a84', shirtColor: '#d8d8dc' }, + { code: 'sky', name: '晴空飞行员', hair: 'flat', hairColor: '#4a668c', skinTone: '#f0dcc0', vestColor: '#7fa8d8', shirtColor: '#f0f4fa' }, + { code: 'rose', name: '蔷薇淑女', hair: 'long', hairColor: '#7e3348', skinTone: '#f6e2d0', vestColor: '#c96a8a', shirtColor: '#f4dce4' }, + { code: 'ember', name: '余烬铁匠', hair: 'spiky', hairColor: '#4a2118', skinTone: '#dcb890', vestColor: '#a84020', shirtColor: '#e2c9a8' }, + { code: 'sand', name: '沙丘旅人', hair: 'curly', hairColor: '#a8854e', skinTone: '#e0c09a', vestColor: '#c9a86a', shirtColor: '#f2e8d2' }, + { code: 'moss', name: '苔原德鲁伊', hair: 'curly', hairColor: '#4c5a2c', skinTone: '#e8d4b4', vestColor: '#6a7a3d', shirtColor: '#dce2c4' }, + { code: 'plum', name: '梅子贵妇', hair: 'long', hairColor: '#52284a', skinTone: '#f0dcc8', vestColor: '#8a4a7e', shirtColor: '#e8d4e4' }, + { code: 'rust', name: '铁锈机械师', hair: 'flat', hairColor: '#6a3520', skinTone: '#e4c4a0', vestColor: '#a05a2a', shirtColor: '#d8c2a8' }, + { code: 'navy', name: '藏青卫士', hair: 'flat', hairColor: '#1e2a44', skinTone: '#ecd4b8', vestColor: '#2c3e68', shirtColor: '#c9d2e4' }, + { code: 'blizzard', name: '暴雪求生者', hair: 'spiky', hairColor: '#dfe6f0', skinTone: '#f6ece0', vestColor: '#a8bcd0', shirtColor: '#f4f8fc' }, + { code: 'crow', name: '乌鸦信使', hair: 'long', hairColor: '#14121c', skinTone: '#e8d8c8', vestColor: '#2a2534', shirtColor: '#a8a2b4' }, + { code: 'lava', name: '熔岩勇士', hair: 'spiky', hairColor: '#6e1408', skinTone: '#e0b898', vestColor: '#c93a10', shirtColor: '#f2b03c' }, + { code: 'glacier', name: '冰川猎人', hair: 'flat', hairColor: '#b8d8e8', skinTone: '#f0e8dc', vestColor: '#7fb8d8', shirtColor: '#eef8fc' }, + { code: 'swamp', name: '沼泽行者', hair: 'curly', hairColor: '#2c3a20', skinTone: '#d8c4a0', vestColor: '#4a5c2e', shirtColor: '#a8b088' }, + { code: 'sunset', name: '落日吟游者', hair: 'long', hairColor: '#c95a2e', skinTone: '#f2dcc0', vestColor: '#e8823c', shirtColor: '#f8d8a8' }, + { code: 'thorn', name: '荆棘骑士', hair: 'spiky', hairColor: '#2a3318', skinTone: '#e8d0ac', vestColor: '#54682c', shirtColor: '#c9d0a8' }, + { code: 'shadow', name: '暗影刺客', hair: 'spiky', hairColor: '#0c0a12', skinTone: '#d8c8c0', vestColor: '#1e1828', shirtColor: '#3a3448' }, + { code: 'gold', name: '鎏金贵族', hair: 'curly', hairColor: '#c9a11e', skinTone: '#f4e4c8', vestColor: '#e0b83a', shirtColor: '#f8ecc2' }, + { code: 'blood', name: '血月狂战士', hair: 'long', hairColor: '#3a0a0e', skinTone: '#e0c0a8', vestColor: '#8e1c24', shirtColor: '#d8a8a0' }, + { code: 'void', name: '极夜术士', hair: 'flat', hairColor: '#08060e', skinTone: '#d0c4c8', vestColor: '#141020', shirtColor: '#2e2840' }, + { code: 'king', name: '饥荒之王', hair: 'crown', hairColor: '#2a1c10', skinTone: '#f0dcc0', vestColor: '#8e2c3a', shirtColor: '#f2e0b8' }, +] + +export function skinByCode(code) { + return SKINS.find((s) => s.code === code) || SKINS[0] +} + +// 通用描边填充 +function fs(c, fill, stroke = '#1d1409', lw = 2) { + if (fill) { c.fillStyle = fill; c.fill() } + if (stroke) { c.strokeStyle = stroke; c.lineWidth = lw; c.stroke() } +} + +// 发型绘制(以头心 (0, headY) 为基准,头半径 8.4) +function drawHair(c, skin, headY) { + c.fillStyle = skin.hairColor + if (skin.hair === 'spiky') { + // 威尔逊式尖刺 + c.beginPath() + c.moveTo(-8, headY - 2) + c.lineTo(-9, headY - 12) + c.lineTo(-4.5, headY - 6.5) + c.lineTo(-3, headY - 15) + c.lineTo(1, headY - 7) + c.lineTo(4, headY - 13) + c.lineTo(6.5, headY - 5.5) + c.lineTo(8.4, headY - 1) + c.quadraticCurveTo(4, headY - 7.5, -8, headY - 2) + c.closePath() + c.fill() + } else if (skin.hair === 'flat') { + // 平刘海盖头 + c.beginPath() + c.arc(0, headY - 1.5, 8.6, Math.PI, 0) + c.quadraticCurveTo(8, headY - 6, 5, headY - 5.5) + c.lineTo(-5, headY - 5.5) + c.quadraticCurveTo(-8, headY - 6, -8.6, headY - 1.5) + c.closePath() + c.fill() + c.strokeStyle = '#1d1409' + c.lineWidth = 1.2 + c.beginPath() + c.arc(0, headY - 1.5, 8.6, Math.PI * 1.05, -Math.PI * 0.05) + c.stroke() + } else if (skin.hair === 'long') { + // 披肩长发:顶盖 + 两侧垂发 + c.beginPath() + c.arc(0, headY - 2, 8.8, Math.PI, 0) + c.closePath() + c.fill() + c.beginPath() + c.moveTo(-8.6, headY - 2) + c.quadraticCurveTo(-10.5, headY + 8, -8, headY + 15) + c.lineTo(-4.5, headY + 13) + c.quadraticCurveTo(-7, headY + 5, -6.4, headY - 1) + c.closePath() + c.fill() + c.beginPath() + c.moveTo(8.6, headY - 2) + c.quadraticCurveTo(10.5, headY + 8, 8, headY + 15) + c.lineTo(4.5, headY + 13) + c.quadraticCurveTo(7, headY + 5, 6.4, headY - 1) + c.closePath() + c.fill() + } else if (skin.hair === 'curly') { + // 蓬卷:一圈圆团 + ;[[-6, headY - 6, 4.4], [0, headY - 9, 5], [6, headY - 6, 4.4], [-3, headY - 8.5, 4], [3, headY - 8.5, 4]].forEach(([hx, hy, r]) => { + c.beginPath() + c.arc(hx, hy, r, 0, 7) + c.fill() + }) + } else if (skin.hair === 'crown') { + // 短发 + 小金冠 + c.beginPath() + c.arc(0, headY - 2.5, 8.5, Math.PI * 1.05, -Math.PI * 0.05) + c.quadraticCurveTo(0, headY - 7, -8.2, headY - 3.6) + c.closePath() + c.fill() + c.beginPath() + c.moveTo(-5.5, headY - 9) + c.lineTo(-5.5, headY - 14) + c.lineTo(-2.8, headY - 10.5) + c.lineTo(0, headY - 15.5) + c.lineTo(2.8, headY - 10.5) + c.lineTo(5.5, headY - 14) + c.lineTo(5.5, headY - 9) + c.closePath() + fs(c, '#e8c14d', '#8e6a14', 1.4) + } +} + +// 手持工具(在前臂末端坐标系内绘制) +function drawTool(c, toolCode) { + if (!toolCode) return + c.save() + c.translate(0, 11) + c.rotate(-0.5) + if (toolCode === 'shovel') { + c.strokeStyle = '#7a5b33' + c.lineWidth = 3 + c.beginPath(); c.moveTo(0, 4); c.lineTo(0, -14); c.stroke() + c.beginPath() + c.moveTo(-4, -14) + c.quadraticCurveTo(0, -24, 4, -14) + c.closePath() + fs(c, '#9aa0a8', '#1d1409', 1.4) + } else if (toolCode === 'tentaclespike') { + c.strokeStyle = '#6b4a7e' + c.lineWidth = 3.4 + c.beginPath(); c.moveTo(0, 6); c.quadraticCurveTo(2, -8, 0, -20); c.stroke() + c.fillStyle = '#e8e2d4' + ;[[0, -8], [1, -14]].forEach(([x, y]) => { + c.beginPath(); c.moveTo(x, y); c.lineTo(x + 4, y - 1); c.lineTo(x + 1, y - 4); c.closePath(); c.fill() + }) + } else if (toolCode === 'torch') { + c.strokeStyle = '#7a5b33' + c.lineWidth = 3 + c.beginPath(); c.moveTo(0, 4); c.lineTo(0, -12); c.stroke() + c.beginPath() + c.moveTo(0, -20) + c.quadraticCurveTo(5, -13, 0, -10) + c.quadraticCurveTo(-5, -13, 0, -20) + fs(c, '#f0923c', '#b5541e', 1.2) + } else if (toolCode === 'axe') { + c.strokeStyle = '#7a5b33' + c.lineWidth = 3 + c.beginPath(); c.moveTo(0, 4); c.lineTo(0, -13); c.stroke() + c.beginPath() + c.moveTo(0, -13); c.lineTo(8, -10); c.lineTo(7, -4); c.lineTo(0, -7) + c.closePath() + fs(c, '#9aa0a8', '#1d1409', 1.4) + } else if (toolCode === 'pick') { + c.strokeStyle = '#7a5b33' + c.lineWidth = 3 + c.beginPath(); c.moveTo(0, 4); c.lineTo(0, -13); c.stroke() + c.beginPath() + c.moveTo(-7, -12) + c.quadraticCurveTo(0, -19, 7, -12) + c.quadraticCurveTo(0, -15, -7, -12) + fs(c, '#9aa0a8', '#1d1409', 1.4) + } else if (toolCode === 'spear') { + c.strokeStyle = '#7a5b33' + c.lineWidth = 2.8 + c.beginPath(); c.moveTo(0, 8); c.lineTo(0, -16); c.stroke() + c.beginPath() + c.moveTo(-2.6, -15); c.lineTo(0, -23); c.lineTo(2.6, -15) + c.closePath() + fs(c, '#b0b4bc', '#1d1409', 1.4) + } + c.restore() +} + +// 通用人物绘制:以脚底 (0,0) 为原点,头顶约 -32px +// pose = { dir, walking, walkT, swing, breathe, toolCode, ghost, time, act, fall } +// act: 动作分化(chop 横劈 / mine 上凿下砸 / dig 下铲 / attack 突刺 / eat 抬手进食),swing 1→0 驱动 +// fall: 死亡倒地进度 0→1(倒完后由调用方切换为幽灵形态) +export function drawPlayerFig(c, skin, pose = {}) { + const { dir = 1, walking = false, walkT = 0, swing = 0, breathe = 0, toolCode = '', ghost = false, time = 0, act = '', fall = 0 } = pose + c.save() + c.scale(dir, 1) + c.lineJoin = 'round' + c.lineCap = 'round' + if (!ghost && fall > 0) { + // 死亡倒地:绕脚底向后倒 + 渐隐 + c.rotate(fall * 1.4) + c.globalAlpha = 1 - fall * 0.35 + } + if (ghost) { + // 幽灵形态:半透明白影摇曳 + 黑点眼(复刻原版幽灵) + const wob = Math.sin(time * 2.4) * 1.6 + c.globalAlpha = 0.55 + c.beginPath() + c.moveTo(0, -30) + c.bezierCurveTo(12, -28, 13, -10, 10 + wob, 2) + c.quadraticCurveTo(6, 8, 2, 3) + c.quadraticCurveTo(0, 8, -2, 3) + c.quadraticCurveTo(-6, 8, -10 - wob, 2) + c.bezierCurveTo(-13, -10, -12, -28, 0, -30) + fs(c, '#e9ecf4', 'rgba(120,130,160,0.8)', 1.6) + c.fillStyle = '#2a2a3a' + c.beginPath() + c.arc(-3.4, -20, 1.6, 0, 7) + c.arc(3.4, -20, 1.6, 0, 7) + c.fill() + c.globalAlpha = 1 + c.restore() + return + } + const walkSwing = walking ? Math.sin(walkT * 11) : 0 + // 腿(深灰裤) + c.strokeStyle = '#2b2b33' + c.lineWidth = 4.4 + c.beginPath() + c.moveTo(-3, 2) + c.lineTo(-3 + walkSwing * 4.5, 15) + c.moveTo(3, 2) + c.lineTo(3 - walkSwing * 4.5, 15) + c.stroke() + // 鞋 + c.fillStyle = '#1d1409' + c.beginPath() + c.ellipse(-3 + walkSwing * 4.5, 16, 3.4, 2, 0, 0, 7) + c.ellipse(3 - walkSwing * 4.5, 16, 3.4, 2, 0, 0, 7) + c.fill() + // 身体:衬衫 + 两片马甲 + c.beginPath() + c.moveTo(-6, -14 + breathe) + c.lineTo(6, -14 + breathe) + c.lineTo(7, 4) + c.lineTo(-7, 4) + c.closePath() + fs(c, skin.shirtColor, '#1d1409', 1.8) + c.beginPath() + c.moveTo(-6.6, -14 + breathe) + c.lineTo(-2, -13 + breathe) + c.lineTo(-2.6, 4) + c.lineTo(-7, 4) + c.closePath() + fs(c, skin.vestColor, '#1d1409', 1.6) + c.beginPath() + c.moveTo(6.6, -14 + breathe) + c.lineTo(2, -13 + breathe) + c.lineTo(2.6, 4) + c.lineTo(7, 4) + c.closePath() + fs(c, skin.vestColor, '#1d1409', 1.6) + // 后臂(随走路摆) + c.strokeStyle = skin.shirtColor + c.lineWidth = 3.6 + c.beginPath() + c.moveTo(-5, -11 + breathe) + c.lineTo(-7 - walkSwing * 3, -1 + Math.abs(walkSwing)) + c.stroke() + // 头 + const headY = -22 + breathe + c.beginPath() + c.arc(0, headY, 8.4, 0, 7) + fs(c, skin.skinTone, '#1d1409', 1.8) + // 发型 + drawHair(c, skin, headY) + // 眼睛(竖点)+ 嘴 + c.strokeStyle = '#1d1409' + c.lineWidth = 1.8 + c.beginPath() + c.moveTo(2.4, headY - 2) + c.lineTo(2.4, headY + 0.6) + c.moveTo(6.4, headY - 2) + c.lineTo(6.4, headY + 0.6) + c.stroke() + c.lineWidth = 1.4 + c.beginPath() + c.arc(4.6, headY + 3.4, 2, 0.3, Math.PI - 0.5) + c.stroke() + // 前臂 + 手持工具:挥击动作由 swing(1→0) 驱动,act 决定挥法 + let swingAng = -0.35 + walkSwing * 0.32 + let thrust = 0 + if (swing > 0) { + const t = 1 - swing + if (act === 'mine' || act === 'dig') swingAng = -2.7 + t * 3.2 // 高举下砸/下铲 + else if (act === 'attack') { swingAng = -1.05; thrust = Math.sin(t * Math.PI) * 8 } // 平持突刺 + else if (act === 'eat') swingAng = -0.4 - Math.sin(t * Math.PI) * 1.95 // 抬手到嘴边 + else swingAng = -2.1 + t * 2.7 // 砍树/通用横劈 + } + c.save() + c.translate(5 + thrust, -11 + breathe) + c.rotate(swingAng) + c.strokeStyle = skin.shirtColor + c.lineWidth = 3.6 + c.beginPath() + c.moveTo(0, 0) + c.lineTo(0, 11) + c.stroke() + drawTool(c, toolCode) + c.restore() + c.restore() +} + +// 离屏渲染 72px 立绘 dataURL(更衣室/房间座位预览用,带缓存) +const previewCache = {} +export function renderSkinPreview(code, size = 72) { + const key = code + '@' + size + if (previewCache[key]) return previewCache[key] + const skin = skinByCode(code) + const cv = document.createElement('canvas') + cv.width = size + cv.height = size + const c = cv.getContext('2d') + c.translate(size / 2, size * 0.86) + const scale = size / 52 + c.scale(scale, scale) + drawPlayerFig(c, skin, { dir: 1 }) + const url = cv.toDataURL() + previewCache[key] = url + return url +} diff --git a/src/layouts/Header.vue b/src/layouts/Header.vue deleted file mode 100644 index 5dfaf00..0000000 --- a/src/layouts/Header.vue +++ /dev/null @@ -1,32 +0,0 @@ - - - - - diff --git a/src/main.js b/src/main.js index 4f9904b..c89ef7c 100644 --- a/src/main.js +++ b/src/main.js @@ -1,17 +1,20 @@ +// 前端入口:挂载 Pinia、路由与全局样式,启动前先拉取站点配置(主题) import { createApp } from 'vue' import { createPinia } from 'pinia' -import Antd from 'ant-design-vue' -import './style.css' import App from './App.vue' -import '@fortawesome/fontawesome-free/css/all.css' // 引入所有样式 -// 注册路由 import router from './router' +import { useThemeStore } from './stores/theme' +// 主题专用字体:像素字(街机主题)与终端字(掌机/赛博主题),本地打包无需外网 +import '@fontsource/press-start-2p' +import '@fontsource/vt323' +import './styles/base.css' +import './styles/themes.css' const app = createApp(App) - - -// 注册Ant Design组件 app.use(createPinia()) app.use(router) -app.use(Antd) -app.mount('#app') +// 先应用主题再挂载,避免首屏闪烁默认色 +const themeStore = useThemeStore() +themeStore.fetchConfig().finally(() => { + app.mount('#app') +}) diff --git a/src/router/index.js b/src/router/index.js index 796771d..5bd704d 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -1,38 +1,64 @@ -import { createRouter, createWebHistory } from 'vue-router' -import { useUserStore } from '@/stores/user' -import MainLayout from '@/layouts/Header.vue' +// 路由定义:登录页外全部需要登录,后台路由需要超管角色 +import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router' -// 在路由配置中添加个人中心路由 const routes = [ + { path: '/login', name: 'login', component: () => import('../views/Login.vue'), meta: { bare: true } }, + { path: '/', name: 'lobby', component: () => import('../views/Lobby.vue') }, + { path: '/center', name: 'center', component: () => import('../views/GameCenter.vue') }, + { path: '/signin', name: 'signin', component: () => import('../views/SignIn.vue') }, + { path: '/vip', name: 'vip', component: () => import('../views/Vip.vue') }, + { path: '/rank', name: 'rank', component: () => import('../views/Rank.vue') }, + { path: '/profile', name: 'profile', component: () => import('../views/Profile.vue') }, + { path: '/play/:code', name: 'play', component: () => import('../views/GamePlay.vue') }, + { path: '/battle', name: 'battle', component: () => import('../views/Battle.vue') }, + { path: '/battle/room', name: 'battleRoom', component: () => import('../views/BattleRoom.vue') }, + { path: '/chat', name: 'chat', component: () => import('../views/Chat.vue') }, { - path: '/', - component: MainLayout, + path: '/admin', + component: () => import('../views/admin/AdminLayout.vue'), + meta: { admin: true }, children: [ - { path: '', name: 'Home', component: () => import('@/views/index/index.vue') }, - ] + { path: '', name: 'adminDashboard', component: () => import('../views/admin/AdminDashboard.vue') }, + { path: 'themes', name: 'adminThemes', component: () => import('../views/admin/AdminThemes.vue') }, + { path: 'users', name: 'adminUsers', component: () => import('../views/admin/AdminUsers.vue') }, + { path: 'games', name: 'adminGames', component: () => import('../views/admin/AdminGames.vue') }, + { path: 'orders', name: 'adminOrders', component: () => import('../views/admin/AdminOrders.vue') }, + { path: 'vip', name: 'adminVip', component: () => import('../views/admin/AdminVip.vue') }, + { path: 'ai', name: 'adminAI', component: () => import('../views/admin/AdminAI.vue') }, + { path: 'version', name: 'adminVersion', component: () => import('../views/admin/AdminVersion.vue') }, + ], }, + { path: '/:pathMatch(.*)*', redirect: '/' }, ] const router = createRouter({ - history: createWebHistory(), + // Electron 桌面端以 file:// 加载,history 模式无法工作,改用 hash 模式;浏览器保持 history + history: window.desktop ? createWebHashHistory() : createWebHistory(), routes, - scrollBehavior(to, from, savedPosition) { - return savedPosition || { top: 0 } +}) + +// 全局守卫:未登录跳登录页,非超管禁入后台 +router.beforeEach((to, from) => { + const token = localStorage.getItem('token') + if (!token && !to.meta.bare) return { name: 'login' } + if (token && to.name === 'login') return { name: 'lobby' } + if (to.matched.some((r) => r.meta.admin)) { + const user = JSON.parse(localStorage.getItem('user') || 'null') + if (user?.role !== 1) return { name: 'lobby' } + // 后台菜单记忆:从站外进入后台根路径时,自动回到上次停留的后台页面 + // (在后台内部点「数据概览」不触发,保证概览页仍可正常访问) + const lastPath = localStorage.getItem('admin_last_path') + if (to.path === '/admin' && !from.path.startsWith('/admin') && lastPath && lastPath !== '/admin') { + return lastPath + } } }) -router.beforeEach((to, from) => { - const userStore = useUserStore() - if (to.meta.requiresAuth && !userStore.user) { - return { name: 'Login', query: { redirect: to.fullPath } } +// 记录最后停留的后台页面(刷新/下次进入后台时恢复) +router.afterEach((to) => { + if (to.path.startsWith('/admin')) { + localStorage.setItem('admin_last_path', to.path) } }) export default router - -router.beforeEach((to, from) => { - const userStore = useUserStore() - if (to.meta.requiresAuth && !userStore.user) { - return { name: 'Login', query: { redirect: to.fullPath } } - } -}) diff --git a/src/stores/battle.js b/src/stores/battle.js new file mode 100644 index 0000000..0c1a6ef --- /dev/null +++ b/src/stores/battle.js @@ -0,0 +1,174 @@ +// 对战 Store:管理 WebSocket 连接、房间状态、聊天记录与结算信息 +// 该连接同时承载私聊/好友的实时推送(转发给 chat store 处理) +import { defineStore } from 'pinia' +import { toast } from '../api/http' +import { useChatStore } from './chat' + +export const useBattleStore = defineStore('battle', { + state: () => ({ + ws: null, // WebSocket 实例 + connected: false, // 连接状态 + roomState: null, // 服务端下发的房间状态快照(个人视角) + chatLog: [], // 聊天与系统消息列表 + battleEnd: null, // 最近一次结算信息(弹窗展示) + closedReason: '', // 房间被解散的原因 + pingTimer: null, // 应用层心跳定时器 + billAnims: [], // 台球回放动画队列(服务端每杆推送,组件依次播放) + ddzHint: null, // 斗地主出牌提示结果 {cards|suggest_pass|none, t} + chessHints: null, // 象棋走法提示 {from_r, from_c, targets:[{r,c,capture}]} + pendingInvite: null, // 收到的好友房间邀请 {code,game,game_name,host_id,host_name,host_avatar,t} + }), + getters: { + // 是否已在房间内 + inRoom: (s) => !!s.roomState, + // 我的座位号 + mySeat: (s) => s.roomState?.my_seat ?? -1, + }, + actions: { + // 建立连接(带 JWT),断线自动提示;返回 Promise 在连接成功后 resolve + connect() { + if (this.ws && (this.ws.readyState === 0 || this.ws.readyState === 1)) { + return Promise.resolve() + } + const token = localStorage.getItem('token') + // WS 地址:桌面端从配置的服务器地址推导(file:// 下 location.host 为空),浏览器走同源 + let host = location.host + let proto = location.protocol === 'https:' ? 'wss' : 'ws' + const base = window.desktop?.serverBase + if (base) { + const u = new URL(base) + host = u.host + proto = u.protocol === 'https:' ? 'wss' : 'ws' + } + const url = `${proto}://${host}/ws?token=${encodeURIComponent(token)}` + return new Promise((resolve, reject) => { + const ws = new WebSocket(url) + this.ws = ws + ws.onopen = () => { + this.connected = true + // 应用层心跳:30 秒一次防止代理断连 + this.pingTimer = setInterval(() => this.send('ping', {}), 30000) + resolve() + } + ws.onmessage = (ev) => this.handleMessage(JSON.parse(ev.data)) + ws.onclose = () => { + this.connected = false + clearInterval(this.pingTimer) + } + ws.onerror = () => { + reject(new Error('连接失败')) + } + }) + }, + // 处理服务端消息 + handleMessage(msg) { + const { type, data } = msg + switch (type) { + case 'room_state': + this.roomState = data + break + case 'chat': + this.chatLog.push(data) + // 聊天记录只保留最近 100 条 + if (this.chatLog.length > 100) this.chatLog.shift() + break + case 'battle_end': + this.battleEnd = data + break + case 'bill_anim': + // 台球击球回放:入队,由台球组件按顺序消费播放 + this.billAnims.push(data) + break + case 'ddz_hint': + // 斗地主提示:带时间戳保证连续两次相同结果也能触发侦听 + this.ddzHint = { ...data, t: Date.now() } + break + case 'chess_hints': + // 象棋走法提示:选中棋子的全部合法落点 + this.chessHints = data + break + case 'room_closed': + this.closedReason = data.reason || '房间已解散' + this.roomState = null + toast(this.closedReason, 'info') + break + case 'chat_msg': + // 私聊消息:交给聊天 store(未读数/会话/弹提示) + useChatStore().onIncoming(data) + break + case 'friend_event': + // 好友申请/通过事件 + useChatStore().onFriendEvent(data) + break + case 'room_invite': + // 好友房间邀请:带时间戳触发全局弹窗(GamePlay/BattleRoom 之外任何页面都能收到) + this.pendingInvite = { ...data, t: Date.now() } + break + case 'error': + toast(data.msg || '操作失败') + break + } + }, + // 发送消息(统一信封格式) + send(type, data = {}) { + if (this.ws?.readyState === 1) { + this.ws.send(JSON.stringify({ type, data })) + } + }, + // ---- 房间操作 ---- + createRoom(opts) { this.send('create_room', opts) }, + joinRoom(code) { this.send('join_room', { code }) }, + leaveRoom() { + this.send('leave_room') + this.reset() + }, + setReady(ready) { this.send('ready', { ready }) }, + startGame() { this.send('start') }, + sendChat(text) { this.send('chat', { text }) }, + // ---- 斗地主操作 ---- + ddzBid(call) { this.send('ddz_bid', { call }) }, + ddzRob(rob) { this.send('ddz_rob', { rob }) }, + ddzPlay(cards) { this.send('ddz_play', { cards }) }, + ddzPass() { this.send('ddz_pass') }, + // 请求出牌提示:seq 从 0 递增,连点轮换候选 + ddzHintReq(seq) { this.send('ddz_hint', { seq }) }, + // 托管开关:开启后每回合由系统规则 AI 代打 + setEscrow(on) { this.send('escrow', { on }) }, + // ---- 象棋操作 ---- + chessMove(m) { this.send('chess_move', m) }, + // 请求选中棋子的合法落点(新手走法高亮) + chessHintsReq(fromR, fromC) { this.send('chess_hints', { from_r: fromR, from_c: fromC }) }, + // ---- 大富翁操作 ---- + monoRoll() { this.send('mono_roll') }, + monoBuy(buy) { this.send('mono_buy', { buy }) }, + // 建筑升级决策(落在自己地产上时) + monoUpgrade(up) { this.send('mono_upgrade', { up }) }, + // ---- 飞行棋操作 ---- + ludoRoll() { this.send('ludo_roll') }, + ludoMove(plane) { this.send('ludo_move', { plane }) }, + // ---- 台球操作 ---- + // spinX 左右塞 -1~1(右塞为正),spinY 高低杆 -1~1(高杆为正) + billShot(angle, power, spinX = 0, spinY = 0) { + this.send('bill_shot', { angle, power, spin_x: spinX, spin_y: spinY }) + }, + resign() { this.send('resign') }, + // 清空本地状态(离开房间/退出登录) + reset() { + this.roomState = null + this.chatLog = [] + this.battleEnd = null + this.billAnims = [] + this.ddzHint = null + this.chessHints = null + }, + // 彻底断开 + disconnect() { + // 主动断开也要清心跳:close 是异步的,不能只依赖 onclose 回调兜底 + clearInterval(this.pingTimer) + this.pingTimer = null + this.ws?.close() + this.ws = null + this.reset() + }, + }, +}) diff --git a/src/stores/chat.js b/src/stores/chat.js new file mode 100644 index 0000000..bfd7eab --- /dev/null +++ b/src/stores/chat.js @@ -0,0 +1,180 @@ +// 聊天 Store:好友关系、私聊会话、未读数与实时推送处理 +// 实时通道复用对战 WebSocket(battle store),离线消息靠数据库兜底 +import { defineStore } from 'pinia' +import http, { toast } from '../api/http' +import { useBattleStore } from './battle' + +export const useChatStore = defineStore('chat', { + state: () => ({ + unread: 0, // 未读私聊总数(导航/悬浮按钮角标) + conversations: [], // 会话列表 [{peer,last_msg,last_at,from_me,unread}] + friends: [], // 好友列表 [{user_id,nickname,avatar,online,since}] + requests: { received: [], sent: [] }, // 好友申请(收到的/发出的) + reqBadge: 0, // 待处理申请数(红点) + activePeer: null, // 当前聊天对象(userBrief) + messages: [], // 当前会话消息(时间升序) + hasMore: false, // 是否还有更早的消息 + widgetOpen: false, // 悬浮小窗是否展开 + widgetView: 'list', // 小窗视图:list=会话列表 chat=聊天 + inited: false, // 是否已完成初始化 + }), + actions: { + // 登录后初始化:建立 WS(收实时消息)+ 拉未读数与申请红点 + async init() { + if (this.inited) return + this.inited = true + try { + await useBattleStore().connect() + } catch { + // 后端未启动时静默,进入对战页等场景会再次尝试 + } + this.refreshUnread() + this.refreshRequests() + }, + async refreshUnread() { + try { + const d = await http.get('/chat/unread') + this.unread = d.unread + } catch {} + }, + async loadConversations() { + const d = await http.get('/chat/conversations') + this.conversations = d.list + this.unread = d.total_unread + }, + async loadFriends() { + this.friends = await http.get('/friends') + }, + async refreshRequests() { + try { + const d = await http.get('/friends/requests') + this.requests = d + this.reqBadge = d.received.length + } catch {} + }, + // 打开与某人的聊天(peer 至少含 user_id/nickname/avatar) + async openChat(peer) { + this.activePeer = peer + this.messages = [] + this.hasMore = false + const d = await http.get('/chat/messages', { params: { peer_id: peer.user_id, size: 50 } }) + this.messages = d.list + this.hasMore = d.has_more + if (d.peer?.user_id) this.activePeer = d.peer + // 本地清零该会话未读 + const conv = this.conversations.find((x) => x.peer.user_id === peer.user_id) + if (conv && conv.unread > 0) { + this.unread = Math.max(0, this.unread - conv.unread) + conv.unread = 0 + } else { + this.refreshUnread() + } + }, + // 加载更早的消息(向上翻页) + async loadEarlier() { + if (!this.activePeer || !this.messages.length) return + const d = await http.get('/chat/messages', { + params: { peer_id: this.activePeer.user_id, before_id: this.messages[0].id, size: 50 }, + }) + this.messages = [...d.list, ...this.messages] + this.hasMore = d.has_more + }, + // 发送消息 + async sendMsg(text) { + if (!this.activePeer) return + const msg = await http.post('/chat/send', { to_id: this.activePeer.user_id, content: text }) + this.messages.push(msg) + this.bumpConversation(this.activePeer, msg, true) + }, + // 当前聊天窗是否对某人打开(悬浮小窗或聊天室页面;hash 判断兼容桌面端 hash 路由) + chatOpenWith(uid) { + if (!this.activePeer || this.activePeer.user_id !== uid) return false + const onChatPage = location.pathname.startsWith('/chat') || location.hash.startsWith('#/chat') + return (this.widgetOpen && this.widgetView === 'chat') || onChatPage + }, + // 收到 WS 推送的私聊消息 + onIncoming(data) { + const { msg, from } = data + if (this.chatOpenWith(from.user_id)) { + this.messages.push(msg) + http.post('/chat/read', { peer_id: from.user_id }).catch(() => {}) + } else { + this.unread++ + toast(`💬 ${from.nickname}:${msg.content.slice(0, 30)}`, 'info') + } + this.bumpConversation(from, msg, false) + }, + // 收到好友事件推送(申请/通过) + onFriendEvent(data) { + if (data.kind === 'request') { + toast(`👥 ${data.user.nickname} 申请加你为好友`, 'info') + this.refreshRequests() + } else if (data.kind === 'accept') { + toast(`🎉 ${data.user.nickname} 通过了你的好友申请`, 'success') + this.loadFriends().catch(() => {}) + this.refreshRequests() + } + }, + // 更新会话列表的最后一条消息并置顶 + bumpConversation(peer, msg, fromMe) { + const i = this.conversations.findIndex((x) => x.peer.user_id === peer.user_id) + if (i >= 0) { + const conv = this.conversations.splice(i, 1)[0] + conv.last_msg = msg.content + conv.last_at = msg.created_at + conv.from_me = fromMe + if (!fromMe && !this.chatOpenWith(peer.user_id)) conv.unread++ + this.conversations.unshift(conv) + } else { + this.conversations.unshift({ + peer, last_msg: msg.content, last_at: msg.created_at, from_me: fromMe, + unread: fromMe || this.chatOpenWith(peer.user_id) ? 0 : 1, + }) + } + }, + // ---- 好友操作 ---- + async searchUsers(keyword) { + return await http.get('/friends/search', { params: { keyword } }) + }, + async requestFriend(userId) { + const d = await http.post('/friends/request', { user_id: userId }) + if (d.relation === 'friend') { + toast(d.msg || '已成为好友', 'success') + this.loadFriends().catch(() => {}) + } else { + toast('申请已发送,等待对方同意', 'success') + } + this.refreshRequests() + return d.relation + }, + async respondFriend(id, accept) { + await http.post('/friends/respond', { id, accept }) + toast(accept ? '已同意,你们现在是好友了' : '已拒绝', accept ? 'success' : 'info') + this.refreshRequests() + if (accept) this.loadFriends().catch(() => {}) + }, + async removeFriend(uid) { + await http.delete(`/friends/${uid}`) + toast('已删除好友', 'info') + this.friends = this.friends.filter((f) => f.user_id !== uid) + }, + // 悬浮小窗控制(打开时顺便确保 WS 在线,断线后能自动恢复实时推送) + openWidget() { + this.widgetOpen = true + this.widgetView = 'list' + useBattleStore().connect().catch(() => {}) + this.loadConversations().catch(() => {}) + this.loadFriends().catch(() => {}) + }, + // 在小窗里直接跟某人聊 + async widgetChat(peer) { + this.widgetOpen = true + this.widgetView = 'chat' + await this.openChat(peer) + }, + // 退出登录时清空 + reset() { + this.$reset() + }, + }, +}) diff --git a/src/stores/starveCoop.js b/src/stores/starveCoop.js new file mode 100644 index 0000000..16c8575 --- /dev/null +++ b/src/stores/starveCoop.js @@ -0,0 +1,126 @@ +// 饥荒组队联机 Store:独立 WebSocket 连接 + 房间状态(建房/邀请码/座位/皮肤/准备) +// 对局内实时消息(starve_input/starve_state/starve_end)直接转发给游戏组件注册的回调, +// 不落 pinia 响应式状态(高频快照,避免无谓的响应式开销) +import { defineStore } from 'pinia' +import { toast } from '../api/http' + +// 游戏内消息回调(模块级,非响应式):StarveGame 挂载对局时注册 +let gameHandler = null + +export const useStarveCoopStore = defineStore('starveCoop', { + state: () => ({ + ws: null, // WebSocket 实例(独立于对战大厅的连接) + connected: false, // 连接状态 + roomState: null, // 房间状态快照(座位/皮肤/准备/状态) + closedReason: '', // 房间被解散原因 + pingTimer: null, // 应用层心跳 + }), + getters: { + inRoom: (s) => !!s.roomState, + mySeat: (s) => s.roomState?.my_seat ?? -1, + isHost: (s) => s.roomState && s.roomState.host_id === s.roomState.seats?.[s.roomState.my_seat]?.user_id, + // 已入座的玩家列表(联机开局用) + activePlayers: (s) => + (s.roomState?.seats || []) + .filter((seat) => seat.occupied) + .map((seat) => ({ seat: seat.index, name: seat.name, skin: seat.skin || 'wilson', userId: seat.user_id })), + }, + actions: { + // 建立独立连接;带 JWT,成功后 resolve + connect() { + if (this.ws && (this.ws.readyState === 0 || this.ws.readyState === 1)) { + return Promise.resolve() + } + const token = localStorage.getItem('token') + let host = location.host + let proto = location.protocol === 'https:' ? 'wss' : 'ws' + const base = window.desktop?.serverBase + if (base) { + const u = new URL(base) + host = u.host + proto = u.protocol === 'https:' ? 'wss' : 'ws' + } + const url = `${proto}://${host}/ws?token=${encodeURIComponent(token)}` + return new Promise((resolve, reject) => { + const ws = new WebSocket(url) + this.ws = ws + ws.onopen = () => { + this.connected = true + this.pingTimer = setInterval(() => this.send('ping', {}), 30000) + resolve() + } + ws.onmessage = (ev) => this.handleMessage(JSON.parse(ev.data)) + ws.onclose = () => { + // 主动 disconnect 时 this.ws 已被置 null,这里只处理"意外断线": + // 后端断线即移除座位,客户端必须同步清掉房间状态,否则 UI 仍显示在房间内 + if (this.ws !== ws) return + this.connected = false + clearInterval(this.pingTimer) + if (this.roomState) { + this.roomState = null + gameHandler?.('room_closed', { reason: '连接已断开' }) + toast('连接已断开,请重新建房或加入', 'info') + } + } + ws.onerror = () => reject(new Error('连接失败')) + }) + }, + handleMessage(msg) { + const { type, data } = msg + switch (type) { + case 'room_state': + this.roomState = data + gameHandler?.(type, data) + break + case 'starve_input': + case 'starve_state': + case 'starve_end': + // 对局内实时消息:直接交给游戏组件(主机收 input,客机收 state,全员收 end) + gameHandler?.(type, data) + break + case 'room_closed': + this.closedReason = data.reason || '房间已解散' + this.roomState = null + gameHandler?.('room_closed', data) + toast(this.closedReason, 'info') + break + case 'error': + toast(data.msg || '操作失败') + break + // chat_msg / friend_event 由主连接(battle store)负责,这里忽略避免重复处理 + } + }, + send(type, data = {}) { + if (this.ws?.readyState === 1) { + this.ws.send(JSON.stringify({ type, data })) + } + }, + // ---- 房间操作 ---- + createRoom() { this.send('create_room', { game: 'starve', mode: 'pvp' }) }, + joinRoom(code) { this.send('join_room', { code: String(code || '').trim().toUpperCase() }) }, + leaveRoom() { + this.send('leave_room') + this.roomState = null + }, + setReady(ready) { this.send('ready', { ready }) }, + setSkin(code) { this.send('skin', { skin: code }) }, + startGame() { this.send('start') }, + // ---- 对局内消息 ---- + sendInput(data) { this.send('starve_input', data) }, // 客机 → 房主(服务端转发) + sendState(data) { this.send('starve_state', data) }, // 房主 → 其余座位(服务端广播) + sendOver() { this.send('starve_over') }, // 房主宣告全灭结束 + // 游戏组件注册/注销对局消息回调 + setGameHandler(cb) { gameHandler = cb }, + clearGameHandler() { gameHandler = null }, + // 断开连接(退出游玩页时调用) + disconnect() { + clearInterval(this.pingTimer) + const ws = this.ws + this.ws = null // 先置空再 close,onclose 回调据此识别"主动断开"并跳过断线处理 + ws?.close() + this.connected = false + this.roomState = null + gameHandler = null + }, + }, +}) diff --git a/src/stores/theme.js b/src/stores/theme.js new file mode 100644 index 0000000..844098c --- /dev/null +++ b/src/stores/theme.js @@ -0,0 +1,46 @@ +// 主题与站点配置 Store:拉取 /api/config,把 CSS 变量写入 :root 并给 body 挂主题 class +import { defineStore } from 'pinia' +import http from '../api/http' + +export const useThemeStore = defineStore('theme', { + state: () => ({ + siteName: '像素游戏厅', // 站点名称 + announcement: '', // 公告 + themeCode: 'arcade', // 当前主题编码 + themeName: '电玩厅', // 当前主题名称 + carouselStyle: 'slide', // 大厅轮播效果:slide/fade/coverflow/cards + carouselInterval: 5, // 自动轮播间隔(秒) + }), + actions: { + // 拉取站点配置并应用主题(登录前后、后台切换后都会调用) + async fetchConfig() { + try { + const data = await http.get('/config') + this.siteName = data.site_name + this.announcement = data.announcement + this.carouselStyle = data.carousel_style || 'slide' + this.carouselInterval = data.carousel_interval || 5 + this.applyTheme(data.theme) + document.title = this.siteName + } catch (e) { + // 后端未启动时保持默认主题,不阻塞页面 + } + }, + // 把主题 CSS 变量写入根节点,并切换 body 的 theme-xxx class(装饰背景用) + applyTheme(theme) { + if (!theme) return + this.themeCode = theme.code + this.themeName = theme.name + const root = document.documentElement + Object.entries(theme.css_vars || {}).forEach(([key, value]) => { + root.style.setProperty(key, value) + }) + document.body.className = document.body.className + .split(' ') + .filter((c) => !c.startsWith('theme-')) + .concat(`theme-${theme.code}`) + .join(' ') + .trim() + }, + }, +}) diff --git a/src/stores/user.js b/src/stores/user.js index 5987aad..024459f 100644 --- a/src/stores/user.js +++ b/src/stores/user.js @@ -1,87 +1,50 @@ +// 用户 Store:登录态、积分余额(游戏结算后实时刷新) import { defineStore } from 'pinia' -import { ref, computed } from 'vue' -import {loginApi, registerApi} from "@/api/request/user.js"; -import {message, notification} from "ant-design-vue"; +import http from '../api/http' -export const useUserStore = defineStore('user', () => { - const user = ref(JSON.parse(localStorage.getItem('user') || 'null')) - const modal = ref(localStorage.getItem('authModal') || 'false') - - // 添加初始化状态校验 - const initUser = () => { - const userData = localStorage.getItem('user') - if (!userData) return null - try { - return JSON.parse(userData) - } catch { - localStorage.removeItem('user') +export const useUserStore = defineStore('user', { + state: () => ({ + token: localStorage.getItem('token') || '', + user: JSON.parse(localStorage.getItem('user') || 'null'), + }), + getters: { + // 是否已登录 + loggedIn: (s) => !!s.token, + // 是否超级管理员 + isAdmin: (s) => s.user?.role === 1, + }, + actions: { + // 保存登录结果 + setLogin({ token, user }) { + this.token = token + this.user = user + localStorage.setItem('token', token) + localStorage.setItem('user', JSON.stringify(user)) + }, + // 更新用户信息(积分变化、改资料后) + setUser(user) { + this.user = user + localStorage.setItem('user', JSON.stringify(user)) + }, + // 只更新积分余额(结算响应里带了最新余额) + setPoints(points) { + if (this.user) { + this.user.points = points + localStorage.setItem('user', JSON.stringify(this.user)) + } + }, + // 从服务端刷新最新资料 + async refresh() { + if (!this.token) return + const user = await http.get('/user/profile') + this.setUser(user) + }, + // 退出登录 + logout() { + this.token = '' + this.user = null localStorage.removeItem('token') - return null - } - } - - const isAuthenticated = computed(() => !!user.value?.nick_name) - - function login(credentials, functionName) { - console.log(functionName, 'sssssssssssss') - loginApi({ - account: credentials.username, - password: credentials.password, - remember: credentials.remember, - }).then((res) => { - const userData = { - nick_name: res.nick_name, - role_name: res.role_name, - role_value: res.role_value, - } - user.value = userData; - localStorage.setItem('user', JSON.stringify(userData)) - localStorage.setItem('token', res.token) - notification.success({ - message: '登录成功', - description: `欢迎回来${res.nick_name}`, - }) - functionName() - }) - } - function register(credentials, functionName) { - registerApi({ - account: credentials.username, - password: credentials.password, - email: credentials.email, - code: credentials.code, - }).then((res) => { - const userData = { - nick_name: res.nick_name, - role_name: res.role_name, - role_value: res.role_value, - } - user.value = userData; - localStorage.setItem('user', JSON.stringify(userData)) - localStorage.setItem('token', res.token) - message.success('注册成功') - functionName() - }) - } - - function logout() { - user.value = null - localStorage.removeItem('user') - localStorage.removeItem('token') - } - - function setModal(value) { - modal.value = modal.value === 'false' ? 'true' : 'false'; - localStorage.setItem('authModal', value) - } - - return { - modal, - user, - isAuthenticated, - setModal, - login, - register, - logout - } -}) \ No newline at end of file + localStorage.removeItem('user') + }, + }, +}) diff --git a/src/styles/base.css b/src/styles/base.css new file mode 100644 index 0000000..3ee37af --- /dev/null +++ b/src/styles/base.css @@ -0,0 +1,262 @@ +/* ===================================================================== + 全局基础样式:所有颜色/圆角/光效均来自主题 CSS 变量(后台切换主题全站生效) + ===================================================================== */ +:root { + /* 默认主题变量(等待 /api/config 下发覆盖时的兜底值,与电玩厅主题一致) */ + --bg: #0d0e2b; + --bg-panel: #16173d; + --bg-card: #1d1f4e; + --primary: #ff3e7f; + --primary-2: #ffb300; + --accent: #00e5ff; + --text: #ffffff; + --text-dim: #9a9ac4; + --border: #2c2e6e; + --radius: 0px; + --glow: 0 0 18px rgba(255, 62, 127, 0.45); +} +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} +html, +body { + min-height: 100%; +} +body { + background: var(--bg); + color: var(--text); + font-family: 'PingFang SC', 'Microsoft YaHei', 'Segoe UI', sans-serif; + font-size: 14px; + transition: background 0.4s ease, color 0.4s ease; +} +a { + color: inherit; + text-decoration: none; +} +button { + font-family: inherit; +} +/* ---------- 通用面板与卡片 ---------- */ +.panel { + background: var(--bg-panel); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 18px; +} +.card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius); + transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s; +} +.card:hover { + transform: translateY(-3px); + box-shadow: var(--glow); + border-color: var(--primary); +} +/* ---------- 按钮 ---------- */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + border: none; + cursor: pointer; + border-radius: calc(var(--radius) / 1.6); + padding: 9px 18px; + font-size: 14px; + font-weight: 600; + color: #fff; + background: linear-gradient(135deg, var(--primary), var(--primary-2)); + transition: filter 0.15s, transform 0.1s, opacity 0.15s; +} +.btn:hover { + filter: brightness(1.12); +} +.btn:active { + transform: scale(0.97); +} +.btn:disabled { + opacity: 0.45; + cursor: not-allowed; +} +.btn-ghost { + background: transparent; + border: 1px solid var(--border); + color: var(--text); +} +.btn-ghost:hover { + border-color: var(--primary); + color: var(--primary); +} +.btn-accent { + background: linear-gradient(135deg, var(--accent), var(--primary-2)); +} +.btn-sm { + padding: 5px 12px; + font-size: 12px; +} +.btn-lg { + padding: 12px 30px; + font-size: 16px; +} +/* ---------- 表单 ---------- */ +.input { + width: 100%; + background: var(--bg); + border: 1px solid var(--border); + border-radius: calc(var(--radius) / 1.6); + color: var(--text); + padding: 10px 12px; + font-size: 14px; + outline: none; + transition: border-color 0.15s, box-shadow 0.15s; +} +.input:focus { + border-color: var(--primary); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 25%, transparent); +} +select.input { + appearance: auto; +} +/* ---------- 标题与文字 ---------- */ +.page-title { + font-size: 22px; + font-weight: 800; + margin-bottom: 16px; + display: flex; + align-items: center; + gap: 8px; +} +.page-title::before { + content: ''; + width: 6px; + height: 22px; + border-radius: 4px; + background: linear-gradient(180deg, var(--primary), var(--primary-2)); + box-shadow: var(--glow); +} +.text-dim { + color: var(--text-dim); +} +.text-primary { + color: var(--primary); +} +.text-accent { + color: var(--accent); +} +/* ---------- 标签徽章 ---------- */ +.tag { + display: inline-block; + padding: 2px 10px; + border-radius: 999px; + font-size: 12px; + border: 1px solid var(--border); + color: var(--text-dim); + background: var(--bg); +} +.tag-primary { + color: var(--primary); + border-color: var(--primary); +} +.tag-accent { + color: var(--accent); + border-color: var(--accent); +} +/* ---------- 表格 ---------- */ +.table { + width: 100%; + border-collapse: collapse; +} +.table th, +.table td { + text-align: left; + padding: 10px 12px; + border-bottom: 1px solid var(--border); + font-size: 13px; +} +.table th { + color: var(--text-dim); + font-weight: 600; + white-space: nowrap; +} +.table tr:last-child td { + border-bottom: none; +} +/* ---------- 全局提示 toast ---------- */ +.toast { + position: fixed; + top: 76px; + right: 20px; + z-index: 9999; + padding: 10px 18px; + border-radius: 10px; + font-size: 14px; + color: #fff; + background: #e5484d; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35); + opacity: 0; + transform: translateX(24px); + transition: all 0.3s ease; + max-width: 320px; +} +.toast.show { + opacity: 1; + transform: translateX(0); +} +.toast-success { + background: #2f9e6e; +} +.toast-info { + background: #3b82f6; +} +/* ---------- 弹窗遮罩 ---------- */ +.modal-mask { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(3px); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} +.modal { + background: var(--bg-panel); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 24px; + width: min(480px, 92vw); + max-height: 86vh; + overflow: auto; + box-shadow: var(--glow); +} +/* ---------- 分页 ---------- */ +.pager { + display: flex; + gap: 8px; + align-items: center; + justify-content: center; + margin-top: 14px; +} +/* ---------- 骨架/空状态 ---------- */ +.empty { + text-align: center; + color: var(--text-dim); + padding: 40px 0; + font-size: 13px; +} +/* ---------- 滚动条 ---------- */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} +::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 4px; +} +::-webkit-scrollbar-thumb:hover { + background: var(--primary); +} diff --git a/src/styles/themes.css b/src/styles/themes.css new file mode 100644 index 0000000..406fdf6 --- /dev/null +++ b/src/styles/themes.css @@ -0,0 +1,1967 @@ +/* ===================================================================== + 主题风格系统:每套主题是一种完整的设计语言,而不是换色板。 + 颜色令牌来自后台下发的 CSS 变量(themes.css_vars), + 这里负责结构性差异:字体 / 圆角 / 边框 / 阴影 / 背景纹理 / 交互手感。 + —— 十二套主题 —— + arcade 像素街机:像素字、扫描线 CRT、硬阴影按键 + retro 复古掌机:Game Boy 四阶绿、点阵液晶、掌机边框 + cyberpunk 赛博朋克:切角面板、霓虹描边、故障标题、透视网格 + vaporwave 蒸汽波:落日渐变、玻璃拟态、镭射渐变字 + candy 贴纸卡通:粗描边贴纸、错位硬阴影、波点手账 + darkmin 极简暗黑:细线、纯平、零装饰(以"无"为个性) + ink 水墨古风:宣纸、双线裱框、朱砂印章、衬线书卷 + terminal 终端黑客:等宽字、方括号按键、磷光屏闪烁、跳动光标 + steampunk 蒸汽朋克:黄铜铭牌、四角铆钉、机械按钮、齿轮常转 + ocean 深海玻璃:磨砂玻璃、气泡上浮、按钮波光、水下光柱 + sakura 樱花和风:花瓣飘落、绯红团子按钮、标题摇曳樱花 + space 星际深空:三层星野、流星划过、渐变描边太空舱按钮 + ===================================================================== */ + +/* ===================================================================== + 1. 像素街机 arcade —— 深夜街机厅,一切都是方的、硬的、会发光的 + ===================================================================== */ +body.theme-arcade { + background-image: radial-gradient(rgba(255, 255, 255, 0.14) 1px, transparent 1px), + radial-gradient(rgba(0, 229, 255, 0.1) 1px, transparent 1px), + radial-gradient(ellipse 80% 50% at 50% -10%, rgba(255, 62, 127, 0.16), transparent); + background-size: 90px 90px, 140px 140px, 100% 100%; + background-position: 0 0, 45px 60px, 0 0; + /* 中文正文用宋体承接像素英文——老式电玩对话框的味道 */ + font-family: 'NSimSun', 'SimSun', 'Songti SC', 'PingFang SC', 'Microsoft YaHei', serif; + font-size: 15px; +} +/* CRT 扫描线 + 四角暗角,盖在全站之上 */ +body.theme-arcade::after { + content: ''; + position: fixed; + inset: 0; + pointer-events: none; + z-index: 9990; + background: repeating-linear-gradient(0deg, transparent 0 3px, rgba(0, 0, 0, 0.1) 3px 4px), + radial-gradient(ellipse 130% 130% at 50% 50%, transparent 68%, rgba(0, 0, 0, 0.32)); +} +/* 全站直角 + 像素粗边 + 硬投影 */ +body.theme-arcade .panel, +body.theme-arcade .card, +body.theme-arcade .modal, +body.theme-arcade .input, +body.theme-arcade .tag, +body.theme-arcade .toast, +body.theme-arcade .avatar { + border-radius: 0; +} +body.theme-arcade .panel, +body.theme-arcade .card, +body.theme-arcade .modal { + border: 3px solid var(--border); + box-shadow: 0 6px 0 rgba(0, 0, 0, 0.45); +} +body.theme-arcade .card:hover { + transform: translate(-2px, -2px); + border-color: var(--accent); + box-shadow: 8px 8px 0 rgba(0, 0, 0, 0.45), var(--glow); +} +/* 像素按键:按下去会"咔哒"沉底 */ +body.theme-arcade .btn { + border-radius: 0; + background: var(--primary); + border: 3px solid color-mix(in srgb, var(--primary) 40%, #000); + font-family: 'Press Start 2P', 'NSimSun', 'SimSun', 'Songti SC', serif; + font-size: 12px; + letter-spacing: 1px; + box-shadow: inset -3px -3px 0 rgba(0, 0, 0, 0.3), inset 3px 3px 0 rgba(255, 255, 255, 0.22), + 0 5px 0 rgba(0, 0, 0, 0.5); +} +body.theme-arcade .btn:hover { + filter: brightness(1.15); +} +body.theme-arcade .btn:active { + transform: translateY(5px); + box-shadow: inset -3px -3px 0 rgba(0, 0, 0, 0.3), inset 3px 3px 0 rgba(255, 255, 255, 0.22), + 0 0 0 rgba(0, 0, 0, 0.5); +} +body.theme-arcade .btn-accent { + background: var(--accent); + border-color: color-mix(in srgb, var(--accent) 40%, #000); + color: #06263a; +} +body.theme-arcade .btn-ghost { + background: transparent; + border-color: var(--border); + box-shadow: 0 5px 0 rgba(0, 0, 0, 0.35); +} +body.theme-arcade .input { + border-width: 3px; +} +body.theme-arcade .input:focus { + box-shadow: 0 0 0 3px var(--accent); +} +/* 标题:像素字 + 闪烁光标 ▶ */ +body.theme-arcade .page-title { + font-family: 'Press Start 2P', 'NSimSun', 'SimSun', 'Songti SC', serif; + font-size: 17px; + letter-spacing: 1px; + text-shadow: 3px 3px 0 rgba(0, 0, 0, 0.55); +} +body.theme-arcade .page-title::before { + content: '▶'; + width: auto; + height: auto; + background: none; + box-shadow: none; + color: var(--primary); + animation: arcade-blink 1s steps(2, end) infinite; +} +@keyframes arcade-blink { + 50% { + opacity: 0; + } +} +/* 导航:实底 + 底部霓虹跑马灯带 */ +body.theme-arcade .nav { + background: var(--bg-panel); + backdrop-filter: none; + border-bottom: 3px solid var(--border); +} +body.theme-arcade .nav::after { + content: ''; + display: block; + height: 4px; + background: repeating-linear-gradient(90deg, var(--primary) 0 24px, var(--primary-2) 24px 48px, var(--accent) 48px 72px); + background-size: 72px 4px; + animation: arcade-marquee 1.2s steps(6) infinite; +} +@keyframes arcade-marquee { + to { + background-position: 72px 0; + } +} +body.theme-arcade .nav .link { + border-radius: 0; +} +body.theme-arcade .nav .link.active { + background: var(--primary); + box-shadow: inset -2px -2px 0 rgba(0, 0, 0, 0.35), inset 2px 2px 0 rgba(255, 255, 255, 0.25); +} +body.theme-arcade .brand-name { + background: none; + color: var(--primary-2); + font-family: 'Press Start 2P', 'NSimSun', 'SimSun', 'Songti SC', serif; + font-size: 13px; + text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.55); +} +/* 游戏画面像素化采样,放大缩小都是马赛克质感 */ +body.theme-arcade canvas { + image-rendering: pixelated; +} +/* 游戏卡片:图标区铺霓虹棋盘格,角标全部改方 */ +body.theme-arcade .game-card .icon-area { + background: repeating-conic-gradient(rgba(255, 62, 127, 0.13) 0 25%, rgba(0, 229, 255, 0.05) 0 50%); + background-size: 26px 26px; + border-bottom: 3px solid var(--border); +} +body.theme-arcade .game-card .price-tag, +body.theme-arcade .game-card .owned-tag, +body.theme-arcade .game-card .free-tag { + border-radius: 0; + box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.5); +} +body.theme-arcade .game-card .name { + font-weight: 800; +} +/* 筛选纸片:像素方块按键 */ +body.theme-arcade .chip { + border-radius: 0; + border-width: 2px; +} +body.theme-arcade .chip.on { + background: var(--primary); + box-shadow: inset -2px -2px 0 rgba(0, 0, 0, 0.35), inset 2px 2px 0 rgba(255, 255, 255, 0.25); +} +body.theme-arcade ::-webkit-scrollbar-thumb { + border-radius: 0; + background: var(--primary); +} + +/* ===================================================================== + 2. 复古掌机 retro —— Game Boy 单色液晶:全站只有一种绿 + ===================================================================== */ +body.theme-retro { + /* 点阵液晶:底色上铺满细密网点;中文用仿宋衬托终端英文 */ + background-image: radial-gradient(rgba(0, 0, 0, 0.22) 1px, transparent 1px); + background-size: 4px 4px; + font-family: 'VT323', 'FangSong', '仿宋', 'STFangsong', 'NSimSun', 'Courier New', monospace; + font-size: 16px; +} +/* 掌机机身边框:屏幕嵌在灰绿色外壳里 */ +body.theme-retro::after { + content: ''; + position: fixed; + inset: 0; + pointer-events: none; + z-index: 9990; + box-shadow: inset 0 0 0 8px #8b956d, inset 0 0 0 10px #4a5340, inset 0 0 46px rgba(0, 0, 0, 0.42); +} +/* 液晶显示没有圆角、没有阴影、没有光效 */ +body.theme-retro .panel, +body.theme-retro .card, +body.theme-retro .modal, +body.theme-retro .input, +body.theme-retro .btn, +body.theme-retro .tag, +body.theme-retro .toast, +body.theme-retro .avatar, +body.theme-retro .nav .link { + border-radius: 0; + box-shadow: none; +} +body.theme-retro .panel, +body.theme-retro .card, +body.theme-retro .modal { + border: 2px solid var(--border); +} +body.theme-retro .card:hover { + transform: none; + box-shadow: none; + border-color: var(--primary); + background: color-mix(in srgb, var(--bg-card) 60%, var(--primary) 12%); +} +/* 按钮:深绿实底,悬停时正片负冲(反色) */ +body.theme-retro .btn { + background: var(--primary); + color: #0f380f; + border: 2px solid var(--border); + font-family: inherit; + font-size: 16px; + font-weight: 400; +} +body.theme-retro .btn:hover { + filter: none; + background: var(--text); + color: #0f380f; +} +body.theme-retro .btn:active { + transform: translate(2px, 2px); +} +body.theme-retro .btn-ghost { + background: transparent; + color: var(--text); +} +body.theme-retro .btn-accent { + background: var(--accent); + color: #0f380f; +} +body.theme-retro .page-title { + font-size: 24px; + font-weight: 400; + letter-spacing: 1px; +} +body.theme-retro .page-title::before { + content: '■'; + width: auto; + height: auto; + background: none; + box-shadow: none; + color: var(--primary); +} +body.theme-retro .nav { + background: var(--bg-panel); + backdrop-filter: none; + border-bottom: 2px solid var(--border); +} +body.theme-retro .nav .link.active { + background: var(--primary); + color: #0f380f; + box-shadow: none; +} +body.theme-retro .brand-name { + background: none; + color: var(--text); + font-size: 19px; +} +body.theme-retro .brand-icon { + filter: none; +} +/* 提示与弹层也遵守单色纪律 */ +body.theme-retro .toast { + background: #306230; + border: 2px solid #0f380f; + color: #dceeaa; +} +body.theme-retro .toast-success, +body.theme-retro .toast-info { + background: #4a7a24; +} +body.theme-retro canvas { + image-rendering: pixelated; +} +/* 游戏卡片:图标滤成单色绿——液晶屏里的精灵图 */ +body.theme-retro .game-card .icon-area { + background: var(--bg-panel); + border-bottom: 2px solid var(--border); +} +body.theme-retro .game-card .game-icon { + filter: grayscale(1) sepia(1) hue-rotate(52deg) saturate(3.2) brightness(1.05); +} +body.theme-retro .game-card .price-tag { + background: var(--primary); + color: #0f380f; + border-radius: 0; +} +body.theme-retro .game-card .owned-tag, +body.theme-retro .game-card .free-tag { + background: transparent; + border: 2px solid var(--primary); + color: var(--primary); + border-radius: 0; +} +/* 筛选纸片:液晶纯平方块 */ +body.theme-retro .chip { + border-radius: 0; + background: var(--bg-panel); +} +body.theme-retro .chip.on { + background: var(--primary); + color: #0f380f; + border-color: var(--border); + box-shadow: none; +} +body.theme-retro ::-webkit-scrollbar-thumb { + border-radius: 0; + background: var(--border); +} + +/* ===================================================================== + 3. 赛博朋克 cyberpunk —— 切角、霓虹、故障感 + ===================================================================== */ +body.theme-cyberpunk { + background-image: radial-gradient(ellipse 80% 55% at 50% -10%, rgba(122, 4, 235, 0.28), transparent), + linear-gradient(transparent 85%, rgba(5, 217, 232, 0.12)); + /* 中文用黑体拉出硬朗机械感,整体轻微加宽字距 */ + font-family: 'VT323', 'SimHei', '黑体', 'Heiti SC', 'Microsoft YaHei', sans-serif; + letter-spacing: 0.4px; +} +/* 地平线透视网格 */ +body.theme-cyberpunk::after { + content: ''; + position: fixed; + left: 0; + right: 0; + bottom: 0; + height: 32vh; + pointer-events: none; + z-index: 0; + background: linear-gradient(rgba(5, 217, 232, 0.15) 1px, transparent 1px), + linear-gradient(90deg, rgba(5, 217, 232, 0.15) 1px, transparent 1px); + background-size: 44px 28px; + transform: perspective(280px) rotateX(50deg); + transform-origin: bottom; + mask-image: linear-gradient(transparent, #000 60%); + -webkit-mask-image: linear-gradient(transparent, #000 60%); +} +/* 缓慢下移的扫描亮线 */ +body.theme-cyberpunk::before { + content: ''; + position: fixed; + left: 0; + right: 0; + top: -10%; + height: 22vh; + pointer-events: none; + z-index: 9990; + background: linear-gradient(transparent, rgba(5, 217, 232, 0.05), transparent); + animation: cyber-scan 7s linear infinite; +} +@keyframes cyber-scan { + to { + transform: translateY(120vh); + } +} +/* 切角面板:右上与左下削去一刀 */ +body.theme-cyberpunk .panel, +body.theme-cyberpunk .card, +body.theme-cyberpunk .modal { + border-radius: 0; + border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent); + clip-path: polygon(0 0, calc(100% - 16px) 0, 100% 16px, 100% 100%, 16px 100%, 0 calc(100% - 16px)); + box-shadow: inset 0 0 26px rgba(5, 217, 232, 0.05); +} +body.theme-cyberpunk .card:hover { + transform: translateY(-3px); + border-color: var(--primary); + /* clip-path 会裁掉 box-shadow,用 drop-shadow 让辉光贴着切角轮廓 */ + box-shadow: inset 0 0 26px rgba(255, 42, 109, 0.1); + filter: drop-shadow(0 0 10px rgba(255, 42, 109, 0.45)); +} +/* 按钮:终端字 + 全大写 + 小切角 */ +body.theme-cyberpunk .btn { + border-radius: 0; + clip-path: polygon(0 0, calc(100% - 10px) 0, 100% 10px, 100% 100%, 10px 100%, 0 calc(100% - 10px)); + font-family: 'VT323', 'PingFang SC', 'Microsoft YaHei', monospace; + font-size: 17px; + font-weight: 400; + text-transform: uppercase; + letter-spacing: 2px; + background: linear-gradient(100deg, var(--primary), var(--primary-2)); +} +body.theme-cyberpunk .btn-ghost { + background: transparent; + border: 1px solid var(--accent); + color: var(--accent); +} +body.theme-cyberpunk .btn-ghost:hover { + background: color-mix(in srgb, var(--accent) 16%, transparent); + color: #fff; +} +body.theme-cyberpunk .input { + border-radius: 0; + clip-path: polygon(0 0, calc(100% - 10px) 0, 100% 10px, 100% 100%, 10px 100%, 0 calc(100% - 10px)); +} +body.theme-cyberpunk .tag { + border-radius: 0; +} +/* 标题:双色错位的故障字 */ +body.theme-cyberpunk .page-title { + text-transform: uppercase; + letter-spacing: 3px; + text-shadow: 2px 0 var(--primary), -2px 0 var(--accent); +} +body.theme-cyberpunk .page-title::before { + background: var(--accent); + box-shadow: 0 0 12px var(--accent); + border-radius: 0; + clip-path: polygon(0 0, 100% 0, 100% calc(100% - 5px), 0 100%); +} +body.theme-cyberpunk .nav { + border-bottom: 1px solid color-mix(in srgb, var(--accent) 50%, transparent); + box-shadow: 0 1px 18px rgba(5, 217, 232, 0.18); +} +body.theme-cyberpunk .nav .link { + border-radius: 0; +} +body.theme-cyberpunk .nav .link.active { + background: transparent; + border: 1px solid var(--primary); + color: var(--primary); + box-shadow: inset 0 0 14px rgba(255, 42, 109, 0.25), 0 0 10px rgba(255, 42, 109, 0.35); +} +body.theme-cyberpunk .modal { + box-shadow: none; + filter: drop-shadow(0 0 18px rgba(5, 217, 232, 0.35)); +} +/* 游戏卡片:图标区铺霓虹网格,图标带电光辉光 */ +body.theme-cyberpunk .game-card .icon-area { + background: linear-gradient(rgba(5, 217, 232, 0.09) 1px, transparent 1px), + linear-gradient(90deg, rgba(5, 217, 232, 0.09) 1px, transparent 1px), + radial-gradient(circle at 72% 22%, rgba(255, 42, 109, 0.2), transparent 62%); + background-size: 22px 22px, 22px 22px, 100% 100%; + border-bottom: 1px solid color-mix(in srgb, var(--accent) 45%, transparent); +} +body.theme-cyberpunk .game-card .game-icon { + filter: drop-shadow(0 0 10px rgba(5, 217, 232, 0.65)); +} +body.theme-cyberpunk .game-card .name { + letter-spacing: 2px; +} +body.theme-cyberpunk .game-card .price-tag, +body.theme-cyberpunk .game-card .owned-tag, +body.theme-cyberpunk .game-card .free-tag { + border-radius: 0; +} +/* 筛选纸片:霓虹描边端子 */ +body.theme-cyberpunk .chip { + border-radius: 0; +} +body.theme-cyberpunk .chip.on { + background: transparent; + border: 1px solid var(--primary); + color: var(--primary); + box-shadow: inset 0 0 12px rgba(255, 42, 109, 0.25), 0 0 8px rgba(255, 42, 109, 0.3); +} +body.theme-cyberpunk ::-webkit-scrollbar-thumb { + border-radius: 0; + background: var(--accent); +} + +/* ===================================================================== + 4. 蒸汽波 vaporwave —— 落日、镭射、玻璃拟态 + ===================================================================== */ +body.theme-vaporwave { + background-image: linear-gradient(180deg, #14082e 0%, #2a1352 42%, #742a86 78%, #b3407e 100%); + background-attachment: fixed; + /* 通篇衬线字:怀旧杂志排版感 */ + font-family: Georgia, 'Palatino Linotype', 'STSong', 'Songti SC', 'SimSun', serif; +} +/* 条纹落日 + 粉色网格地面 */ +body.theme-vaporwave::after { + content: ''; + position: fixed; + inset: 0; + pointer-events: none; + z-index: 0; + background: + radial-gradient(circle 210px at 82% 16%, rgba(255, 113, 206, 0.55) 0 58%, transparent 60%), + linear-gradient(rgba(255, 113, 206, 0.14) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 113, 206, 0.14) 1px, transparent 1px); + background-size: 100% 100%, 100% 30px, 56px 100%; + background-position: 0 0, 0 100%, 0 100%; + -webkit-mask-image: linear-gradient(#000 0 34%, transparent 40% 72%, #000 82%); + mask-image: linear-gradient(#000 0 34%, transparent 40% 72%, #000 82%); +} +/* 玻璃拟态面板:磨砂半透明浮在渐变上 */ +body.theme-vaporwave .panel, +body.theme-vaporwave .card, +body.theme-vaporwave .modal { + background: rgba(255, 255, 255, 0.07); + border: 1px solid rgba(255, 255, 255, 0.18); + backdrop-filter: blur(14px) saturate(1.5); + -webkit-backdrop-filter: blur(14px) saturate(1.5); + box-shadow: 0 8px 30px rgba(20, 8, 46, 0.35); +} +body.theme-vaporwave .card:hover { + transform: translateY(-4px) rotate(-0.6deg); + border-color: rgba(255, 113, 206, 0.65); + box-shadow: 0 14px 38px rgba(255, 113, 206, 0.25); +} +body.theme-vaporwave .input { + background: rgba(255, 255, 255, 0.09); + border-color: rgba(255, 255, 255, 0.22); + border-radius: 999px; + padding-left: 16px; +} +/* 按钮:胶囊形镭射渐变 */ +body.theme-vaporwave .btn { + border-radius: 999px; + background: linear-gradient(100deg, var(--primary), var(--primary-2), var(--accent)); + background-size: 200% 100%; + box-shadow: 0 8px 22px rgba(255, 113, 206, 0.35); + transition: background-position 0.4s ease, transform 0.1s, filter 0.15s; +} +body.theme-vaporwave .btn:hover { + background-position: 100% 0; +} +body.theme-vaporwave .tag { + border-radius: 999px; + background: rgba(255, 255, 255, 0.08); +} +/* 标题:斜体衬线 + 镭射渐变字 */ +body.theme-vaporwave .page-title { + font-family: Georgia, 'Songti SC', 'SimSun', serif; + font-style: italic; + letter-spacing: 5px; + background: linear-gradient(90deg, var(--primary), var(--accent)); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} +body.theme-vaporwave .page-title::before { + display: none; +} +body.theme-vaporwave .nav { + background: rgba(26, 11, 46, 0.5); + backdrop-filter: blur(16px) saturate(1.4); + border-bottom: 1px solid rgba(255, 255, 255, 0.14); +} +body.theme-vaporwave .nav .link { + border-radius: 999px; +} +body.theme-vaporwave .nav .link.active { + box-shadow: 0 6px 18px rgba(255, 113, 206, 0.4); +} +body.theme-vaporwave .avatar { + background: rgba(255, 255, 255, 0.1); +} +body.theme-vaporwave .modal { + background: rgba(30, 12, 56, 0.72); +} +/* 游戏卡片:图标区是一幅小落日,玻璃感角标 */ +body.theme-vaporwave .game-card .icon-area { + background: radial-gradient(circle 40px at 76% 34%, rgba(255, 190, 225, 0.9) 0 38px, transparent 40px), + linear-gradient(180deg, rgba(255, 113, 206, 0.32), rgba(122, 4, 235, 0.3)); + border-bottom: 1px solid rgba(255, 255, 255, 0.16); +} +body.theme-vaporwave .game-card .game-icon { + filter: drop-shadow(0 6px 14px rgba(255, 113, 206, 0.55)); +} +body.theme-vaporwave .game-card .price-tag, +body.theme-vaporwave .game-card .owned-tag, +body.theme-vaporwave .game-card .free-tag { + backdrop-filter: blur(6px); +} +/* 筛选纸片:玻璃胶囊 */ +body.theme-vaporwave .chip { + background: rgba(255, 255, 255, 0.07); + border-color: rgba(255, 255, 255, 0.2); +} +body.theme-vaporwave .chip.on { + box-shadow: 0 6px 16px rgba(255, 113, 206, 0.4); +} +body.theme-vaporwave ::-webkit-scrollbar-thumb { + background: rgba(255, 113, 206, 0.5); +} + +/* ===================================================================== + 5. 贴纸卡通 candy —— 手账波点 + 粗描边贴纸 + 错位硬阴影 + ===================================================================== */ +body.theme-candy { + background-image: radial-gradient(rgba(255, 111, 165, 0.16) 3px, transparent 3px), + radial-gradient(rgba(94, 203, 247, 0.14) 3px, transparent 3px); + background-size: 42px 42px, 42px 42px; + background-position: 0 0, 21px 21px; + /* 圆体字:软乎乎的儿童贴纸感 */ + font-family: 'Comic Sans MS', 'YouYuan', '幼圆', 'Yuanti SC', 'Microsoft YaHei', sans-serif; + font-weight: 600; +} +/* 贴纸三件套:粗巧克力描边 + 错位硬阴影 + 圆角 */ +body.theme-candy .panel, +body.theme-candy .card, +body.theme-candy .modal { + border: 3px solid #3b2145; + box-shadow: 5px 5px 0 #3b2145; +} +body.theme-candy .card:hover { + transform: translate(-2px, -2px) rotate(-1.2deg); + border-color: #3b2145; + box-shadow: 8px 8px 0 #3b2145; +} +body.theme-candy .btn { + border: 3px solid #3b2145; + border-radius: 14px; + background: var(--primary); + box-shadow: 4px 4px 0 #3b2145; + font-weight: 800; + transition: transform 0.12s, box-shadow 0.12s, background 0.15s; +} +body.theme-candy .btn:hover { + filter: none; + transform: translate(-2px, -2px); + box-shadow: 6px 6px 0 #3b2145; +} +body.theme-candy .btn:active { + transform: translate(2px, 2px); + box-shadow: 1px 1px 0 #3b2145; +} +body.theme-candy .btn-accent { + background: var(--accent); +} +body.theme-candy .btn-ghost { + background: #fff; + color: #3b2145; +} +body.theme-candy .input { + border: 3px solid #3b2145; + background: #fff; + box-shadow: 3px 3px 0 #3b2145; +} +body.theme-candy .input:focus { + box-shadow: 3px 3px 0 var(--primary); + border-color: #3b2145; +} +/* 标签变成歪着贴的小贴纸 */ +body.theme-candy .tag { + border: 2px solid #3b2145; + background: #fff; + color: #3b2145; + box-shadow: 2px 2px 0 #3b2145; + transform: rotate(-2deg); +} +body.theme-candy .page-title { + font-weight: 900; + letter-spacing: 1px; +} +body.theme-candy .page-title::before { + content: '🍭'; + width: auto; + height: auto; + background: none; + box-shadow: none; + font-size: 22px; + transform: rotate(-8deg); +} +body.theme-candy .nav { + background: #fff; + backdrop-filter: none; + border-bottom: 3px solid #3b2145; +} +body.theme-candy .nav .link { + border-radius: 999px; + font-weight: 700; +} +body.theme-candy .nav .link.active { + border: 2px solid #3b2145; + box-shadow: 3px 3px 0 #3b2145; +} +body.theme-candy .brand-icon { + filter: none; +} +body.theme-candy .avatar { + border: 2px solid #3b2145; + background: #fff; +} +body.theme-candy .toast { + border: 3px solid #3b2145; + box-shadow: 4px 4px 0 #3b2145; + border-radius: 14px; + font-weight: 700; +} +body.theme-candy .modal { + box-shadow: 8px 8px 0 #3b2145; +} +/* 游戏卡片:白底波点图标区,图标垫一枚描边贴纸圆 */ +body.theme-candy .game-card .icon-area { + background: #fff radial-gradient(rgba(255, 111, 165, 0.28) 3px, transparent 3px); + background-size: 26px 26px; + border-bottom: 3px solid #3b2145; +} +body.theme-candy .game-card .game-icon { + background: #fff; + border: 3px solid #3b2145; + border-radius: 50%; + padding: 9px; + font-size: 38px; + filter: none; + box-shadow: 3px 3px 0 #3b2145; +} +body.theme-candy .game-card .price-tag, +body.theme-candy .game-card .owned-tag, +body.theme-candy .game-card .free-tag { + border: 2px solid #3b2145; + box-shadow: 2px 2px 0 #3b2145; + background: #fff; + color: #3b2145; + transform: rotate(3deg); +} +body.theme-candy .game-card .price-tag { + background: var(--primary); + color: #fff; +} +/* 筛选纸片:描边小饼干 */ +body.theme-candy .chip { + border: 2px solid #3b2145; + background: #fff; + color: #3b2145; + font-weight: 700; +} +body.theme-candy .chip.on { + background: var(--primary); + color: #fff; + border-color: #3b2145; + box-shadow: 2px 2px 0 #3b2145; +} +body.theme-candy ::-webkit-scrollbar-thumb { + background: var(--primary); + border-radius: 999px; +} + +/* ===================================================================== + 6. 极简暗黑 darkmin —— 克制到底:细线、纯平、无装饰、无光效 + ===================================================================== */ +body.theme-darkmin { + background-image: none; +} +body.theme-darkmin .panel, +body.theme-darkmin .card, +body.theme-darkmin .modal { + border: 1px solid var(--border); + box-shadow: none; +} +body.theme-darkmin .card:hover { + transform: translateY(-2px); + box-shadow: none; + border-color: color-mix(in srgb, var(--primary) 55%, var(--border)); +} +/* 按钮:纯平实底,没有渐变没有阴影 */ +body.theme-darkmin .btn { + background: var(--primary); + border-radius: 8px; + font-weight: 500; + box-shadow: none; +} +body.theme-darkmin .btn:hover { + filter: brightness(1.08); +} +body.theme-darkmin .btn-accent { + background: var(--accent); + color: #08211a; +} +body.theme-darkmin .btn-ghost { + background: transparent; + border: 1px solid var(--border); +} +body.theme-darkmin .input { + background: var(--bg-panel); + border-radius: 8px; +} +body.theme-darkmin .input:focus { + box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 30%, transparent); +} +body.theme-darkmin .page-title { + font-weight: 600; + font-size: 20px; +} +body.theme-darkmin .page-title::before { + width: 4px; + border-radius: 2px; + background: var(--primary); + box-shadow: none; +} +body.theme-darkmin .nav { + background: color-mix(in srgb, var(--bg) 90%, transparent); + border-bottom: 1px solid var(--border); +} +body.theme-darkmin .nav .link.active { + background: var(--bg-card); + color: var(--primary); + box-shadow: none; +} +body.theme-darkmin .brand-name { + background: none; + color: var(--text); + font-weight: 600; +} +body.theme-darkmin .brand-icon { + filter: none; +} +body.theme-darkmin .tag { + border-radius: 6px; +} +/* 游戏卡片:图标区纯平不铺渐变,图标不带投影 */ +body.theme-darkmin .game-card .icon-area { + background: var(--bg-panel); + border-bottom: 1px solid var(--border); +} +body.theme-darkmin .game-card .game-icon { + filter: none; +} +/* 筛选纸片:无渐变无光效,选中只换色 */ +body.theme-darkmin .chip.on { + background: var(--bg-card); + color: var(--primary); + border-color: var(--primary); + box-shadow: none; +} +body.theme-darkmin .modal { + box-shadow: 0 24px 64px rgba(0, 0, 0, 0.5); +} + +/* ===================================================================== + 7. 水墨古风 ink —— 宣纸、裱框、朱砂印、书卷气 + ===================================================================== */ +body.theme-ink { + /* 宣纸纤维纹理:极淡的经纬织纹 + 大面积留白 */ + background-image: repeating-linear-gradient(0deg, rgba(43, 38, 32, 0.016) 0 2px, transparent 2px 4px), + repeating-linear-gradient(90deg, rgba(43, 38, 32, 0.016) 0 2px, transparent 2px 4px), + radial-gradient(ellipse 60% 42% at 82% -6%, rgba(61, 107, 92, 0.07), transparent); + /* 楷体正文:一笔一划的书卷气 */ + font-family: 'KaiTi', '楷体', 'STKaiti', 'Kaiti SC', Georgia, 'Songti SC', serif; + font-size: 15.5px; +} +/* 双线裱框:细边框外再套一圈淡线,像装裱的画轴 */ +body.theme-ink .panel, +body.theme-ink .card, +body.theme-ink .modal { + border: 1px solid var(--border); + outline: 1px solid color-mix(in srgb, var(--border) 55%, transparent); + outline-offset: 4px; + box-shadow: 0 2px 10px rgba(43, 38, 32, 0.05); +} +body.theme-ink .card:hover { + transform: translateY(-2px); + border-color: var(--primary); + box-shadow: 0 8px 22px rgba(43, 38, 32, 0.12); +} +/* 按钮:朱砂色方章 */ +body.theme-ink .btn { + background: var(--primary); + border-radius: 4px; + font-family: inherit; + letter-spacing: 4px; + box-shadow: none; +} +body.theme-ink .btn:hover { + filter: brightness(0.92); +} +body.theme-ink .btn-accent { + background: var(--accent); +} +body.theme-ink .btn-ghost { + border: 1px solid var(--primary); + color: var(--primary); + letter-spacing: 4px; +} +body.theme-ink .input { + background: #fff; + border-radius: 4px; +} +/* 标题:左侧一枚歪盖的朱砂印 */ +body.theme-ink .page-title { + letter-spacing: 6px; + font-weight: 700; +} +body.theme-ink .page-title::before { + content: '戏'; + width: 26px; + height: 26px; + background: var(--primary); + box-shadow: none; + border-radius: 4px; + color: #fff8f0; + font-size: 15px; + font-weight: 400; + display: inline-flex; + align-items: center; + justify-content: center; + transform: rotate(-4deg); +} +body.theme-ink .nav { + background: color-mix(in srgb, var(--bg-panel) 94%, transparent); + border-bottom: 1px solid var(--border); +} +body.theme-ink .nav .link { + border-radius: 0; + letter-spacing: 2px; +} +body.theme-ink .nav .link.active { + background: transparent; + color: var(--primary); + border-bottom: 2px solid var(--primary); + box-shadow: none; +} +body.theme-ink .brand-name { + background: none; + color: var(--text); + letter-spacing: 3px; +} +body.theme-ink .brand-icon { + filter: none; +} +body.theme-ink .tag { + border-radius: 2px; + background: transparent; + letter-spacing: 1px; +} +body.theme-ink .tag-primary { + background: color-mix(in srgb, var(--primary) 8%, transparent); +} +body.theme-ink .avatar { + border-radius: 4px; + background: #fff; +} +body.theme-ink .modal { + box-shadow: 0 16px 48px rgba(43, 38, 32, 0.18); +} +/* 游戏卡片:图标区做旧宣纸色,右下角落一枚小朱印 */ +body.theme-ink .game-card .icon-area { + background: #f3eddc; + border-bottom: 1px solid var(--border); +} +body.theme-ink .game-card .icon-area::after { + content: ''; + position: absolute; + right: 9px; + bottom: 9px; + width: 13px; + height: 13px; + background: var(--primary); + border-radius: 2px; + opacity: 0.85; + transform: rotate(-8deg); +} +body.theme-ink .game-card .game-icon { + filter: saturate(0.72) contrast(0.95); +} +body.theme-ink .game-card .name { + letter-spacing: 2px; +} +body.theme-ink .game-card .price-tag, +body.theme-ink .game-card .owned-tag, +body.theme-ink .game-card .free-tag { + border-radius: 2px; + letter-spacing: 1px; +} +body.theme-ink .game-card .price-tag { + background: var(--primary); +} +/* 筛选纸片:留白 + 朱砂描边选中 */ +body.theme-ink .chip { + border-radius: 2px; + background: transparent; + letter-spacing: 2px; +} +body.theme-ink .chip.on { + background: transparent; + color: var(--primary); + border-color: var(--primary); + box-shadow: none; +} +body.theme-ink ::-webkit-scrollbar-thumb { + background: #cfc6b0; +} + +/* ===================================================================== + 8. 终端黑客 terminal —— 等宽字、方括号按键、磷光绿屏 + 一切都像跑在一台老式终端里:按钮是 [命令],标题后面有跳动的光标 + ===================================================================== */ +body.theme-terminal { + /* 磷光屏底:极淡的绿色网格 + 顶部余晖 */ + background-image: linear-gradient(rgba(53, 242, 107, 0.04) 1px, transparent 1px), + linear-gradient(90deg, rgba(53, 242, 107, 0.04) 1px, transparent 1px), + radial-gradient(ellipse 70% 42% at 50% 0, rgba(53, 242, 107, 0.07), transparent); + background-size: 30px 30px, 30px 30px, 100% 100%; + font-family: 'Consolas', 'Courier New', 'Microsoft YaHei', monospace; +} +/* 扫描线覆盖层:偶尔像老 CRT 一样闪一下 */ +body.theme-terminal::after { + content: ''; + position: fixed; + inset: 0; + pointer-events: none; + z-index: 9990; + background: repeating-linear-gradient(0deg, transparent 0 2px, rgba(0, 0, 0, 0.14) 2px 4px); + animation: term-flicker 5s steps(1) infinite; +} +@keyframes term-flicker { + 0%, + 91%, + 93.5%, + 100% { + background-color: transparent; + } + 92% { + background-color: rgba(190, 255, 205, 0.04); + } +} +/* 全站直角:终端里没有圆角这回事 */ +body.theme-terminal .panel, +body.theme-terminal .card, +body.theme-terminal .modal, +body.theme-terminal .input, +body.theme-terminal .tag, +body.theme-terminal .toast, +body.theme-terminal .avatar, +body.theme-terminal .btn, +body.theme-terminal .chip, +body.theme-terminal .nav .link { + border-radius: 0; +} +body.theme-terminal .panel, +body.theme-terminal .card, +body.theme-terminal .modal { + border: 1px solid var(--border); + box-shadow: inset 0 0 0 1px rgba(53, 242, 107, 0.05), inset 0 0 26px rgba(53, 242, 107, 0.04); +} +/* 卡片交互:不位移,通电发光 */ +body.theme-terminal .card:hover { + transform: none; + border-color: var(--primary); + background: color-mix(in srgb, var(--bg-card) 88%, var(--primary) 6%); + box-shadow: inset 0 0 0 1px rgba(53, 242, 107, 0.12), var(--glow); +} +/* 按钮 = 一条可执行的命令:[开始游戏],悬停反色像被选中 */ +body.theme-terminal .btn { + background: transparent; + border: 1px solid var(--primary); + color: var(--primary); + font-family: inherit; + font-weight: 700; + letter-spacing: 1px; + gap: 2px; +} +body.theme-terminal .btn::before { + content: '['; + opacity: 0.65; +} +body.theme-terminal .btn::after { + content: ']'; + opacity: 0.65; +} +body.theme-terminal .btn:hover { + filter: none; + background: var(--primary); + color: #041208; + box-shadow: var(--glow); +} +body.theme-terminal .btn:active { + transform: translate(1px, 1px); +} +body.theme-terminal .btn-accent { + background: transparent; + border-color: var(--primary-2); + color: var(--primary-2); +} +body.theme-terminal .btn-accent:hover { + background: var(--primary-2); + color: #1a1206; + box-shadow: 0 0 14px rgba(255, 176, 32, 0.4); +} +body.theme-terminal .btn-ghost { + border: 1px dashed var(--border); + color: var(--text-dim); +} +body.theme-terminal .btn-ghost:hover { + background: transparent; + border-color: var(--primary); + color: var(--primary); + box-shadow: none; +} +body.theme-terminal .input { + border-color: var(--border); + font-family: inherit; +} +body.theme-terminal .input:focus { + border-color: var(--primary); + box-shadow: 0 0 0 1px var(--primary), 0 0 12px rgba(53, 242, 107, 0.25); +} +/* 标题:提示符 > 开头,结尾一枚跳动的块状光标 */ +body.theme-terminal .page-title { + font-family: inherit; + letter-spacing: 1px; +} +body.theme-terminal .page-title::before { + content: '>'; + width: auto; + height: auto; + background: none; + box-shadow: none; + color: var(--primary); + font-weight: 900; +} +body.theme-terminal .page-title::after { + content: '▊'; + color: var(--primary); + animation: term-caret 1s steps(2, end) infinite; +} +@keyframes term-caret { + 50% { + opacity: 0; + } +} +body.theme-terminal .nav { + background: rgba(3, 9, 3, 0.92); + backdrop-filter: none; + border-bottom: 1px solid var(--primary); + box-shadow: 0 0 18px rgba(53, 242, 107, 0.14); +} +body.theme-terminal .nav .link.active { + background: transparent; + color: var(--primary); + box-shadow: inset 0 -2px 0 var(--primary); +} +body.theme-terminal .brand-name { + background: none; + color: var(--primary); + font-family: inherit; + text-shadow: 0 0 8px rgba(53, 242, 107, 0.55); +} +body.theme-terminal .brand-icon { + filter: grayscale(1) sepia(1) hue-rotate(70deg) saturate(3); +} +/* 标签与筛选:虚线框像注释,选中变实线通电 */ +body.theme-terminal .tag { + border: 1px dashed var(--border); + background: transparent; + font-family: inherit; +} +body.theme-terminal .chip { + border: 1px dashed var(--border); + background: transparent; +} +body.theme-terminal .chip.on { + border-style: solid; + border-color: var(--primary); + color: var(--primary); + background: rgba(53, 242, 107, 0.08); + box-shadow: none; +} +body.theme-terminal .toast { + border: 1px solid rgba(255, 255, 255, 0.35); + font-family: inherit; +} +/* 游戏卡片:图标区铺网格,图标带磷光辉光 */ +body.theme-terminal .game-card .icon-area { + background: linear-gradient(rgba(53, 242, 107, 0.05) 1px, transparent 1px), + linear-gradient(90deg, rgba(53, 242, 107, 0.05) 1px, transparent 1px); + background-size: 16px 16px; + border-bottom: 1px solid var(--border); +} +body.theme-terminal .game-card .game-icon { + filter: drop-shadow(0 0 10px rgba(53, 242, 107, 0.45)); +} +body.theme-terminal .game-card .price-tag, +body.theme-terminal .game-card .owned-tag, +body.theme-terminal .game-card .free-tag { + border-radius: 0; + font-family: inherit; +} +body.theme-terminal ::-webkit-scrollbar-thumb { + border-radius: 0; + background: var(--border); +} +body.theme-terminal ::-webkit-scrollbar-thumb:hover { + background: var(--primary); +} + +/* ===================================================================== + 9. 蒸汽朋克 steampunk —— 黄铜、铆钉、皮革与齿轮 + 面板是四角打着铆钉的金属铭牌,按钮是有配重感的机械铜钮 + ===================================================================== */ +body.theme-steampunk { + /* 深色皮革双斜织纹 + 顶部一盏暖黄铜光 */ + background-image: repeating-linear-gradient(45deg, rgba(240, 227, 201, 0.02) 0 2px, transparent 2px 6px), + repeating-linear-gradient(-45deg, rgba(0, 0, 0, 0.14) 0 2px, transparent 2px 6px), + radial-gradient(ellipse 90% 60% at 50% -12%, rgba(192, 138, 62, 0.18), transparent); + font-family: Georgia, 'Times New Roman', 'SimSun', '宋体', 'Songti SC', serif; + font-size: 15px; +} +/* 金属铭牌三件套:双圈边框 + 四角铆钉 + 顶部拉丝高光 */ +body.theme-steampunk .panel, +body.theme-steampunk .card, +body.theme-steampunk .modal { + border: 2px solid var(--border); + outline: 1px solid rgba(192, 138, 62, 0.22); + outline-offset: -7px; + background-image: radial-gradient(circle 3px at 8px 8px, #d8a54e 1.6px, #3a2a12 2.2px, transparent 3px), + radial-gradient(circle 3px at calc(100% - 8px) 8px, #d8a54e 1.6px, #3a2a12 2.2px, transparent 3px), + radial-gradient(circle 3px at 8px calc(100% - 8px), #d8a54e 1.6px, #3a2a12 2.2px, transparent 3px), + radial-gradient(circle 3px at calc(100% - 8px) calc(100% - 8px), #d8a54e 1.6px, #3a2a12 2.2px, transparent 3px), + linear-gradient(180deg, rgba(240, 227, 201, 0.05), transparent 24%); + box-shadow: inset 0 1px 0 rgba(240, 227, 201, 0.1), 0 4px 12px rgba(0, 0, 0, 0.4); +} +body.theme-steampunk .card:hover { + transform: translateY(-2px); + border-color: var(--primary); + box-shadow: inset 0 1px 0 rgba(240, 227, 201, 0.12), 0 8px 20px rgba(0, 0, 0, 0.5), var(--glow); +} +/* 机械铜钮:竖向铜质渐变 + 上高光下阴影,按下咔哒内凹 */ +body.theme-steampunk .btn { + background: linear-gradient(180deg, #d8a54e, #a06c2c 55%, #8a5a2b); + border: 1px solid #4a3418; + color: #241505; + text-shadow: 0 1px 0 rgba(255, 235, 195, 0.45); + font-family: inherit; + font-weight: 700; + letter-spacing: 2px; + box-shadow: inset 0 1px 0 rgba(255, 235, 195, 0.55), inset 0 -2px 3px rgba(60, 36, 8, 0.55), + 0 3px 6px rgba(0, 0, 0, 0.45); +} +body.theme-steampunk .btn:hover { + filter: brightness(1.1) saturate(1.08); + box-shadow: inset 0 1px 0 rgba(255, 235, 195, 0.55), inset 0 -2px 3px rgba(60, 36, 8, 0.55), + 0 3px 12px rgba(192, 138, 62, 0.55); +} +body.theme-steampunk .btn:active { + transform: translateY(1px); + box-shadow: inset 0 2px 6px rgba(40, 22, 4, 0.65); +} +body.theme-steampunk .btn-accent { + background: linear-gradient(180deg, #6fbfa8, #3f7f6d 55%, #35695b); + border-color: #1e4038; + color: #06201a; + text-shadow: 0 1px 0 rgba(220, 255, 244, 0.35); +} +body.theme-steampunk .btn-ghost { + background: transparent; + border: 1px solid var(--primary); + color: var(--primary); + box-shadow: none; + text-shadow: none; +} +body.theme-steampunk .btn-ghost:hover { + background: rgba(192, 138, 62, 0.12); + filter: none; +} +/* 输入框:凹进金属面板里的开孔 */ +body.theme-steampunk .input { + background: #150d06; + border: 1px solid var(--border); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.55); +} +body.theme-steampunk .input:focus { + border-color: var(--primary); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.55), 0 0 0 2px rgba(192, 138, 62, 0.25); +} +/* 标题:左侧一枚常转的黄铜齿轮 */ +body.theme-steampunk .page-title { + letter-spacing: 3px; + font-weight: 700; +} +body.theme-steampunk .page-title::before { + content: '⚙'; + width: auto; + height: auto; + background: none; + box-shadow: none; + color: var(--primary); + font-size: 22px; + animation: steam-gear 6s linear infinite; +} +@keyframes steam-gear { + to { + transform: rotate(360deg); + } +} +body.theme-steampunk .nav { + background: linear-gradient(180deg, #2c1d10, #241810); + backdrop-filter: none; + border-bottom: 2px solid var(--primary); + box-shadow: 0 2px 0 rgba(0, 0, 0, 0.6), 0 4px 14px rgba(0, 0, 0, 0.5); +} +body.theme-steampunk .nav .link.active { + background: linear-gradient(180deg, #d8a54e, #8a5a2b); + color: #241505; + box-shadow: inset 0 1px 0 rgba(255, 235, 195, 0.5); +} +body.theme-steampunk .brand-name { + background: none; + color: var(--primary); + font-family: inherit; + letter-spacing: 2px; + text-shadow: 0 1px 0 rgba(0, 0, 0, 0.8); +} +body.theme-steampunk .brand-icon { + filter: sepia(0.6) saturate(1.4) hue-rotate(-12deg); +} +/* 标签:小铜牌 */ +body.theme-steampunk .tag { + background: rgba(192, 138, 62, 0.1); + border: 1px solid var(--border); + border-radius: 4px; +} +body.theme-steampunk .chip { + border-radius: 4px; +} +body.theme-steampunk .chip.on { + background: linear-gradient(180deg, #d8a54e, #8a5a2b); + color: #241505; + border-color: #4a3418; + box-shadow: inset 0 1px 0 rgba(255, 235, 195, 0.5); +} +body.theme-steampunk .toast { + border: 2px solid rgba(74, 52, 24, 0.85); + border-radius: 6px; + font-family: inherit; +} +body.theme-steampunk .avatar { + border: 2px solid var(--border); + background: #201409; +} +body.theme-steampunk .modal { + box-shadow: inset 0 1px 0 rgba(240, 227, 201, 0.1), 0 20px 56px rgba(0, 0, 0, 0.65); +} +/* 游戏卡片:暖光舞台 + 微做旧的图标 */ +body.theme-steampunk .game-card .icon-area { + background: radial-gradient(ellipse 70% 80% at 50% 18%, rgba(192, 138, 62, 0.22), transparent), #201409; + border-bottom: 2px solid var(--border); +} +body.theme-steampunk .game-card .game-icon { + filter: sepia(0.35) saturate(0.95) drop-shadow(0 4px 8px rgba(0, 0, 0, 0.6)); +} +body.theme-steampunk .game-card .price-tag { + background: linear-gradient(180deg, #d8a54e, #8a5a2b); + color: #241505; + border-radius: 4px; +} +body.theme-steampunk .game-card .owned-tag, +body.theme-steampunk .game-card .free-tag { + border-radius: 4px; +} +body.theme-steampunk ::-webkit-scrollbar-thumb { + background: #6a4d28; + border-radius: 4px; +} + +/* ===================================================================== + 10. 深海玻璃 ocean —— 磨砂玻璃面板悬浮深海,气泡上浮,按钮掠过波光 + ===================================================================== */ +body.theme-ocean { + background-image: radial-gradient(ellipse 70% 46% at 26% -8%, rgba(64, 190, 255, 0.18), transparent), + radial-gradient(ellipse 52% 40% at 78% -6%, rgba(52, 224, 194, 0.1), transparent), + linear-gradient(180deg, #0a2a44 0%, #051726 46%, #030f1c 100%); + background-attachment: fixed; + letter-spacing: 0.2px; +} +/* 水面斜射下来的光柱,缓慢摇动 */ +body.theme-ocean::before { + content: ''; + position: fixed; + top: 0; + left: 10%; + width: 36%; + height: 62vh; + pointer-events: none; + z-index: 0; + background: linear-gradient(195deg, rgba(140, 220, 255, 0.08), transparent 70%); + clip-path: polygon(22% 0, 62% 0, 100% 100%, 0 100%); + animation: ocean-ray 9s ease-in-out infinite alternate; +} +@keyframes ocean-ray { + to { + transform: translateX(9%) skewX(-5deg); + opacity: 0.65; + } +} +/* 气泡群持续上浮(同尺寸平铺保证循环无缝) */ +body.theme-ocean::after { + content: ''; + position: fixed; + inset: 0; + pointer-events: none; + z-index: 9990; + background-image: radial-gradient(circle 3px at 12% 88%, rgba(255, 255, 255, 0.22) 2px, transparent 3px), + radial-gradient(circle 2px at 38% 64%, rgba(255, 255, 255, 0.16) 1.4px, transparent 2px), + radial-gradient(circle 4px at 64% 92%, rgba(255, 255, 255, 0.13) 3px, transparent 4px), + radial-gradient(circle 2.5px at 85% 74%, rgba(255, 255, 255, 0.18) 1.8px, transparent 2.5px), + radial-gradient(circle 1.6px at 50% 40%, rgba(255, 255, 255, 0.14) 1.1px, transparent 1.6px); + background-size: 100% 120vh; + animation: ocean-bubbles 22s linear infinite; +} +@keyframes ocean-bubbles { + to { + background-position: 0 -120vh; + } +} +/* 磨砂玻璃三件套:半透明底 + 毛玻璃 + 顶部一线白高光(玻璃厚度) */ +body.theme-ocean .panel, +body.theme-ocean .card, +body.theme-ocean .modal { + border: 1px solid var(--border); + backdrop-filter: blur(14px) saturate(1.25); + -webkit-backdrop-filter: blur(14px) saturate(1.25); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.14), 0 10px 30px rgba(2, 16, 28, 0.45); +} +body.theme-ocean .card:hover { + transform: translateY(-4px); + border-color: rgba(52, 224, 194, 0.55); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.18), var(--glow); +} +body.theme-ocean .modal { + background: rgba(13, 45, 71, 0.82); +} +/* 玻璃胶囊按钮:悬停一道波光从左掠到右 */ +body.theme-ocean .btn { + position: relative; + overflow: hidden; + border-radius: 999px; + background: linear-gradient(135deg, rgba(41, 197, 255, 0.85), rgba(110, 140, 255, 0.85)); + border: 1px solid rgba(255, 255, 255, 0.35); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.4), 0 6px 18px rgba(41, 197, 255, 0.25); +} +body.theme-ocean .btn::before { + content: ''; + position: absolute; + top: 0; + left: -80%; + width: 60%; + height: 100%; + background: linear-gradient(100deg, transparent, rgba(255, 255, 255, 0.45), transparent); + transform: skewX(-18deg); +} +body.theme-ocean .btn:hover { + filter: brightness(1.06); +} +body.theme-ocean .btn:hover::before { + animation: ocean-sheen 0.6s ease; +} +@keyframes ocean-sheen { + to { + left: 130%; + } +} +body.theme-ocean .btn:active { + transform: scale(0.96); +} +body.theme-ocean .btn-accent { + background: linear-gradient(135deg, rgba(52, 224, 194, 0.9), rgba(41, 197, 255, 0.85)); + color: #052a26; +} +body.theme-ocean .btn-ghost { + background: rgba(255, 255, 255, 0.06); + border: 1px solid rgba(255, 255, 255, 0.25); + color: var(--text); + backdrop-filter: blur(8px); + box-shadow: none; +} +body.theme-ocean .btn-ghost:hover { + border-color: rgba(52, 224, 194, 0.6); + color: var(--accent); +} +body.theme-ocean .input { + background: rgba(5, 23, 38, 0.55); + border: 1px solid rgba(126, 208, 255, 0.25); + border-radius: 12px; +} +body.theme-ocean .input:focus { + border-color: rgba(52, 224, 194, 0.6); + box-shadow: 0 0 0 3px rgba(52, 224, 194, 0.15); +} +/* 标题:一串上下浮动的气泡 */ +body.theme-ocean .page-title::before { + content: '🫧'; + width: auto; + height: auto; + background: none; + box-shadow: none; + font-size: 20px; + animation: ocean-bob 3s ease-in-out infinite; +} +@keyframes ocean-bob { + 50% { + transform: translateY(-4px); + } +} +body.theme-ocean .nav { + background: rgba(8, 32, 52, 0.55); + backdrop-filter: blur(16px) saturate(1.3); + border-bottom: 1px solid rgba(126, 208, 255, 0.2); +} +body.theme-ocean .nav .link { + border-radius: 999px; +} +body.theme-ocean .nav .link.active { + background: rgba(41, 197, 255, 0.18); + color: #bdeeff; + box-shadow: inset 0 0 0 1px rgba(41, 197, 255, 0.4); +} +body.theme-ocean .brand-name { + background: linear-gradient(90deg, #6ee7ff, #34e0c2); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} +/* 标签与筛选:小玻璃片 */ +body.theme-ocean .tag { + background: rgba(255, 255, 255, 0.06); + border-color: rgba(126, 208, 255, 0.25); +} +body.theme-ocean .chip { + background: rgba(255, 255, 255, 0.05); + border-color: rgba(126, 208, 255, 0.22); +} +body.theme-ocean .chip.on { + background: rgba(41, 197, 255, 0.2); + border-color: rgba(41, 197, 255, 0.55); + color: #bdeeff; + box-shadow: none; +} +body.theme-ocean .toast { + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.25); + border-radius: 14px; +} +body.theme-ocean .avatar { + background: rgba(255, 255, 255, 0.08); + border-color: rgba(126, 208, 255, 0.3); +} +/* 游戏卡片:从水面往下看的光锥,图标带水光 */ +body.theme-ocean .game-card .icon-area { + background: linear-gradient(180deg, rgba(64, 190, 255, 0.22), rgba(5, 23, 38, 0.05) 80%); + border-bottom: 1px solid rgba(126, 208, 255, 0.18); +} +body.theme-ocean .game-card .game-icon { + filter: drop-shadow(0 8px 16px rgba(41, 197, 255, 0.45)); +} +body.theme-ocean .game-card .price-tag, +body.theme-ocean .game-card .owned-tag, +body.theme-ocean .game-card .free-tag { + backdrop-filter: blur(6px); +} +body.theme-ocean ::-webkit-scrollbar-thumb { + background: rgba(126, 208, 255, 0.35); + border-radius: 999px; +} + +/* ===================================================================== + 11. 樱花和风 sakura —— 花瓣飘落、绯红团子按钮、标题摇曳的樱花 + ===================================================================== */ +body.theme-sakura { + /* 米粉底 + 顶部粉霞 + 极淡的鱼鳞(青海波)弧纹 */ + background-image: radial-gradient(circle 30px at 50% -6px, transparent 25px, rgba(217, 69, 95, 0.06) 25.5px 27px, transparent 28px), + radial-gradient(circle 30px at 0 38px, transparent 25px, rgba(143, 119, 181, 0.05) 25.5px 27px, transparent 28px), + radial-gradient(circle 30px at 100% 38px, transparent 25px, rgba(143, 119, 181, 0.05) 25.5px 27px, transparent 28px), + radial-gradient(ellipse 80% 38% at 50% -6%, rgba(242, 165, 180, 0.28), transparent); + background-size: 64px 44px, 64px 44px, 64px 44px, 100% 100%; + font-family: 'Yu Mincho', 'KaiTi', '楷体', 'STKaiti', 'Kaiti SC', 'Microsoft YaHei', serif; + font-size: 15px; +} +/* 两层花瓣以不同速度徐徐飘落(尺寸=平铺周期,循环无缝) */ +body.theme-sakura::before { + content: ''; + position: fixed; + inset: 0; + pointer-events: none; + z-index: 9990; + background-image: radial-gradient(ellipse 4px 3px at 22% 10%, rgba(240, 148, 168, 0.4) 60%, transparent 61%), + radial-gradient(ellipse 3px 2.4px at 58% 4%, rgba(217, 69, 95, 0.28) 60%, transparent 61%), + radial-gradient(ellipse 3.4px 2.6px at 86% 12%, rgba(242, 165, 180, 0.36) 60%, transparent 61%); + background-size: 100% 80vh; + animation: sakura-fall 26s linear infinite; +} +body.theme-sakura::after { + content: ''; + position: fixed; + inset: 0; + pointer-events: none; + z-index: 9990; + background-image: radial-gradient(ellipse 6px 4.6px at 12% 14%, rgba(240, 148, 168, 0.55) 60%, transparent 61%), + radial-gradient(ellipse 5px 4px at 40% 2%, rgba(242, 165, 180, 0.5) 60%, transparent 61%), + radial-gradient(ellipse 5.4px 4.2px at 72% 8%, rgba(217, 69, 95, 0.34) 60%, transparent 61%), + radial-gradient(ellipse 4.6px 3.6px at 94% 18%, rgba(240, 148, 168, 0.45) 60%, transparent 61%); + background-size: 100% 90vh; + animation: sakura-fall 13s linear infinite; +} +@keyframes sakura-fall { + to { + background-position: 0 90vh; + } +} +/* 和纸卡片:白底细粉边 + 柔粉影 */ +body.theme-sakura .panel, +body.theme-sakura .card, +body.theme-sakura .modal { + background: rgba(255, 255, 255, 0.9); + border: 1px solid var(--border); + box-shadow: 0 4px 16px rgba(217, 69, 95, 0.07); +} +body.theme-sakura .card:hover { + transform: translateY(-3px); + border-color: var(--primary-2); + box-shadow: var(--glow); +} +/* 团子按钮:圆滚滚,悬停轻轻浮起,按下压扁一点 */ +body.theme-sakura .btn { + background: var(--primary); + border-radius: 999px; + letter-spacing: 3px; + box-shadow: 0 4px 12px rgba(217, 69, 95, 0.28); + transition: transform 0.15s, box-shadow 0.15s, background 0.15s; +} +body.theme-sakura .btn:hover { + filter: none; + transform: translateY(-2px); + box-shadow: 0 8px 18px rgba(217, 69, 95, 0.35); +} +body.theme-sakura .btn:active { + transform: translateY(0) scale(0.96); + box-shadow: 0 2px 6px rgba(217, 69, 95, 0.3); +} +body.theme-sakura .btn-accent { + background: var(--accent); + box-shadow: 0 4px 12px rgba(111, 158, 99, 0.3); +} +body.theme-sakura .btn-ghost { + background: #fff; + border: 1px solid var(--primary-2); + color: var(--primary); + box-shadow: none; +} +body.theme-sakura .btn-ghost:hover { + background: #fff0f2; + border-color: var(--primary); +} +body.theme-sakura .input { + background: #fff; + border-radius: 12px; +} +body.theme-sakura .input:focus { + border-color: var(--primary-2); + box-shadow: 0 0 0 3px rgba(242, 165, 180, 0.35); +} +/* 标题:一朵随风摇曳的樱花 */ +body.theme-sakura .page-title { + letter-spacing: 4px; + font-weight: 700; +} +body.theme-sakura .page-title::before { + content: '🌸'; + width: auto; + height: auto; + background: none; + box-shadow: none; + font-size: 20px; + transform-origin: 50% 0; + animation: sakura-sway 3.2s ease-in-out infinite; +} +@keyframes sakura-sway { + 0%, + 100% { + transform: rotate(-8deg); + } + 50% { + transform: rotate(10deg); + } +} +body.theme-sakura .nav { + background: rgba(255, 251, 250, 0.86); + backdrop-filter: blur(10px); + border-bottom: 1px solid var(--border); +} +body.theme-sakura .nav .link { + border-radius: 999px; + letter-spacing: 1px; +} +body.theme-sakura .nav .link.active { + background: var(--primary); + color: #fff; + box-shadow: 0 4px 10px rgba(217, 69, 95, 0.3); +} +body.theme-sakura .brand-name { + background: none; + color: var(--primary); + letter-spacing: 2px; +} +body.theme-sakura .brand-icon { + filter: none; +} +body.theme-sakura .tag { + background: #fff; + border-radius: 999px; +} +body.theme-sakura .avatar { + background: #fff; +} +body.theme-sakura .toast { + border-radius: 14px; + letter-spacing: 1px; +} +body.theme-sakura .modal { + box-shadow: 0 18px 48px rgba(120, 60, 70, 0.22); +} +/* 游戏卡片:粉霞图标区,右上角落一朵小樱花 */ +body.theme-sakura .game-card .icon-area { + background: linear-gradient(180deg, #ffe9ec, #fff6f5); + border-bottom: 1px solid var(--border); +} +body.theme-sakura .game-card .icon-area::after { + content: '🌸'; + position: absolute; + right: 8px; + top: 8px; + font-size: 14px; + opacity: 0.7; + transform: rotate(14deg); +} +body.theme-sakura .game-card .game-icon { + filter: drop-shadow(0 6px 12px rgba(217, 69, 95, 0.25)); +} +body.theme-sakura .game-card .price-tag { + background: var(--primary); + border-radius: 999px; +} +body.theme-sakura .game-card .owned-tag, +body.theme-sakura .game-card .free-tag { + border-radius: 999px; +} +body.theme-sakura .chip { + background: #fff; + border-radius: 999px; +} +body.theme-sakura .chip.on { + background: var(--primary); + color: #fff; + border-color: var(--primary); + box-shadow: 0 3px 8px rgba(217, 69, 95, 0.3); +} +body.theme-sakura ::-webkit-scrollbar-thumb { + background: #eeb7c2; + border-radius: 999px; +} + +/* ===================================================================== + 12. 星际深空 space —— 三层星野明暗闪烁,流星划过,太空舱按钮 + ===================================================================== */ +body.theme-space { + background-image: radial-gradient(1.6px 1.6px at 18% 22%, rgba(255, 255, 255, 0.8) 50%, transparent 51%), + radial-gradient(1.2px 1.2px at 66% 46%, rgba(255, 255, 255, 0.55) 50%, transparent 51%), + radial-gradient(2px 2px at 42% 78%, rgba(200, 225, 255, 0.6) 50%, transparent 51%), + radial-gradient(ellipse 60% 40% at 80% 110%, rgba(123, 108, 255, 0.22), transparent), + radial-gradient(ellipse 50% 36% at 8% -10%, rgba(89, 216, 255, 0.12), transparent); + background-size: 220px 220px, 150px 150px, 280px 280px, 100% 100%, 100% 100%; + letter-spacing: 0.3px; +} +/* 第二层星野:整体明暗呼吸,像星星在闪 */ +body.theme-space::before { + content: ''; + position: fixed; + inset: 0; + pointer-events: none; + z-index: 0; + background-image: radial-gradient(2px 2px at 30% 40%, rgba(255, 255, 255, 0.9) 50%, transparent 51%), + radial-gradient(1.4px 1.4px at 72% 18%, rgba(180, 220, 255, 0.8) 50%, transparent 51%), + radial-gradient(1.8px 1.8px at 86% 66%, rgba(255, 240, 200, 0.75) 50%, transparent 51%), + radial-gradient(1.2px 1.2px at 12% 82%, rgba(255, 255, 255, 0.7) 50%, transparent 51%); + background-size: 300px 300px, 240px 240px, 340px 340px, 200px 200px; + animation: space-twinkle 3.4s ease-in-out infinite alternate; +} +@keyframes space-twinkle { + from { + opacity: 0.3; + } + to { + opacity: 1; + } +} +/* 每隔几秒一颗流星划过 */ +body.theme-space::after { + content: ''; + position: fixed; + top: 0; + left: 0; + width: 130px; + height: 2px; + pointer-events: none; + z-index: 9990; + border-radius: 999px; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.9), transparent); + opacity: 0; + animation: space-meteor 8s linear infinite; +} +@keyframes space-meteor { + 0%, + 88% { + opacity: 0; + transform: translate(104vw, -6vh) rotate(-28deg); + } + 89% { + opacity: 1; + } + 97% { + opacity: 0.9; + } + 100% { + opacity: 0; + transform: translate(28vw, 54vh) rotate(-28deg); + } +} +body.theme-space .panel, +body.theme-space .card, +body.theme-space .modal { + border: 1px solid var(--border); + box-shadow: inset 0 0 30px rgba(123, 108, 255, 0.05), 0 6px 22px rgba(0, 0, 0, 0.4); +} +body.theme-space .card:hover { + transform: translateY(-4px); + border-color: var(--accent); + box-shadow: inset 0 0 30px rgba(89, 216, 255, 0.07), var(--glow); +} +/* 太空舱按钮:渐变描边 + 舱内紫光,悬停底部点火喷出金色尾焰 */ +body.theme-space .btn { + border: 1px solid transparent; + border-radius: 999px; + background: linear-gradient(var(--bg-card), var(--bg-card)) padding-box, + linear-gradient(135deg, var(--primary), var(--accent)) border-box; + color: #d6d1ff; + box-shadow: inset 0 0 14px rgba(123, 108, 255, 0.25); + transition: box-shadow 0.2s, transform 0.15s; +} +body.theme-space .btn:hover { + filter: none; + transform: translateY(-1px); + box-shadow: inset 0 0 18px rgba(123, 108, 255, 0.4), 0 6px 20px rgba(255, 177, 77, 0.35); +} +body.theme-space .btn:active { + transform: translateY(1px); + box-shadow: inset 0 0 20px rgba(123, 108, 255, 0.5); +} +body.theme-space .btn-accent { + background: linear-gradient(var(--bg-card), var(--bg-card)) padding-box, + linear-gradient(135deg, var(--accent), var(--primary-2)) border-box; + color: #aef; +} +body.theme-space .btn-ghost { + background: transparent; + border: 1px dashed var(--border); + color: var(--text-dim); + box-shadow: none; +} +body.theme-space .btn-ghost:hover { + border-style: solid; + border-color: var(--primary); + color: #d6d1ff; +} +body.theme-space .input { + background: rgba(8, 8, 26, 0.7); + border-radius: 10px; +} +body.theme-space .input:focus { + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(123, 108, 255, 0.2), inset 0 0 12px rgba(123, 108, 255, 0.1); +} +/* 标题:一颗缓缓浮动的星球 */ +body.theme-space .page-title::before { + content: '🪐'; + width: auto; + height: auto; + background: none; + box-shadow: none; + font-size: 20px; + animation: space-float 4s ease-in-out infinite; +} +@keyframes space-float { + 50% { + transform: translateY(-4px) rotate(-8deg); + } +} +body.theme-space .nav { + background: rgba(8, 8, 26, 0.72); + backdrop-filter: blur(12px); + border-bottom: none; +} +body.theme-space .nav::after { + content: ''; + display: block; + height: 1px; + background: linear-gradient(90deg, transparent, var(--primary), var(--accent), transparent); +} +body.theme-space .nav .link { + border-radius: 999px; +} +body.theme-space .nav .link.active { + background: linear-gradient(135deg, rgba(123, 108, 255, 0.25), rgba(89, 216, 255, 0.18)); + box-shadow: inset 0 0 0 1px rgba(123, 108, 255, 0.5); + color: #d6d1ff; +} +body.theme-space .brand-name { + background: linear-gradient(90deg, #a99cff, #59d8ff); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} +body.theme-space .tag { + border-radius: 999px; + background: rgba(123, 108, 255, 0.08); +} +body.theme-space .chip { + border-radius: 999px; +} +body.theme-space .chip.on { + background: linear-gradient(135deg, rgba(123, 108, 255, 0.3), rgba(89, 216, 255, 0.2)); + border-color: var(--primary); + color: #d6d1ff; + box-shadow: none; +} +body.theme-space .toast { + border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.16); +} +body.theme-space .avatar { + background: var(--bg-card); +} +/* 游戏卡片:图标区就是一小片星空 */ +body.theme-space .game-card .icon-area { + background: radial-gradient(1.4px 1.4px at 22% 30%, rgba(255, 255, 255, 0.8) 50%, transparent 51%), + radial-gradient(1.2px 1.2px at 68% 18%, rgba(255, 255, 255, 0.6) 50%, transparent 51%), + radial-gradient(1.6px 1.6px at 82% 62%, rgba(180, 220, 255, 0.7) 50%, transparent 51%), + linear-gradient(200deg, rgba(123, 108, 255, 0.25), rgba(8, 8, 26, 0.2)); + background-size: 90px 90px, 70px 70px, 110px 110px, 100% 100%; + border-bottom: 1px solid var(--border); +} +body.theme-space .game-card .game-icon { + filter: drop-shadow(0 0 12px rgba(123, 108, 255, 0.55)); +} +body.theme-space .game-card .price-tag { + background: linear-gradient(135deg, var(--primary), var(--primary-2)); +} +body.theme-space ::-webkit-scrollbar-thumb { + background: #4b44a0; + border-radius: 999px; +} diff --git a/src/views/Battle.vue b/src/views/Battle.vue new file mode 100644 index 0000000..8eb1a89 --- /dev/null +++ b/src/views/Battle.vue @@ -0,0 +1,271 @@ + + + + + diff --git a/src/views/BattleRoom.vue b/src/views/BattleRoom.vue new file mode 100644 index 0000000..279c435 --- /dev/null +++ b/src/views/BattleRoom.vue @@ -0,0 +1,618 @@ + + + + + diff --git a/src/views/Chat.vue b/src/views/Chat.vue new file mode 100644 index 0000000..0e63782 --- /dev/null +++ b/src/views/Chat.vue @@ -0,0 +1,484 @@ + + + + + diff --git a/src/views/GameCenter.vue b/src/views/GameCenter.vue new file mode 100644 index 0000000..27923e7 --- /dev/null +++ b/src/views/GameCenter.vue @@ -0,0 +1,295 @@ + + + + + diff --git a/src/views/GamePlay.vue b/src/views/GamePlay.vue new file mode 100644 index 0000000..f3e2656 --- /dev/null +++ b/src/views/GamePlay.vue @@ -0,0 +1,1327 @@ + + + + + diff --git a/src/views/Lobby.vue b/src/views/Lobby.vue new file mode 100644 index 0000000..bdaddc8 --- /dev/null +++ b/src/views/Lobby.vue @@ -0,0 +1,473 @@ + + + + + diff --git a/src/views/Login.vue b/src/views/Login.vue new file mode 100644 index 0000000..68a66f0 --- /dev/null +++ b/src/views/Login.vue @@ -0,0 +1,161 @@ + + + + + diff --git a/src/views/Profile.vue b/src/views/Profile.vue new file mode 100644 index 0000000..149eec6 --- /dev/null +++ b/src/views/Profile.vue @@ -0,0 +1,492 @@ + + + + + diff --git a/src/views/Rank.vue b/src/views/Rank.vue new file mode 100644 index 0000000..5a5a839 --- /dev/null +++ b/src/views/Rank.vue @@ -0,0 +1,147 @@ + + + + + diff --git a/src/views/SignIn.vue b/src/views/SignIn.vue new file mode 100644 index 0000000..bf2a2c4 --- /dev/null +++ b/src/views/SignIn.vue @@ -0,0 +1,279 @@ + + + + + diff --git a/src/views/Vip.vue b/src/views/Vip.vue new file mode 100644 index 0000000..73da8dc --- /dev/null +++ b/src/views/Vip.vue @@ -0,0 +1,336 @@ + + + + + diff --git a/src/views/admin/AdminAI.vue b/src/views/admin/AdminAI.vue new file mode 100644 index 0000000..533843f --- /dev/null +++ b/src/views/admin/AdminAI.vue @@ -0,0 +1,293 @@ + + + + + diff --git a/src/views/admin/AdminDashboard.vue b/src/views/admin/AdminDashboard.vue new file mode 100644 index 0000000..fdd4d26 --- /dev/null +++ b/src/views/admin/AdminDashboard.vue @@ -0,0 +1,358 @@ + + + + + diff --git a/src/views/admin/AdminGames.vue b/src/views/admin/AdminGames.vue new file mode 100644 index 0000000..7529f5d --- /dev/null +++ b/src/views/admin/AdminGames.vue @@ -0,0 +1,439 @@ + + + + + diff --git a/src/views/admin/AdminLayout.vue b/src/views/admin/AdminLayout.vue new file mode 100644 index 0000000..780fada --- /dev/null +++ b/src/views/admin/AdminLayout.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/src/views/admin/AdminOrders.vue b/src/views/admin/AdminOrders.vue new file mode 100644 index 0000000..2fed032 --- /dev/null +++ b/src/views/admin/AdminOrders.vue @@ -0,0 +1,232 @@ + + + + + diff --git a/src/views/admin/AdminThemes.vue b/src/views/admin/AdminThemes.vue new file mode 100644 index 0000000..4f575f5 --- /dev/null +++ b/src/views/admin/AdminThemes.vue @@ -0,0 +1,164 @@ + + + + + diff --git a/src/views/admin/AdminUsers.vue b/src/views/admin/AdminUsers.vue new file mode 100644 index 0000000..b4b6d56 --- /dev/null +++ b/src/views/admin/AdminUsers.vue @@ -0,0 +1,596 @@ + + + + + diff --git a/src/views/admin/AdminVersion.vue b/src/views/admin/AdminVersion.vue new file mode 100644 index 0000000..0298ce7 --- /dev/null +++ b/src/views/admin/AdminVersion.vue @@ -0,0 +1,228 @@ + + +