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