diff --git a/mute-btn.png b/mute-btn.png new file mode 100644 index 0000000..2bdcc4b Binary files /dev/null and b/mute-btn.png differ diff --git a/star-raid/css/style.css b/star-raid/css/style.css index 339fca7..6a293e5 100644 --- a/star-raid/css/style.css +++ b/star-raid/css/style.css @@ -102,6 +102,12 @@ h1.logo{font-size:clamp(20px,5.4vw,44px);color:#fff;letter-spacing:4px;line-heig #tb-pause{position:absolute;top:6px;right:6px;z-index:46;font:inherit;font-size:11px;width:40px;height:40px; background:rgba(16,26,51,.7);border:3px solid var(--line);color:var(--ink);letter-spacing:1px} #tb-pause:active{background:var(--cyan);color:#000} +#b-mute{position:absolute;top:10px;right:10px;z-index:5;width:42px;height:42px; + display:flex;align-items:center;justify-content:center; + background:rgba(16,26,51,.85);border:3px solid var(--line);color:var(--ink);cursor:pointer;padding:0;line-height:1} +#b-mute canvas{image-rendering:pixelated;pointer-events:none} +#b-mute:hover{border-color:var(--cyan);color:var(--cyan)} +#b-mute:active{background:var(--cyan);color:#000} .blink{animation:blk 1s steps(2) infinite} @keyframes blk{50%{opacity:.25}} .help-list{font-size:clamp(7px,1.5vw,10px);color:var(--ink);line-height:2.2;letter-spacing:2px;text-align:left} diff --git a/star-raid/index.html b/star-raid/index.html index 59b2317..83778b5 100644 --- a/star-raid/index.html +++ b/star-raid/index.html @@ -63,6 +63,7 @@
+
PIXEL ROGUELIKE SHMUP

STAR
RAID

星尘突击队
diff --git a/star-raid/js/audio.js b/star-raid/js/audio.js index 8136c58..82d8109 100644 --- a/star-raid/js/audio.js +++ b/star-raid/js/audio.js @@ -4,9 +4,11 @@ const AU={ctx:null,master:null,mgain:null,simMute:false, init(){ if(this.ctx) return; try{ this.ctx=new (window.AudioContext||window.webkitAudioContext)(); this.master=this.ctx.createGain(); this.master.gain.value=0.5; this.master.connect(this.ctx.destination); - this.mgain=this.ctx.createGain(); this.mgain.gain.value=SAVE.music?0.16:0; this.mgain.connect(this.master); + this.mgain=this.ctx.createGain(); this.mgain.gain.value=(SAVE.mute||!SAVE.music)?0:0.16; this.mgain.connect(this.master); }catch(e){} }, + /* 音乐音量统一闸门:静音 或 音乐关 都归零(SAVE.mute 管 SFX 由 tone/noise 自查) */ + applyMute(){ if(this.mgain) this.mgain.gain.value=(SAVE.mute||!SAVE.music)?0:0.16; }, now(){return this.ctx?this.ctx.currentTime:0;}, env(g,t,a,d,v){g.gain.setValueAtTime(0,t);g.gain.linearRampToValueAtTime(v,t+a);g.gain.exponentialRampToValueAtTime(0.001,t+a+d);}, tone(f,type,dur,vol=0.2,slide=0){ if(!this.ctx||SAVE.mute||this.simMute)return; diff --git a/star-raid/js/core.js b/star-raid/js/core.js index c9d69ec..e9e99ae 100644 --- a/star-raid/js/core.js +++ b/star-raid/js/core.js @@ -53,7 +53,7 @@ addEventListener("keydown",e=>{ if(GAMEKEYS.includes(e.code))e.preventDefault(); KEYS[e.code]=true; AU.init(); if(AU.ctx&&AU.ctx.state==="suspended")AU.ctx.resume(); - if(e.code==="KeyM"){SAVE.music=!SAVE.music;if(AU.mgain)AU.mgain.gain.value=SAVE.music?0.16:0;persist();} + if(e.code==="KeyM"){SAVE.music=!SAVE.music;AU.applyMute();persist();} if(e.code==="Escape"&&UI==="demoSim"){exitDemoSim();return;} if(e.code==="KeyP"||e.code==="Escape")togglePause(); if(UI==="cards"&&["Digit1","Digit2","Digit3"].includes(e.code))pickCard(+e.code.slice(-1)-1); diff --git a/star-raid/js/net.js b/star-raid/js/net.js index 2baecca..d7e8ba1 100644 --- a/star-raid/js/net.js +++ b/star-raid/js/net.js @@ -51,8 +51,11 @@ const NET = { SAVE.codex = {}; SAVE.coins = 0; SAVE.ships = ["falcon"]; SAVE.m = {}; SAVE.best = { s0: 0, s1: 0, e0: 0, e1: 0, stage: 0 }; SAVE.rush = {}; + SAVE.mute = false; SAVE.music = true; // 音频开关也防串号,恢复默认 if (typeof SETUP !== "undefined") SETUP.ship = "falcon"; persist(); + if (typeof updateMuteBtn === "function") updateMuteBtn(); + if (typeof AU !== "undefined") AU.applyMute(); }, /* 应用服务端返回的用户数据:图鉴以服务端为准【替换】本地 @@ -72,10 +75,14 @@ const NET = { SAVE.m = Object.assign({}, d.state.m); SAVE.best = Object.assign({ s0: 0, s1: 0, e0: 0, e1: 0, stage: 0 }, d.state.best); SAVE.rush = Object.assign({}, d.state.rush); + SAVE.mute = d.state.mute === true; // 音频开关跟随账号(缺省不静音) + SAVE.music = d.state.music !== false; // 缺省音乐开 if (d.state.ship && SAVE.ships.includes(d.state.ship) && typeof SETUP !== "undefined") SETUP.ship = d.state.ship; } persist(); this._mutePush = false; + if (typeof updateMuteBtn === "function") updateMuteBtn(); // 刷新基地静音按钮图标 + if (typeof AU !== "undefined") AU.applyMute(); if (!d.state) this.pushState(); // 服务端无存档(老账号首登):把本地存档上传初始化 }, @@ -90,6 +97,7 @@ const NET = { coins: SAVE.coins, ships: SAVE.ships, ship: typeof SETUP !== "undefined" ? SETUP.ship : "falcon", m: SAVE.m, best: SAVE.best, rush: SAVE.rush || {}, + mute: !!SAVE.mute, music: SAVE.music !== false, } }).catch(() => { /* 网络失败静默:本地存档仍在 */ }); }, 600); }, diff --git a/star-raid/js/ui.js b/star-raid/js/ui.js index eb14bf1..e6e081c 100644 --- a/star-raid/js/ui.js +++ b/star-raid/js/ui.js @@ -14,6 +14,49 @@ $("b-hangar").onclick=()=>{AU.init();buildShop();show("o-hangar");}; $("b-codex").onclick=()=>{AU.init();buildCodex();show("o-codex");}; $("b-rush").onclick=()=>{AU.init();buildRush();show("o-rush");}; $("b-back6").onclick=()=>{UI="title";show("o-title");}; +/* 基地右上角:静音开关(SFX tone/noise 自查 SAVE.mute;音乐走 AU.applyMute 闸门;persist 触发云同步) + 图标为手绘像素画:开声=青喇叭+绿音波 / 静音=灰喇叭+红叉 */ +const SPK_ON=[ // 音波三根竖线:从左到右由短到长 +".....SS.........", +"....SSS.........", +"..SSSSS........W", +"SSSSSSS......W.W", +"SSSSSSSS...W.W.W", +"SSSSSSSSS..W.W.W", +"SSSSSSSS...W.W.W", +"SSSSSSS......W.W", +"..SSSSS........W", +"....SSS.........", +".....SS........."]; +const SPK_OFF=[ +".....SS.........", +"....SSS.........", +"..SSSSS.........", +"SSSSSSS...X...X.", +"SSSSSSSS...X.X..", +"SSSSSSSSS...X...", +"SSSSSSSS...X.X..", +"SSSSSSS...X...X.", +"..SSSSS.........", +"....SSS.........", +".....SS........."]; +function updateMuteBtn(){ + const c=$("b-mute-icon");if(!c)return; + const g=c.getContext("2d");g.clearRect(0,0,c.width,c.height); + const art=SAVE.mute?SPK_OFF:SPK_ON; + for(let y=0;y{ + SAVE.mute=!SAVE.mute;persist(); + AU.init();if(AU.ctx&&AU.ctx.state==="suspended")AU.ctx.resume(); + AU.applyMute();updateMuteBtn(); + if(!SAVE.mute)SFX.pick(); // 解除静音给个反馈音(静音时 tone 自查抑制) +}; +updateMuteBtn(); $("b-back4").onclick=()=>{UI="title";bestLine();show("o-title");}; $("b-back5").onclick=exitDemoSim; /* 图鉴测试模式(隐藏入口):连点图鉴标题“军”字10次开关。 diff --git a/star-raid/server/api.js b/star-raid/server/api.js index df79f24..c618123 100644 --- a/star-raid/server/api.js +++ b/star-raid/server/api.js @@ -22,7 +22,7 @@ const RUN_MODES = new Set(["stage", "endless", "rush"]); const SHIP_KEYS = new Set(["falcon", "ghost", "bastion", "valkyrie", "asura"]); const PASSIVE_KEYS = new Set(["fire", "crit", "shield", "nano", "mag", "bounty", "odcore", "ghost"]); const META_MAX = { weapon: 2, armor: 2, hpup: 2, luck: 2, wallet: 3, mag2: 2, odstart: 1 }; -const DEFAULT_STATE = { coins: 0, ships: ["falcon"], ship: "falcon", m: {}, best: { s0: 0, s1: 0, e0: 0, e1: 0, stage: 0 }, rush: {} }; +const DEFAULT_STATE = { coins: 0, ships: ["falcon"], ship: "falcon", m: {}, best: { s0: 0, s1: 0, e0: 0, e1: 0, stage: 0 }, rush: {}, mute: false, music: true }; const qInsUser = db.prepare("INSERT INTO users(username,email,pass_hash,created_at) VALUES(?,?,?,?)"); const qInsStats = db.prepare("INSERT OR IGNORE INTO user_stats(user_id) VALUES(?)"); @@ -76,7 +76,10 @@ function vState(s) { const rush = {}; if (s.rush && typeof s.rush === "object") for (const k of Object.keys(s.rush).slice(0, 32)) if (/^\d{1,2}$/.test(k) && vInt(s.rush[k], 0, 8640000)) rush[k] = s.rush[k]; - return { coins: s.coins, ships, ship, m, best, rush }; + /* 音频开关:缺省=不静音/音乐开(老存档无此字段) */ + const mute = s.mute === true; + const music = s.music !== false; + return { coins: s.coins, ships, ship, m, best, rush, mute, music }; } /* 无尽模式断点存档校验(波次检查点快照;装备 key 复用图鉴白名单) */