基地静音开关:新增 b-mute 像素图标按钮、AU.applyMute 音乐闸门、mute/music 音频状态云端同步(server DEFAULT_STATE/vState 扩展,含 mute-btn.png 参考图)

This commit is contained in:
JOJO 2026-08-06 13:17:37 +08:00
parent d328c02366
commit adc4fb36d3
8 changed files with 67 additions and 4 deletions

BIN
mute-btn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

View File

@ -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}

View File

@ -63,6 +63,7 @@
<!-- 标题 -->
<div class="ovl hide" id="o-title">
<button id="b-mute" title="静音开关(跟随账号云端保存)"><canvas id="b-mute-icon" width="36" height="26"></canvas></button>
<div class="tag">PIXEL ROGUELIKE SHMUP</div>
<h1 class="logo">STAR<br>RAID</h1>
<div class="logo-cn">星尘突击队</div>

View File

@ -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;

View File

@ -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);

View File

@ -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);
},

View File

@ -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<art.length;y++)for(let x=0;x<art[y].length;x++){
const ch=art[y][x];if(ch===".")continue;
g.fillStyle=SAVE.mute?(ch==="X"?"#ff5566":"#7f8cb8"):(ch==="W"?"#5cff9d":"#41e6ff");
g.fillRect(x*2+2,y*2+2,2,2);
}
}
$("b-mute").onclick=()=>{
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

View File

@ -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 复用图鉴白名单) */