我本来只想做一个21点。
发牌,要牌,停牌,比大小。没了。
后来不知道哪根筋搭错了,开始往上加东西:先加了个血条,然后加了战斗卡牌,然后加了芯片系统,然后加了牌库构筑,然后加了三个BOSS和一个隐藏BOSS。现在这个游戏有33张牌的牌库、五种类型的战斗卡牌、10个芯片槽、抽卡池、以及一堆我自己都经常忘掉的隐藏机制。
地址在这:tiaotiaohu0516.cn:5001。
你说这篇文章算什么?算是使用指南吧——教你怎么玩,顺带告诉你哪些地方可能出BUG,因为那些BUG都是我亲手写出来的。
最本质的区别:你有血条,AI也有血条。默认都是10HP。
每一回合,你跟AI各玩一轮21点。输的人扣血。扣多少取决于几件事:你的芯片有没有+基础伤害、有没有触发黑杰克(天然21点伤害翻倍)、以及对面有没有护盾扛着。
打完一局的标准流程:进战斗→每回合抽3张战斗卡牌→你出牌/要牌/停牌→AI操作→结算→扣血。谁先归零谁输。
回血手段少得可怜。只有个别战斗卡牌能回血,或者特定芯片被动能吸一点。所以你打的每一局都像在走钢丝——这也是为什么标签里有个「肉鸽」。
这是整个游戏复杂度爆炸的根源。
战斗卡牌有五种:
伤害牌(6张)。直接扣对面血,或者让下回合伤害翻倍。需要消耗能量。
防御牌(5张)。给你加护盾,或者让对面下一击伤害减半。
技能牌(17张)。在21点桌面上搞事情——锁对面一张牌不让要、强制对面停牌、弃掉自己所有手牌重抽、偷看对面底牌等等。
能量牌(5张)。使用不消耗能量,反而加能量。能量是你每回合打战斗牌的「费用」,初始3点,每回合回满。
传奇牌。只能从抽卡池出,效果离谱。比如「数据湮灭」直接炸对面手牌。
每回合能打多少张牌取决于你的能量池。伤害牌一般费2-3能,防御牌费1-2能,技能牌费1-4能。所以牌序很重要——你可能需要先打能量牌攒费,再甩伤害牌。也可能对面已经19点了你得赶紧打「强制停牌」锁住他。
「校验和」这张牌——描述是「弃掉所有手牌并重抽」。初版有个问题:如果你已经爆牌了(超过21点),用了这张牌确实把手牌清空了,但playerBust这个状态变量忘了重置。结果就是牌桌干干净净,游戏却觉得你已经输了,没有按钮给你按。画面就卡在那里,你只能刷新重来。
修法就是把那个布尔值改回false。一行代码的事。但我发现这个BUG花了两周——因为我自己玩的时候从来不会在爆牌之后才用这张牌。
另一个是护盾+99的烂活。说出来你可能不信——我当时为了标记「伤害吸收」和「伤害反射」这两张牌是否生效,用了这个操作:给护盾临时+99。对。用护盾数值当标记变量。写的时候心想「护盾反正有上限,99就是个标志位」。但我忘了护盾上限是后来才加的补丁。第一版里护盾可以无限堆,这+99就变成了真·护盾——你打一张吸收牌,对面打你,你掉0血,因为你有99护盾。
后来把这段删了,用独立的_absorbUser和_reflectUser变量来追踪状态。这属于典型的「半夜写代码写的什么玩意儿」系列。
每打完一局你会拿到一些「废料」,拿去抽卡池。卡池里能抽出两种东西:芯片和传奇战斗卡牌。
芯片有10个槽位可以装备,提供各种被动效果。举例:
有意思的是组合。穿刺+暴击=无视护盾的双倍伤害。穿刺+吸血+基础伤害=打多少吸多少。抽牌+1+能量上限=每回合能打更多牌。这里面的策略空间比看起来大。
自动护盾这个被动,最初没有上限。
想象一下:你跟AI磨了50个回合,每回合自动+2护盾。你的护盾变成了114点。AI打你,跳出来一个-6的数字,然后护盾从114变成108。你全程没掉过血。
这个BUG的修法本身不值一提——加个Math.min(shield, 15)。但值得说的是它为什么藏了那么久。因为我测试的时候从来没活过15回合。AI前期伤害太高了,我根本拖不到护盾堆起来。是后来一个玩得比我好的朋友跟我说「你这游戏可以无限拖回合啊,我护盾三百多了」,我才知道有这个问题。
教训就是:你永远不知道玩家怎么玩你的游戏。尤其是当玩家比你玩得好。
在主菜单点「装备芯片」,左边有个牌库编辑器。
默认牌库33张,你可以加减卡片来调整策略。限制是:每种卡最多带2张,传奇只能靠抽,总数必须≥33才能进战斗。
你可以组:
编辑器本身挺简陋的,加减卡片、看统计。够用就行,反正我也懒得做UI了。
早期有个问题:打完一局回主菜单,手上和弃牌堆的卡不会回收。你打5局之后33张牌库变成7张。打开编辑器以为游戏出BUG了——「我的牌呢?」
其实不是丢了,是没回收。在backToTitle()里加了个recoverDeck()调用把所有牌洗回牌库就好了。这个BUG藏得深的原因同上——我开发的时候每局开新档测试,从来没连续打过多局。
游戏有三个阶段的AI:
哨兵——基础AI。普通21点策略,偶尔丢张伤害牌。新手村。
执行者——要牌更激进,伤害牌更多,有时会锁你牌。
主脑——打败前三个对手之后解锁。它会精准计算——你接近21点的时候它直接停牌等你爆,你牌小的时候它猛攻。打它没有必胜策略,纯看牌运和芯片积累。
主脑的解锁有一个早期BUG:检查逻辑只在游戏初始化时跑一次。如果你一局内连续打赢三个BOSS(而不是退出重进),主脑永远不会解锁。因为tryUnlockMainframe()的调用时机不对。现在改成了每次击败对手后实时检查。
这个游戏最开始是给电脑做的。大屏、鼠标悬停、小字密集——全是桌面端思维。
放到手机上第一版:你往下划一下,浏览器给你刷新了。游戏没了。从头开始。
后来花了很长时间搞手机适配,总结下来几个核心问题:
下拉刷新——移动浏览器的默认行为。解决办法:html, body加overflow:hidden和position:fixed,然后用#app容器独立滚动。光靠CSS的overscroll-behavior: none不够,iOS Safari不吃这一套。
长按弹菜单——浏览器默认的长按会弹「复制/选择」菜单。解决:禁止contextmenu事件、禁止selectstart事件、CSS加-webkit-touch-callout: none。三层防。
标题按钮看不见——这个最搞笑。我设的颜色是rgba(0,255,204,0.06)——不透明度6%。在黑色背景上约等于没有。我写的时候还觉得「赛博朋克嘛,若隐若现多酷」。发给朋友测试,对方:??你游戏在哪。
终端滚动——标题画面有个打字机终端,手机上滑不动。原因是touchstart事件里写了preventDefault()阻止了浏览器的滚动行为。删掉就好了。
我现在给自己定的规矩:所有手机端改动必须锁在@media (max-width: 700px)里面。绝对不动桌面端全局的CSS和JS。否则改手机端一定会搞坏桌面端,没有例外。
这个游戏做到V16的时候还是纯数字跳。V17终于加了一整套视觉反馈:
伤害数字从目标身上飘起来(暴击的更大更亮)、护盾格挡有蓝色波纹、平局有金色全屏闪、攻击方会闪一下并抖动。
实现方式很简单:dealDamage函数不直接创建DOM,而是往数组里push特效规格。等resolveRound全部算完之后,延迟80ms一次性渲染。这样不会在逻辑计算中途插DOM操作。
唯一的小问题:如果护盾完全吸收了伤害(实际伤害=0),VFX会生成一个「-0」的伤害数字。看着像个表情包。修法是加了if (dmg > 0)。
用浏览器的localStorage,键名neon21_v2_save。每回合结算后自动存。
存的东西:牌库组成、弃牌堆、已装备芯片、抽卡保底计数、全局属性。
桌面端左下角有个💾按钮手动存。手机端底部栏也有。读档的时候如果牌库不足33张会自动补默认牌。
最后说两句实话。
我做这个游戏没有任何宏大的目标。就是想看看纯前端(HTML+CSS+JS,零框架)能做一个多复杂的游戏。结果越加越多,变成现在这样。
修BUG的过程比我写功能的时间还长。每当我以为「这次肯定没问题了」,就会有人发现一个新的玩法路径把我代码里的漏洞暴露出来。拖50回合的、打完不回收牌库的、手机上各种诡异手势的——我永远在修我没想到的BUG。
如果你上去玩遇到什么问题,那大概率又是我没想到的坑。
地址:tiaotiaohu0516.cn:5001。所有数据存你浏览器里,换设备就没了。
玩得开心——或者说,玩得比我开发的时候开心就行。
本文作者:haotian
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!