TL;DR
Two bugs of the same shape: a lookup that quietly answers when it should admit it doesn't know. Neither throws, neither logs, and both have been shipping.
Both came out of a new build-time tool (tools/tag-items.js) that asks a model the same questions these regexes are guessing at and diffs the answers against shipped behaviour. (A) it found with no model at all — it's just an array length.
(A) Five melees never got a swing type 🗡️
Where (verified):
public/game.js — const MELEE_SWING_TYPES = [...] ends at index 36 (volt_whip)
public/game.js — const MELEE_ITEMS = [...] runs to index 41
- Read site:
meleeSwingType = MELEE_SWING_TYPES[selectedMeleeIdx] || 'slash';
The catch: the || 'slash' was there to be safe, and instead it made the drift invisible. These five take a baseball swing:
| idx |
item |
type |
| 37 |
Karambit |
Admin · Combat Knife |
| 38 |
Trench Bayonet |
Admin · Reach Knife |
| 39 |
Tactical Tomahawk |
Admin · Throwable Axe |
| 40 |
OTs-04 Bayonet |
Admin · Spetsnaz Blade |
| 41 |
Spec-Ops Garrote |
Admin · Silent Kill |
This is exactly the hazard #4 added a length guard for on weaponModels[]. The melee side never got one, and it drifted. MELEE_SWING_TYPES is the third parallel array in that family (MELEE_ITEMS ↔ meleeModels ↔ MELEE_SWING_TYPES) — the other two are still aligned at 42.
Proposed values, from each item's own reach and cooldown next to the ones already in the table:
'stab', // 37 karambit → short curved blade; knife (idx 7) is stab at 1.6 m / 260 ms
'thrust', // 38 bayonet → 3 m reach, same as spear (idx 4), which is thrust
'chop', // 39 tomahawk → axe head; meat_cleaver (idx 33) is chop
'stab', // 40 ots04 → "Bayonet" by name, but 1.8 m and a 240 ms cooldown: a jab
'thrust', // 41 garrote → ⚠️ judgement call
⚠️ The garrote needs a human. None of the eight swing types is a strangle — thrust is the least wrong, not the right answer. Worth a playtest, and worth considering whether a garrote swing type should exist at all.
Also worth doing: extend whatever guard #4 added so this family is checked too, and drop the || 'slash' so the next gap is loud instead of quiet.
(B) "Hand Cannon" is not a cannon 💥
Where (verified): public/game.js, in the Kill Log writer:
const typeStr = ((wpn?.type || sup?.type || '') + '').toLowerCase();
const idStr = (weaponId || '').toLowerCase();
if (/explos|launcher|mortar|firework|grenade|rocket|bomb|nuke|missile|boombow|cannon|artillery/
.test(typeStr + ' ' + idStr)) kind = 'explosive';
The catch: cannon and launcher are substrings of a lot of names that describe the shape of the tube, not how you died. Eight items are currently mis-filed:
| item |
type |
why it matched |
desert_eagle |
Admin · Hand Cannon |
"Hand Cannon" |
hand_cannon |
Secondary+ |
id contains "cannon" |
burst_cannon |
Heavy Burst Rifle |
id contains "cannon" |
foam_cannon |
Industrial Utility |
id contains "cannon" |
prism_launcher |
Bouncing Light |
id contains "launcher" |
portal_launcher |
Spatial |
id contains "launcher" |
confetti_cannon |
Chaos (support) |
id contains "cannon" |
…and it misses real ones: airburst_projector (authored as grenade in PROJECTILE_KIND_BY_ID), plus land_mine, proximity_mine, magnet_mine, stasis_mine and claymore.
The fix is to ask the data, not the name. The game already knows: PROJECTILE_KIND_BY_ID records what each weapon fires, and splashRadius says outright that it detonates.
const EXPLOSIVE_KILL_WORDS = new Set([
'explosive', 'explosives', 'grenade', 'grenades', 'bomb', 'bombs', 'nuke',
'dynamite', 'mine', 'rocket', 'missile', 'frag', 'c4', 'mortar', 'artillery',
'firework', 'fireworks', 'claymore',
]);
function namedAsExplosive(id, type) {
return (String(id || '') + ' ' + String(type || '')).toLowerCase()
.split(/[^a-z0-9]+/) // whole words, so confetti_cannon has no cannon in it
.some(w => EXPLOSIVE_KILL_WORDS.has(w));
}
function isExplosiveKill(weaponId, wpn, sup) {
if (wpn) {
const k = projectileKind(weaponId, wpn);
if (k === 'grenade' || k === 'rocket' || wpn.splashRadius) return true;
return namedAsExplosive(weaponId, wpn.type); // Boombow: a bolt that detonates
}
if (sup) return namedAsExplosive(weaponId, sup.type);
return false;
}
No cannon, no launcher in the word list, on purpose.
Verified over the whole catalogue (99 weapons + 55 support): 7 weapons and 1 support item stop being explosive; airburst_projector, land_mine, proximity_mine, magnet_mine, stasis_mine and claymore start; and rpg, bazooka, grenade_launcher, storm_cannon, boombow, potato_cannon, frag, c4, tac_nuke, dynamite, sticky_charge, mortar_rifle and firework_launcher are unchanged.
Two left deliberately alone — pinball_launcher and potato_cannon. Both are authored as 'grenade' in PROJECTILE_KIND_BY_ID, so the table says lobbed explosive while the name says joke. That's a content call, not a bug, and it wants your eye rather than a patch.
Not in this issue, but found alongside 🔍
The same tool turned up things that are balance or taste, not bugs — filing them here only so they aren't lost:
ELECTRIC_WEAPONS contains the plain pistol and revolver. That set isn't cosmetic: getSecretSynergy() uses it to grant ×1.5 damage on ice zones and ×1.25 on sewer/holiday. freeze_gun is in both ELECTRIC_WEAPONS and FROST_WEAPONS; the actual volt_whip ("Long Electric") is in neither. sg8, a plain shotgun, is in FIRE_WEAPONS (×1.30 on forest).
- 8 melee swing sounds are off for the same reason as (B) — the blade/heavy test reads only
item.id, so ots04 ("Spetsnaz Blade") swings with the blunt sound.
_pickFinisher() falls through to FINISHERS[random] for most weapons, including storm_cannon ("Lightning Explosive"), which never gets the lightning finisher because /Explosive/ is tested before /Lightning/ in projectileKind().
Happy to split any of these into their own issue.
中文摘要
两个同一类的 bug:查表查不到时不吭声,直接给个默认值。
(A) MELEE_SWING_TYPES 只有 37 条,MELEE_ITEMS 有 42 条。最后 5 把 Admin 近战掉出数组,被 || 'slash' 兜成棒球挥棒——特工绞索在打棒球。这跟 #4 给 weaponModels[] 加长度守卫防的是同一个毛病,近战这边没加,就漂移了。绞索那条我给的是 thrust,但八种挥击类型里没有一种是"勒",需要你试手感,也可以考虑干脆加一个 garrote 类型。
(B) 击杀日志用一次子串匹配判"爆炸 vs 枪击",名字里带 cannon / launcher 就算爆炸——沙漠之鹰的击杀图标是爆炸,彩带炮"爆炸地"喷彩带;而真正的空爆发射器一个都没匹配上,地雷也全漏。修法是问数据不问名字:PROJECTILE_KIND_BY_ID 本来就记了每把枪发射什么,splashRadius 直接说明它炸不炸。补给道具没有这张表,就按整词匹配而不是子串。
pinball_launcher 和 potato_cannon 我故意没动:手写表里它俩就是 'grenade',这是内容取向问题不是 bug,留给你定。
TL;DR
Two bugs of the same shape: a lookup that quietly answers when it should admit it doesn't know. Neither throws, neither logs, and both have been shipping.
MELEE_SWING_TYPEShas 37 entries for 42MELEE_ITEMS. The last five Admin melees fall off the end and silently take the|| 'slash'default. The Spec-Ops Garrote is doing a baseball swing.cannonorlauncheris an explosive — the Desert Eagle brags with a blast graphic, and the Confetti Cannon fires confetti explosively. Meanwhile the Airburst Projector, which genuinely is an airburst, matches nothing.Both came out of a new build-time tool (
tools/tag-items.js) that asks a model the same questions these regexes are guessing at and diffs the answers against shipped behaviour. (A) it found with no model at all — it's just an array length.(A) Five melees never got a swing type 🗡️
Where (verified):
public/game.js—const MELEE_SWING_TYPES = [...]ends at index 36 (volt_whip)public/game.js—const MELEE_ITEMS = [...]runs to index 41meleeSwingType = MELEE_SWING_TYPES[selectedMeleeIdx] || 'slash';The catch: the
|| 'slash'was there to be safe, and instead it made the drift invisible. These five take a baseball swing:This is exactly the hazard #4 added a length guard for on
weaponModels[]. The melee side never got one, and it drifted.MELEE_SWING_TYPESis the third parallel array in that family (MELEE_ITEMS↔meleeModels↔MELEE_SWING_TYPES) — the other two are still aligned at 42.Proposed values, from each item's own reach and cooldown next to the ones already in the table:
thrustis the least wrong, not the right answer. Worth a playtest, and worth considering whether agarroteswing type should exist at all.Also worth doing: extend whatever guard #4 added so this family is checked too, and drop the
|| 'slash'so the next gap is loud instead of quiet.(B) "Hand Cannon" is not a cannon 💥
Where (verified):
public/game.js, in the Kill Log writer:The catch:
cannonandlauncherare substrings of a lot of names that describe the shape of the tube, not how you died. Eight items are currently mis-filed:desert_eaglehand_cannonburst_cannonfoam_cannonprism_launcherportal_launcherconfetti_cannon…and it misses real ones:
airburst_projector(authored asgrenadeinPROJECTILE_KIND_BY_ID), plusland_mine,proximity_mine,magnet_mine,stasis_mineandclaymore.The fix is to ask the data, not the name. The game already knows:
PROJECTILE_KIND_BY_IDrecords what each weapon fires, andsplashRadiussays outright that it detonates.No
cannon, nolauncherin the word list, on purpose.Verified over the whole catalogue (99 weapons + 55 support): 7 weapons and 1 support item stop being explosive;
airburst_projector,land_mine,proximity_mine,magnet_mine,stasis_mineandclaymorestart; andrpg,bazooka,grenade_launcher,storm_cannon,boombow,potato_cannon,frag,c4,tac_nuke,dynamite,sticky_charge,mortar_rifleandfirework_launcherare unchanged.Two left deliberately alone —
pinball_launcherandpotato_cannon. Both are authored as'grenade'inPROJECTILE_KIND_BY_ID, so the table says lobbed explosive while the name says joke. That's a content call, not a bug, and it wants your eye rather than a patch.Not in this issue, but found alongside 🔍
The same tool turned up things that are balance or taste, not bugs — filing them here only so they aren't lost:
ELECTRIC_WEAPONScontains the plainpistolandrevolver. That set isn't cosmetic:getSecretSynergy()uses it to grant ×1.5 damage on ice zones and ×1.25 on sewer/holiday.freeze_gunis in bothELECTRIC_WEAPONSandFROST_WEAPONS; the actualvolt_whip("Long Electric") is in neither.sg8, a plain shotgun, is inFIRE_WEAPONS(×1.30 on forest).item.id, soots04("Spetsnaz Blade") swings with the blunt sound._pickFinisher()falls through toFINISHERS[random]for most weapons, includingstorm_cannon("Lightning Explosive"), which never gets the lightning finisher because/Explosive/is tested before/Lightning/inprojectileKind().Happy to split any of these into their own issue.
中文摘要
两个同一类的 bug:查表查不到时不吭声,直接给个默认值。
(A)
MELEE_SWING_TYPES只有 37 条,MELEE_ITEMS有 42 条。最后 5 把 Admin 近战掉出数组,被|| 'slash'兜成棒球挥棒——特工绞索在打棒球。这跟 #4 给weaponModels[]加长度守卫防的是同一个毛病,近战这边没加,就漂移了。绞索那条我给的是thrust,但八种挥击类型里没有一种是"勒",需要你试手感,也可以考虑干脆加一个garrote类型。(B) 击杀日志用一次子串匹配判"爆炸 vs 枪击",名字里带
cannon/launcher就算爆炸——沙漠之鹰的击杀图标是爆炸,彩带炮"爆炸地"喷彩带;而真正的空爆发射器一个都没匹配上,地雷也全漏。修法是问数据不问名字:PROJECTILE_KIND_BY_ID本来就记了每把枪发射什么,splashRadius直接说明它炸不炸。补给道具没有这张表,就按整词匹配而不是子串。pinball_launcher和potato_cannon我故意没动:手写表里它俩就是'grenade',这是内容取向问题不是 bug,留给你定。