From 9edc366cf97cc39ca8dc0d571d4161f57581d5a8 Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Thu, 6 Aug 2026 12:15:17 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9B=BE=E9=89=B4=E6=B5=8B=E8=AF=95=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F+=E5=B7=A1=E9=A3=9E=E5=BC=B9=E6=96=B0=E6=AD=A6?= =?UTF-8?q?=E5=99=A8+=E5=AD=98=E6=A1=A3=E4=BA=91=E5=90=8C=E6=AD=A5+?= =?UTF-8?q?=E6=9C=BA=E5=BA=93=E5=90=88=E5=B9=B6+=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E6=97=A0=E5=B0=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ①图鉴隐藏测试模式:连点标题'军'字10次开关,纯内存标志位全部军械可见可演示,不动SAVE.codex(persist/submitRun读它,动了会污染后端),刷新失效 ②24件主武器说明文案全部替换(用户提供梗文案);演示页介绍单行显示、去掉类别前缀、删'实战模拟·自动驾驶'提示行 ③新主武器'巡飞弹'(loiter):慢攻速追踪导弹(制导引擎新增maxSpd上限5保持中速),命中留火场(zones新kind:fire纯粒子火舌,无圆形区域)+分裂4枚小炸弹快爆;新精灵loms;CODEX_KEYS同步63 ④存档云同步:星币/机体/选中机体/永久强化/本地纪录上服务端(player_state表+POST /api/state+vState白名单清洗);login/me/register响应带state;客户端persist()钩子防抖600ms自动上传;服务端整体替换本地防串号,登出全清;老账号首登自动上传本地存档迁移 ⑤机体选择/购买从出击准备并入机库强化(buildShips);出击默认无尽模式 服务端已重启生效;/api/state全链路在独立测试库curl验证通过(合法推送/回读/非法拦截清洗/401) --- AGENTS.md | 4 +-- star-raid/DEPLOY.md | 2 +- star-raid/index.html | 20 +++++++-------- star-raid/js/combat.js | 6 ++--- star-raid/js/config.js | 3 ++- star-raid/js/core.js | 15 ++++++++++++ star-raid/js/data.js | 54 +++++++++++++++++++++-------------------- star-raid/js/net.js | 38 ++++++++++++++++++++++++++--- star-raid/js/render.js | 5 ++++ star-raid/js/sprites.js | 1 + star-raid/js/ui.js | 31 +++++++++++++++++------ star-raid/server/api.js | 51 +++++++++++++++++++++++++++++++++++--- star-raid/server/db.js | 5 ++++ 13 files changed, 177 insertions(+), 58 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1336892..0ef9150 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -99,5 +99,5 @@ star-raid/ - 结算上报:`submitRun()`(ui.js)在 endRun 与 rush 胜利(enemies.js killBoss)两处调用;`G.submitted` 防重 - 图鉴:服务端 `codex_unlocks` 为准,登陆时与本地 SAVE.codex 并集合并;未收集=剪影+???不可演示 - 背景规则:render() 用 battleUI() 门控;**主页/基地背景=实战模拟**(home.js startHomeSim:随机3主Lv3+3副Lv3+2僚Lv2,G.homeSim 标记,复用 demoSim 自动驾驶刷怪,AU.simMute 静音,不伤/不计分/不上报,主循环看门狗自动重建);其余空闲=星空。离场必须 G=null(b-home/b-quit/buildRush 已处理) -- 新增装备时必须同步 `server/api.js` 的 CODEX_KEYS 白名单(现 24+17+13+8=62) -- 星币/机库/机体仍存 localStorage 不同步(v1 边界,见 DEPLOY.md) +- 新增装备时必须同步 `server/api.js` 的 CODEX_KEYS 白名单(现 25+17+13+8=63) +- 星币/机体/选中机体/永久强化/本地纪录已上服务端(POST /api/state → player_state 表,服务端权威整体替换,登出清空;客户端 persist() 钩子防抖600ms上传) diff --git a/star-raid/DEPLOY.md b/star-raid/DEPLOY.md index e6cf426..23f70c6 100644 --- a/star-raid/DEPLOY.md +++ b/star-raid/DEPLOY.md @@ -53,7 +53,7 @@ server { - 邮箱仅作唯一标识,**无验证邮件、无找回密码**(需 SMTP 服务,留 v2) - **反作弊仅为基础校验**(数值范围/限流):纯客户端游戏无法杜绝作弊,有志者改内存照样能伪造高分。如需严肃竞技,要加服务端回放校验(大工程) -- 星币/机库强化/机体解锁仍存浏览器 localStorage,**不随账号同步**(跨设备冲突解决复杂,留 v2) +- 星币/机体/永久强化/本地纪录已上服务端(`POST /api/state`,`player_state` 表,2026-08-06):登陆时服务端存档整体替换本地;最后登录的设备覆盖云端(无冲突合并);音乐/音效开关等设置仍存 localStorage 不同步 - 排行榜 = 全服单榜,无分页/赛季 ## 数据维护 diff --git a/star-raid/index.html b/star-raid/index.html index 5113880..1034f32 100644 --- a/star-raid/index.html +++ b/star-raid/index.html @@ -83,11 +83,9 @@
— 出击准备 —
选择模式
-
闯关模式
6 大关卡
击破母巢核心
即获胜利
-
无尽模式
无限波次
突变词缀
冲击最高分
+
闯关模式
6 大关卡
击破母巢核心
即获胜利
+
无尽模式
无限波次
突变词缀
冲击最高分
-
选择机体
-
难度
@@ -117,9 +115,12 @@
-
— 机库 · 永久强化 —
+
— 机库 · 强化 —
星币 ✦ 0
-
+
机体(选择 / 解锁)
+
+
永久强化
+
@@ -144,8 +145,8 @@
-
— 军械图鉴 —
-
已收集 0/0 · 点击可实时演示
+
械图鉴 —
+
已收集 0/0 · 点击可实时演示
@@ -154,8 +155,7 @@
演示
-
-
实战模拟 · 自动驾驶 · 触屏/方向键可接管
+
diff --git a/star-raid/js/combat.js b/star-raid/js/combat.js index 6a6d627..31f5ab3 100644 --- a/star-raid/js/combat.js +++ b/star-raid/js/combat.js @@ -93,7 +93,7 @@ function updateBullets(){ if(b.softH){ // 星云奥秘:缓慢转向,不加速 const na=c+clamp(d,-0.05,0.05),s=Math.hypot(b.vx,b.vy); b.vx=Math.cos(na)*s;b.vy=Math.sin(na)*s; - }else{const na=c+clamp(d,-0.16,0.16),s=Math.min(6,Math.hypot(b.vx,b.vy)+0.08); + }else{const na=c+clamp(d,-0.16,0.16),s=Math.min(b.maxSpd||6,Math.hypot(b.vx,b.vy)+0.08); b.vx=Math.cos(na)*s;b.vy=Math.sin(na)*s;}}} if(b.back){b.bt--;if(b.bt<=0&&b.vy<0){b.vy*=-1;b.vx*=-0.4;}} if(b.curve){const c=Math.atan2(b.vy,b.vx)+b.curve,s=Math.hypot(b.vx,b.vy)||0.1; @@ -232,7 +232,7 @@ function collide(){ areaDamage(b.x,b.y,18,b.dmg*0.5); explodeFx(b.x,b.y,"plasma",0.8);} SFX.hit(); - if(b.dead){if(b.t==="sg"||b.t==="ho"||b.t==="bb"||b.t==="am"||b.t==="tb"||b.t==="tb2"||b.t==="dw"||b.t==="ph"||b.t==="nb"||b.t==="ex")explodeOnce(b);break;} + if(b.dead){if(b.t==="sg"||b.t==="ho"||b.t==="bb"||b.t==="am"||b.t==="tb"||b.t==="tb2"||b.t==="dw"||b.t==="ph"||b.t==="nb"||b.t==="ex"||b.t==="lm"||b.t==="lb")explodeOnce(b);break;} } } if(!b.dead&&G.boss){ @@ -240,7 +240,7 @@ function collide(){ if(bossHit(b.x,b.y,b.r)){ if(b.pierce){if(!b.hits)b.hits={};if(!b.hits.boss){b.hits.boss=1;hurtBoss(b.dmg);}} else{b.dead=true; - if(b.t==="sg"||b.t==="ho"||b.t==="bb"||b.t==="am"||b.t==="tb"||b.t==="tb2"||b.t==="dw"||b.t==="ph"||b.t==="nb"||b.t==="ex")explodeOnce(b); + if(b.t==="sg"||b.t==="ho"||b.t==="bb"||b.t==="am"||b.t==="tb"||b.t==="tb2"||b.t==="dw"||b.t==="ph"||b.t==="nb"||b.t==="ex"||b.t==="lm"||b.t==="lb")explodeOnce(b); else hurtBoss(b.dmg);} SFX.hit(); } diff --git a/star-raid/js/config.js b/star-raid/js/config.js index 5bfd1b5..6dd8b00 100644 --- a/star-raid/js/config.js +++ b/star-raid/js/config.js @@ -24,7 +24,8 @@ function loadSave(){ SAVE.m=SAVE.m||{}; SAVE.ships=SAVE.ships||["falcon"]; SAVE.best=SAVE.best||{s0:0,s1:0,e0:0,e1:0,stage:0}; SAVE.codex=SAVE.codex||{}; } -function persist(){try{localStorage.setItem(SAVE_KEY,JSON.stringify(SAVE));}catch(e){}} +function persist(){try{localStorage.setItem(SAVE_KEY,JSON.stringify(SAVE));}catch(e){} + if(typeof NET!=="undefined")NET.pushState();} // 登录态下云端同步(NET 内防抖;未登录自动跳过) loadSave(); /* ---------------- 平衡数值总表 ---------------- diff --git a/star-raid/js/core.js b/star-raid/js/core.js index 558b90f..c9d69ec 100644 --- a/star-raid/js/core.js +++ b/star-raid/js/core.js @@ -251,6 +251,16 @@ function explodeBullet(b){ areaDamage(b.x,b.y,b.aoe||26,b.dmg); explodeFx(b.x,b.y,"executor",1); SFX.boom(true);G.shake=Math.max(G.shake,8); + }else if(b.t==="lm"){ // 巡飞弹:原地火场(kind:fire)+ 分裂4枚小炸弹很快起爆 + areaDamage(b.x,b.y,b.zone||20,b.dmg); + G.zones.push({x:b.x,y:b.y,r:b.zone||20,dps:b.dmg*0.12,life:b.fire||95,col:"#ff8a2e",kind:"fire"}); + for(let i=0;i<4;i++){const la=-Math.PI/2+(i-1.5)*0.85+rnd(-0.2,0.2); + pbul(b.x,b.y,Math.cos(la)*2.8,Math.sin(la)*2.8,b.dmg*0.35,{t:"lb",r:2,bt:12+rndi(0,6),aoe:12,trailCol:"#ff9a3d"});} + explodeFx(b.x,b.y,"fire",1.2); + SFX.boom(false);G.shake=Math.max(G.shake,3); + }else if(b.t==="lb"){ // 巡飞弹小炸弹:小范围快爆 + areaDamage(b.x,b.y,b.aoe||12,b.dmg); + explodeFx(b.x,b.y,"fire",0.6);SFX.boom(false); }else if(b.t==="tb"||b.t==="tb2"){ areaDamage(b.x,b.y,b.aoe||b.zone||24,b.dmg); explodeFx(b.x,b.y,"boom",1); @@ -358,6 +368,11 @@ function fireWeapons(){ for(let i=0;i({n:1+Math.floor(w/2),dmg:7*(1+.22*(w-1)),int:8,spd:5.4,gap:6})}, - shotgun:{name:"超级霰弹",spr:"pelt",rare:0,beh:"streams",ds:"弹丸只从两翼倾泻,中央中空", + shotgun:{name:"超级霰弹",spr:"pelt",rare:0,beh:"streams",ds:"“友谊颗粒”", lvl:w=>({n:5+2*w,dmg:4.2*(1+.18*(w-1)),int:22,spd:5.5,fan:0.5,gap:2,skipMid:1})}, - needle:{name:"蜂针刺弹",spr:"ndl",rare:0,beh:"streams",ds:"淬毒细针,命中后持续蚀甲", + needle:{name:"蜂针刺弹",spr:"ndl",rare:0,beh:"streams",ds:"这可是吐了剧毒的飞刀", lvl:w=>({n:1+Math.floor(w/3),dmg:2.6*(1+.2*(w-1)),int:3,spd:7,gap:4,poison:100})}, - flame:{name:"火焰喷射器",icon:"flame",rare:0,beh:"flame",ds:"周期性喷吐的宽幅烈焰,近身湮灭", + flame:{name:"火焰喷射器",icon:"flame",rare:0,beh:"flame",ds:"让火焰净化一切", lvl:w=>({n:3+Math.floor(w/2),dmg:1.1*(1+.22*(w-1)),range:70+8*w,burst:34+4*w,cd:Math.max(46,74-5*w)})}, - shotgun2:{name:"霜冻霰弹",spr:"shard",rare:1,beh:"streams",ds:"冰晶霰弹,大面积冻结减速", + shotgun2:{name:"霜冻霰弹",spr:"shard",rare:1,beh:"streams",ds:"冷冷冷", lvl:w=>({n:3+w,dmg:3*(1+.18*(w-1)),int:23,spd:5.2,fan:0.42,gap:2,slow:80+25*w})}, - tesla:{name:"电弧发生器",icon:"chain",rare:1,beh:"tesla",ds:"自动电击周围敌人,无处可逃", + tesla:{name:"电弧发生器",icon:"chain",rare:1,beh:"tesla",ds:"?!电电!?", lvl:w=>({n:1+Math.floor(w/2),dmg:8*(1+.25*(w-1)),int:44-2*w,range:110})}, - railgun:{name:"电磁炮",icon:"rail",rare:2,beh:"rail",ds:"蓄力磁轨贯穿敌阵,命中留下电浆区", + railgun:{name:"电磁炮",icon:"rail",rare:2,beh:"rail",ds:"带轮子的垃圾桶", lvl:w=>({dmg:55*(1+.3*(w-1)),charge:120-10*w,spd:11,zone:24+4*w})}, - deathray:{name:"死光",icon:"laser",rare:2,beh:"deathray",ds:"蓄力后自动锁敌的死亡射线", + deathray:{name:"死光",icon:"laser",rare:2,beh:"deathray",ds:"记得带镜子", lvl:w=>({dps:0.9*(1+.28*(w-1)),charge:160-12*w,beam:55,width:5+2*w})}, - crescent:{name:"月牙斩",spr:"cres",rare:1,beh:"crescent",ds:"巨型旋刃缓慢斩出,贯穿敌阵", + crescent:{name:"月牙斩",spr:"cres",rare:1,beh:"crescent",ds:"青豆?", lvl:w=>({dmg:44*(1+.24*(w-1)),int:120,spd:3.4})}, - fission:{name:"分裂弹",spr:"fiss",rare:1,beh:"fission",ds:"飞行一段距离后炸裂为散射弹幕", + fission:{name:"分裂弹",spr:"fiss",rare:1,beh:"fission",ds:"四阶村长", lvl:w=>({dmg:6*(1+.22*(w-1)),int:20,spd:5.2,split:3+Math.floor(w/2),splitAt:26})}, - frost:{name:"冰冻枪",spr:"icelance",rare:1,beh:"frost",ds:"贯穿冰枪,命中将敌人彻底冻结", + frost:{name:"冰冻枪",spr:"icelance",rare:1,beh:"frost",ds:"冷冷冷冷", lvl:w=>({dmg:6*(1+.2*(w-1)),int:16,spd:6,freeze:70+25*w})}, - swarm:{name:"蜂巢导弹",spr:"msl2",rare:1,beh:"homing",ds:"自动追踪的导弹蜂群,命中爆裂", + swarm:{name:"蜂巢导弹",spr:"msl2",rare:1,beh:"homing",ds:"不可用于对付肉山", lvl:w=>({n:1+w,dmg:2.2*(1+.2*(w-1)),int:22,spd:3.2,aoe:10})}, - bubble:{name:"重力泡泡",spr:"bub",rare:1,beh:"bubble",ds:"缓慢漂浮的大泡泡,爆裂冲击", + bubble:{name:"重力泡泡",spr:"bub",rare:1,beh:"bubble",ds:"天呐!是海绵宝宝", lvl:w=>({dmg:18*(1+.25*(w-1)),int:26,spd:1.4,aoe:30+4*w})}, - starfall:{name:"流星火雨",spr:"met",rare:2,beh:"starfall",ds:"流星自脚下逆冲而起,轰炸前方空域", + starfall:{name:"流星火雨",spr:"met",rare:2,beh:"starfall",ds:"天火", lvl:w=>({n:3+Math.floor(w/2),dmg:16*(1+.25*(w-1)),int:150-8*w,aoe:16})}, - boomerang:{name:"回旋镖",spr:"boomer",rare:0,beh:"boom",ds:"亮蓝回旋刃,远射贯穿后回旋返航", + boomerang:{name:"回旋镖",spr:"boomer",rare:0,beh:"boom",ds:"但愿不会打到自己", lvl:w=>({n:1+Math.floor(w/2),dmg:7*(1+.22*(w-1)),int:21,spd:4.8})}, - singularity:{name:"奇点炮",spr:"sing",rare:2,beh:"sing",ds:"发射微型黑洞,牵引后剧烈坍缩", + singularity:{name:"奇点炮",spr:"sing",rare:2,beh:"sing",ds:"雷工蛋?", lvl:w=>({dmg:40*(1+.3*(w-1)),int:200,pull:88,aoe:62,fuse:160})}, // 牵引/爆炸范围与射程增强 - wavecannon:{name:"波动炮",spr:"waveb",rare:2,beh:"wave",ds:"蓄力斩出贯穿敌阵的剑气弧光", + wavecannon:{name:"波动炮",spr:"waveb",rare:2,beh:"wave",ds:"炼狱无双波动炮", lvl:w=>({dmg:32*(1+.3*(w-1)),charge:100-8*w,spd:5,width:14+6*w})}, - holy:{name:"圣光弹",spr:"holyo",rare:2,beh:"holy",ds:"爆裂为旋转生长的圣光十字", + holy:{name:"圣光弹",spr:"holyo",rare:2,beh:"holy",ds:"哇哦", lvl:w=>({dmg:13*(1+.25*(w-1)),int:65,spd:3,len:16+3*w})}, - thundercall:{name:"落雷术",spr:"thnd",rare:1,beh:"thunder",ds:"呼唤天雷轰击前方区域", + thundercall:{name:"落雷术",spr:"thnd",rare:1,beh:"thunder",ds:"何方道友在此渡劫", lvl:w=>({n:2+Math.floor(w/2),dmg:12*(1+.25*(w-1)),int:90-4*w,aoe:13})}, - arcshu:{name:"电弧手里剑",spr:"shuri",rare:2,beh:"shuriken",ds:"旋转雷刃一路放电,贯穿全场", + arcshu:{name:"电弧手里剑",spr:"shuri",rare:2,beh:"shuriken",ds:"这个很熟悉了吧", lvl:w=>({dmg:6*(1+.22*(w-1)),int:95-4*w,spd:2.2,zapDmg:5*(1+.25*(w-1)),pulse:7})}, // 电弧释放速度×3 - volt:{name:"电能机炮",spr:"voltb",rare:1,beh:"volt",ds:"高频电能弹,命中后电弧连锁跳跃", + volt:{name:"电能机炮",spr:"voltb",rare:1,beh:"volt",ds:"疑似电大", lvl:w=>({n:1+Math.floor(w/3),dmg:3.2*(1+.2*(w-1)),int:5,spd:6.2,gap:5,chain:2+Math.floor(w/2)})}, - dawn:{name:"黎明重炮",spr:"dawnsh",rare:2,beh:"dawn",ds:"长蓄力重炮,一炮轰出黎明", + dawn:{name:"黎明重炮",spr:"dawnsh",rare:2,beh:"dawn",ds:"祖宗发射器", lvl:w=>({dmg:70*(1+.3*(w-1)),charge:170-12*w,spd:5,aoe:30+3*w})}, - proto:{name:"协议武装",spr:"protb",rare:2,beh:"protocol",ds:"青蓝连发压制,周期性蓄能重炮", + proto:{name:"协议武装",spr:"protb",rare:2,beh:"protocol",ds:"搜索,歼灭", lvl:w=>({dmg:3*(1+.2*(w-1)),int:6,spd:6.5,heavy:120-8*w,hdmg:36*(1+.28*(w-1)),haoe:26+3*w})}, - nebula:{name:"星云奥秘",spr:"neb",rare:2,beh:"nebula",ds:"缓慢追踪的奥术漩涡,命中绽放群星", - lvl:w=>({dmg:12*(1+.25*(w-1)),int:100-5*w,spd:1.6,stars:10+2*w})} + nebula:{name:"星云奥秘",spr:"neb",rare:2,beh:"nebula",ds:"字面意思", + lvl:w=>({dmg:12*(1+.25*(w-1)),int:100-5*w,spd:1.6,stars:10+2*w})}, + loiter:{name:"巡飞弹",spr:"loms",rare:1,beh:"loiter",ds:"堵桥来", + lvl:w=>({dmg:22*(1+.25*(w-1)),int:68-5*w,spd:4.2,zone:20+3*w,fire:95+15*w})} }; const MAINS=Object.keys(WEAPONS); /* ================= 18 副武器 ================= */ diff --git a/star-raid/js/net.js b/star-raid/js/net.js index a917d42..c9c4c1e 100644 --- a/star-raid/js/net.js +++ b/star-raid/js/net.js @@ -3,7 +3,7 @@ const NET = { user: null, // {username,email} stats: null, // {bossKills,endlessHigh,runs} - codexTotal: 62, + codexTotal: 63, online: false, async api(path, method, body) { @@ -45,7 +45,12 @@ const NET = { async logout() { await this.api("/api/logout", "POST"); this.user = null; this.stats = null; this.online = false; - SAVE.codex = {}; persist(); // 防同机下一账号继承图鉴 + /* 防同机下一账号继承:图鉴/星币/机体/强化/纪录全部清空(user 已置空,persist 不会触发上传) */ + SAVE.codex = {}; + SAVE.coins = 0; SAVE.ships = ["falcon"]; SAVE.m = {}; + SAVE.best = { s0: 0, s1: 0, e0: 0, e1: 0, stage: 0 }; SAVE.rush = {}; + if (typeof SETUP !== "undefined") SETUP.ship = "falcon"; + persist(); }, /* 应用服务端返回的用户数据:图鉴以服务端为准【替换】本地 @@ -53,11 +58,38 @@ const NET = { _apply(d) { this.user = d.user; this.stats = d.stats; - this.codexTotal = d.codexTotal || 62; + this.codexTotal = d.codexTotal || 63; this.online = true; SAVE.codex = {}; if (Array.isArray(d.codex)) for (const k of d.codex) SAVE.codex[k] = true; + /* 服务端存档权威:整体替换本地(防串号,与图鉴同理);替换期间禁止回传 */ + this._mutePush = true; + if (d.state) { + SAVE.coins = d.state.coins; + SAVE.ships = d.state.ships.slice(); + 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); + if (d.state.ship && SAVE.ships.includes(d.state.ship) && typeof SETUP !== "undefined") SETUP.ship = d.state.ship; + } persist(); + this._mutePush = false; + if (!d.state) this.pushState(); // 服务端无存档(老账号首登):把本地存档上传初始化 + }, + + /* 存档云同步(防抖 600ms):星币/机体/选中机体/永久强化/本地纪录。任何 persist 都会触发 */ + _stateT: null, + pushState() { + if (!this.user || this._mutePush) return; + clearTimeout(this._stateT); + this._stateT = setTimeout(() => { + if (!this.user) return; + this.api("/api/state", "POST", { state: { + coins: SAVE.coins, ships: SAVE.ships, + ship: typeof SETUP !== "undefined" ? SETUP.ship : "falcon", + m: SAVE.m, best: SAVE.best, rush: SAVE.rush || {}, + } }).catch(() => { /* 网络失败静默:本地存档仍在 */ }); + }, 600); }, /* 结算上报(fire-and-forget):成功后回写最新统计 */ diff --git a/star-raid/js/render.js b/star-raid/js/render.js index 1685816..2ff7fb4 100644 --- a/star-raid/js/render.js +++ b/star-raid/js/render.js @@ -329,6 +329,11 @@ function render(){ if(G.time%9===0)G.parts.push({x:z.x+rnd(-z.r/2,z.r/2),y:z.y+rnd(-z.r/2,z.r/2),vx:0,vy:-0.2,life:10,col:"#43e8ff",sz:1}); continue; } + if(z.kind==="fire"){ // 巡飞弹火场:纯粒子火舌上卷+黑烟(无圆形区域) + if(G.time%3===0)for(let i=0;i<2;i++)G.parts.push({x:z.x+rnd(-1,1)*z.r*0.7,y:z.y+rnd(-1,1)*z.r*0.5,vx:rnd(-0.25,0.25),vy:rnd(-1.3,-0.5),life:rndi(12,22),col:pick(["#ff8a2e","#ffc64a","#ff4a1f"]),sz:rndi(1,2)}); + if(G.time%8===0)G.parts.push({x:z.x+rnd(-1,1)*z.r*0.5,y:z.y-z.r*0.3,vx:rnd(-0.15,0.15),vy:rnd(-0.7,-0.3),life:rndi(24,38),col:"#5a5a6a",sz:2}); + continue; + } ctx.globalAlpha=0.16;ctx.fillStyle=z.col; ctx.beginPath();ctx.arc(z.x,z.y,z.r,0,TAU);ctx.fill(); ctx.globalAlpha=0.5+Math.sin(G.time*0.1)*0.2;ctx.strokeStyle=z.col; diff --git a/star-raid/js/sprites.js b/star-raid/js/sprites.js index 0cb46da..b5ec8e9 100644 --- a/star-raid/js/sprites.js +++ b/star-raid/js/sprites.js @@ -148,6 +148,7 @@ SPR.icelance=bake(["...K...","..KCK..","..KCK..",".KCWK.",".KCCK.",".KCCK.","..K SPR.msl2=bake(["..K..",".KYK.","KYGYK","KYGYK",".KGK.","K.K.K"],{K:"#2a240d",Y:"#ffe95c",G:"#8ca86a"}); // 蜂巢小导弹 SPR.amsl=bake(["...K...","..KRK..",".KRRRK.","KRWRKRK",".KRRRK.",".KRRRK.","K.KRK.K"],{K:"#241a08",R:"#e0763e",W:"#fff4d9"}); // 空爆重型导弹 SPR.cmsl=bake(["...KK...","..KRRK..",".KRRRRK.","KRWRRWRK",".KRRRRK.",".KRRRRK.","..KRRK..",".K.KK.K."],{K:"#1a0d0d",R:"#e0393e",W:"#ffd23d"}); // 斩首巡航导弹 +SPR.loms=bake(["...W...","..KWK..",".KOOOK.","KKOOOKK","..KOK..","..KOK..","...K..."],{K:"#26160d",W:"#fff4d9",O:"#ff8a2e"}); // 巡飞弹:白尖橙体+翼横杆(与msl2/cmsl/amsl区分) SPR.broad=bake([".KKK.","KBBBK","KBWBK","KBBBK",".KKK."],{K:"#241a08",B:"#c8923e",W:"#fff4d9"}); // 侧舷炮弹 SPR.flare=bake(["..KK..",".KWWK.","KWWWWK","KWWWWK",".KWWK.","..KK.."],{K:"#33240a",W:"#ffd23d"}); // 热诱弹 SPR.sticky=bake([".KKKK.","KGKKGK","KGRRGK","KGRRGK","KGKKGK",".KKKK."],{K:"#142a0d",G:"#5cff9d",R:"#ff5566"}); // 粘着炸弹 diff --git a/star-raid/js/ui.js b/star-raid/js/ui.js index d56360e..703a3a9 100644 --- a/star-raid/js/ui.js +++ b/star-raid/js/ui.js @@ -2,12 +2,12 @@ /* ================= 界面流转 ================= */ const OVLS=["o-auth","o-home","o-title","o-setup","o-hangar","o-cards","o-pause","o-over","o-codex","o-demo","o-rush","o-slot"]; function show(id){for(const o of OVLS)$(o).classList.toggle("hide",o!==id);} -const SETUP={mode:"stage",ship:"falcon",diff:0}; +const SETUP={mode:"endless",ship:"falcon",diff:0}; // 出击默认无尽模式 function bestLine(){ const b=SAVE.best; $("best-line").textContent=`无尽纪录 ${Math.max(b.e0,b.e1)} · 闯关进度 ${b.stage}/6`; } -$("b-go").onclick=()=>{AU.init();buildSetup();show("o-setup");}; +$("b-go").onclick=()=>{AU.init();show("o-setup");}; $("b-back1").onclick=()=>show("o-title"); $("b-back2").onclick=()=>show("o-title"); $("b-hangar").onclick=()=>{AU.init();buildShop();show("o-hangar");}; @@ -16,6 +16,17 @@ $("b-rush").onclick=()=>{AU.init();buildRush();show("o-rush");}; $("b-back6").onclick=()=>{UI="title";show("o-title");}; $("b-back4").onclick=()=>{UI="title";bestLine();show("o-title");}; $("b-back5").onclick=exitDemoSim; +/* 图鉴测试模式(隐藏入口):连点图鉴标题“军”字10次开关。 + 纯内存开关——不动 SAVE.codex(persist/结算上报都读它,动了会污染后端),刷新页面即失效 */ +let codexTestAll=false,cxClicks=0,cxLastClick=0; +$("cx-secret").onclick=()=>{ + const now=Date.now(); + if(now-cxLastClick>2000)cxClicks=0; // 间隔超2秒重新计数 + cxLastClick=now; + if(++cxClicks<10)return; + cxClicks=0;codexTestAll=!codexTestAll; + SFX.pick();buildCodex(); +}; $("b-start").onclick=()=>startGame(); if("ontouchstart" in window){const pb=$("tb-pause");pb.style.display="block";pb.onclick=()=>togglePause();} $("b-resume").onclick=()=>togglePause(); @@ -40,7 +51,8 @@ $("diff-pick").addEventListener("click",e=>{ SETUP.diff=+b.dataset.d; for(const el of $("diff-pick").children)el.classList.toggle("primary",el===b); }); -function buildSetup(){ +/* 机体选择/购买(机库内):购买后整店刷新(星币/按钮态变化),选择只刷新机体卡 */ +function buildShips(){ const box=$("ship-pick");box.innerHTML=""; for(const k in SHIPS){ const s=SHIPS[k],owned=SAVE.ships.includes(k); @@ -53,11 +65,11 @@ function buildSetup(){ d.insertAdjacentHTML("beforeend",`
${s.name}
${s.trait}
`+(owned?"":`
✦ ${s.cost} 解锁
`)); d.onclick=()=>{ if(!SAVE.ships.includes(k)){ - if(SAVE.coins>=s.cost){SAVE.coins-=s.cost;SAVE.ships.push(k);persist();SFX.level();buildSetup();} + if(SAVE.coins>=s.cost){SAVE.coins-=s.cost;SAVE.ships.push(k);persist();SFX.level();buildShop();} else SFX.hurt(); return; } - SETUP.ship=k;buildSetup(); + SETUP.ship=k;persist();buildShips(); // persist 触发云端同步选中机体 }; box.appendChild(d); } @@ -74,6 +86,7 @@ const META=[ ]; function buildShop(){ $("wallet").textContent=SAVE.coins; + buildShips(); const box=$("shop");box.innerHTML=""; for(const m of META){ const lv=SAVE.m[m.key]||0,maxed=lv>=m.max,cost=maxed?0:m.costs[lv]; @@ -142,12 +155,14 @@ function buildCodex(){ const box=$("codex");box.innerHTML=""; const total=MAINS.length+SUB_KEYS.length+DRONE_KEYS.length+PAS_KEYS.length; $("codex-t").textContent=total; - $("codex-n").textContent=codexCount(); + $("codex-n").textContent=codexCount(); // 计数永远显示真实收集数,测试模式不影响 + $("codex-test").style.display=codexTestAll?"":"none"; + $("cx-secret").style.color=codexTestAll?"var(--gold)":""; // 开启后“军”字变金 const mk=(title,keys,dict,kind)=>{ const h=document.createElement("div");h.className="cx-sec";h.textContent=title;box.appendChild(h); const grid=document.createElement("div");grid.className="cx-grid"; for(const k of keys){ - const def=dict[k],owned=!!SAVE.codex[k]; + const def=dict[k],owned=!!SAVE.codex[k]||codexTestAll; // 测试模式:全部可见可演示 const d=document.createElement("div"); d.className="cx-item"+(owned?"":" lock"); // 未收集:剪影+???,不可演示 const img=def.icon?ICONS[def.icon]:SPR[def.spr]; @@ -338,7 +353,7 @@ function startDemoSim(kind,key){ if(key==="shield")G.shield=shieldCap();} $("demo-name").textContent=def.name; const ds=typeof def.ds==="function"?def.ds(lv):def.ds; - $("demo-ds").textContent=(kind==="m"?"主武器 · ":kind==="s"?"副武器 · ":kind==="d"?"僚机 · ":"被动 · ")+ds; + $("demo-ds").textContent=ds; // 只显示介绍本身,单行不折行(nowrap 在 index.html 上) UI="demoSim";show("o-demo");acc=0; banner(def.name,"#41e6ff",90); } diff --git a/star-raid/server/api.js b/star-raid/server/api.js index 1b91c24..952563e 100644 --- a/star-raid/server/api.js +++ b/star-raid/server/api.js @@ -9,7 +9,7 @@ const { vUsername, vEmail, vPassword, vInt, rateLimit, clientIp } = require("./u const CODEX_KEYS = new Set([ "vulcan", "shotgun", "needle", "flame", "shotgun2", "tesla", "railgun", "deathray", "crescent", "fission", "frost", "swarm", "bubble", "starfall", "boomerang", "singularity", - "wavecannon", "holy", "thundercall", "arcshu", "volt", "dawn", "proto", "nebula", + "wavecannon", "holy", "thundercall", "arcshu", "volt", "dawn", "proto", "nebula", "loiter", "broad", "sweep", "leech", "orbit", "sats", "pd", "slowfield", "flares", "seeker", "beamdrone", "frostnova", "thorns", "sticky", "magnetar", "executor", "airstrike", "repair", "gunner", "gunner2", "sniperd", "beamer", "bomber", "frostd", "teslaD", "rammer", @@ -18,6 +18,10 @@ const CODEX_KEYS = new Set([ ]); const CODEX_TOTAL = CODEX_KEYS.size; const RUN_MODES = new Set(["stage", "endless", "rush"]); +/* 存档白名单(与 js/data.js SHIPS、js/ui.js META 保持同步) */ +const SHIP_KEYS = new Set(["falcon", "ghost", "bastion", "valkyrie"]); +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 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(?)"); @@ -33,6 +37,9 @@ const qUpdStats = db.prepare(`INSERT INTO user_stats(user_id,boss_kills,endless_ endless_high=MAX(endless_high,excluded.endless_high), runs=runs+1, updated_at=excluded.updated_at`); +const qGetState = db.prepare("SELECT state FROM player_state WHERE user_id=?"); +const qPutState = db.prepare(`INSERT INTO player_state(user_id,state,updated_at) VALUES(?,?,?) + ON CONFLICT(user_id) DO UPDATE SET state=excluded.state, updated_at=excluded.updated_at`); function statsOf(userId) { const s = qStats.get(userId) || { boss_kills: 0, endless_high: 0, runs: 0 }; @@ -41,6 +48,31 @@ function statsOf(userId) { function codexOf(userId) { return qCodex.all(userId).map((r) => r.entry_key); } +function stateOf(userId) { + const r = qGetState.get(userId); + if (!r) return null; // null=从未同步(老账号首次登陆:客户端用本地存档初始化上传) + try { return JSON.parse(r.state); } catch (e) { return null; } +} + +/* 存档白名单校验:只留合法字段并 clamp,任何字段非法则整体拒收 */ +function vState(s) { + if (!s || typeof s !== "object" || Array.isArray(s)) return null; + if (!vInt(s.coins, 0, 1e7)) return null; + if (!Array.isArray(s.ships)) return null; + const ships = [...new Set(s.ships.filter((k) => SHIP_KEYS.has(k)))].slice(0, 8); + if (!ships.includes("falcon")) ships.unshift("falcon"); + const ship = SHIP_KEYS.has(s.ship) ? s.ship : "falcon"; + const m = {}; + if (s.m && typeof s.m === "object") for (const k of Object.keys(META_MAX)) + if (vInt(s.m[k], 0, META_MAX[k])) m[k] = s.m[k]; + const best = { s0: 0, s1: 0, e0: 0, e1: 0, stage: 0 }; + if (s.best && typeof s.best === "object") for (const k of Object.keys(best)) + if (vInt(s.best[k], 0, 1e8)) best[k] = s.best[k]; + 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 }; +} /* ---------- POST /api/register ---------- */ function register(req, res, ctx, body) { @@ -56,7 +88,7 @@ function register(req, res, ctx, body) { qInsStats.run(uid); const token = auth.createSession(db, uid); ctx.setCookie = auth.sessionCookie(token, ctx.secure); - return ctx.ok({ user: { username, email: email.trim().toLowerCase() }, stats: statsOf(uid), codex: [], codexTotal: CODEX_TOTAL }); + return ctx.ok({ user: { username, email: email.trim().toLowerCase() }, stats: statsOf(uid), codex: [], codexTotal: CODEX_TOTAL, state: DEFAULT_STATE }); } /* ---------- POST /api/login ---------- */ @@ -72,7 +104,7 @@ function login(req, res, ctx, body) { qInsStats.run(u.id); const token = auth.createSession(db, u.id); ctx.setCookie = auth.sessionCookie(token, ctx.secure); - return ctx.ok({ user: { username: u.username, email: u.email }, stats: statsOf(u.id), codex: codexOf(u.id), codexTotal: CODEX_TOTAL }); + return ctx.ok({ user: { username: u.username, email: u.email }, stats: statsOf(u.id), codex: codexOf(u.id), codexTotal: CODEX_TOTAL, state: stateOf(u.id) }); } /* ---------- POST /api/logout ---------- */ @@ -86,7 +118,7 @@ function logout(req, res, ctx) { function me(req, res, ctx) { const u = ctx.user; if (!u) return ctx.ok({ user: null }); - return ctx.ok({ user: { username: u.username, email: u.email, createdAt: u.created_at }, stats: statsOf(u.id), codex: codexOf(u.id), codexTotal: CODEX_TOTAL }); + return ctx.ok({ user: { username: u.username, email: u.email, createdAt: u.created_at }, stats: statsOf(u.id), codex: codexOf(u.id), codexTotal: CODEX_TOTAL, state: stateOf(u.id) }); } /* ---------- GET /api/leaderboard?sort=endless|boss|codex ---------- */ @@ -131,6 +163,16 @@ function run(req, res, ctx, body) { return ctx.ok({ stats: statsOf(uid), codex: codexOf(uid), codexTotal: CODEX_TOTAL }); } +/* ---------- POST /api/state ---------- + 存档云同步:星币/机体/选中机体/永久强化/本地纪录(防抖由客户端负责) */ +function saveState(req, res, ctx, body) { + if (!rateLimit("state:" + ctx.user.id, 60, 5 * 60000)) return ctx.fail(429, "同步太频繁"); + const st = vState(body && body.state); + if (!st) return ctx.fail(400, "非法存档数据"); + qPutState.run(ctx.user.id, JSON.stringify(st), Date.now()); + return ctx.ok({ saved: true }); +} + const ROUTES = { "POST /api/register": { fn: register, auth: false }, "POST /api/login": { fn: login, auth: false }, @@ -138,6 +180,7 @@ const ROUTES = { "GET /api/me": { fn: me, auth: false }, "GET /api/leaderboard": { fn: leaderboard, auth: true }, "POST /api/run": { fn: run, auth: true }, + "POST /api/state": { fn: saveState, auth: true }, }; module.exports = { ROUTES, CODEX_TOTAL }; diff --git a/star-raid/server/db.js b/star-raid/server/db.js index 89e24a0..2b6c41a 100644 --- a/star-raid/server/db.js +++ b/star-raid/server/db.js @@ -55,6 +55,11 @@ CREATE TABLE IF NOT EXISTS runs( created_at INTEGER NOT NULL ); CREATE INDEX IF NOT EXISTS idx_runs_user ON runs(user_id); +CREATE TABLE IF NOT EXISTS player_state( + user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, + state TEXT NOT NULL, + updated_at INTEGER NOT NULL +); `); /* 定期清理过期 session */