| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
|
| 10 |
|
| 11 |
export type WeaponId = |
| 12 |
| "classic" |
| 13 |
| "ghost" |
| 14 |
| "sheriff" |
| 15 |
| "spectre" |
| 16 |
| "phantom" |
| 17 |
| "vandal" |
| 18 |
| "operator"; |
| 19 |
|
| 20 |
export type Zone = "head" | "body" | "legs"; |
| 21 |
|
| 22 |
export type Band = { upTo: number; head: number; body: number; legs: number }; |
| 23 |
|
| 24 |
export type Weapon = { |
| 25 |
id: WeaponId; |
| 26 |
name: string; |
| 27 |
cost: number; |
| 28 |
|
| 29 |
rate: number; |
| 30 |
magazine: number; |
| 31 |
reload: number; |
| 32 |
|
| 33 |
spread: number; |
| 34 |
movePenalty: number; |
| 35 |
range: number; |
| 36 |
auto: boolean; |
| 37 |
bands: Band[]; |
| 38 |
}; |
| 39 |
|
| 40 |
const METRE = 50; |
| 41 |
|
| 42 |
export const WEAPONS: Record<WeaponId, Weapon> = { |
| 43 |
classic: { |
| 44 |
id: "classic", |
| 45 |
name: "Classic", |
| 46 |
cost: 0, |
| 47 |
rate: 6.75, |
| 48 |
magazine: 12, |
| 49 |
reload: 1.75, |
| 50 |
spread: 0.022, |
| 51 |
movePenalty: 0.05, |
| 52 |
range: 1600, |
| 53 |
auto: false, |
| 54 |
bands: [{ upTo: Infinity, head: 78, body: 26, legs: 22 }], |
| 55 |
}, |
| 56 |
ghost: { |
| 57 |
id: "ghost", |
| 58 |
name: "Ghost", |
| 59 |
cost: 500, |
| 60 |
rate: 6.75, |
| 61 |
magazine: 15, |
| 62 |
reload: 1.5, |
| 63 |
spread: 0.016, |
| 64 |
movePenalty: 0.05, |
| 65 |
range: 1800, |
| 66 |
auto: false, |
| 67 |
bands: [ |
| 68 |
{ upTo: 30 * METRE, head: 105, body: 30, legs: 26 }, |
| 69 |
{ upTo: Infinity, head: 88, body: 25, legs: 21 }, |
| 70 |
], |
| 71 |
}, |
| 72 |
sheriff: { |
| 73 |
id: "sheriff", |
| 74 |
name: "Sheriff", |
| 75 |
cost: 800, |
| 76 |
rate: 4, |
| 77 |
magazine: 6, |
| 78 |
reload: 2.25, |
| 79 |
spread: 0.02, |
| 80 |
movePenalty: 0.09, |
| 81 |
range: 2000, |
| 82 |
auto: false, |
| 83 |
bands: [ |
| 84 |
{ upTo: 30 * METRE, head: 159, body: 55, legs: 46 }, |
| 85 |
{ upTo: Infinity, head: 145, body: 50, legs: 42 }, |
| 86 |
], |
| 87 |
}, |
| 88 |
spectre: { |
| 89 |
id: "spectre", |
| 90 |
name: "Spectre", |
| 91 |
cost: 1600, |
| 92 |
rate: 13.33, |
| 93 |
magazine: 30, |
| 94 |
reload: 2.5, |
| 95 |
spread: 0.03, |
| 96 |
movePenalty: 0.03, |
| 97 |
range: 1600, |
| 98 |
auto: true, |
| 99 |
bands: [ |
| 100 |
{ upTo: 20 * METRE, head: 78, body: 26, legs: 22 }, |
| 101 |
{ upTo: Infinity, head: 66, body: 22, legs: 18 }, |
| 102 |
], |
| 103 |
}, |
| 104 |
phantom: { |
| 105 |
id: "phantom", |
| 106 |
name: "Phantom", |
| 107 |
cost: 2900, |
| 108 |
rate: 11, |
| 109 |
magazine: 30, |
| 110 |
reload: 2.5, |
| 111 |
spread: 0.021, |
| 112 |
movePenalty: 0.06, |
| 113 |
range: 2400, |
| 114 |
auto: true, |
| 115 |
bands: [ |
| 116 |
{ upTo: 15 * METRE, head: 156, body: 39, legs: 33 }, |
| 117 |
{ upTo: 30 * METRE, head: 140, body: 35, legs: 29 }, |
| 118 |
{ upTo: Infinity, head: 124, body: 31, legs: 26 }, |
| 119 |
], |
| 120 |
}, |
| 121 |
vandal: { |
| 122 |
id: "vandal", |
| 123 |
name: "Vandal", |
| 124 |
cost: 2900, |
| 125 |
rate: 9.75, |
| 126 |
magazine: 25, |
| 127 |
reload: 2.5, |
| 128 |
spread: 0.024, |
| 129 |
movePenalty: 0.07, |
| 130 |
range: 2400, |
| 131 |
auto: true, |
| 132 |
bands: [{ upTo: Infinity, head: 160, body: 40, legs: 34 }], |
| 133 |
}, |
| 134 |
operator: { |
| 135 |
id: "operator", |
| 136 |
name: "Operator", |
| 137 |
cost: 4700, |
| 138 |
rate: 0.6, |
| 139 |
magazine: 5, |
| 140 |
reload: 3.7, |
| 141 |
spread: 0.004, |
| 142 |
movePenalty: 0.2, |
| 143 |
range: 2800, |
| 144 |
auto: false, |
| 145 |
bands: [{ upTo: Infinity, head: 255, body: 150, legs: 120 }], |
| 146 |
}, |
| 147 |
}; |
| 148 |
|
| 149 |
export const weaponIds = (): WeaponId[] => Object.keys(WEAPONS) as WeaponId[]; |
| 150 |
|
| 151 |
export const isWeaponId = (value: unknown): value is WeaponId => |
| 152 |
typeof value === "string" && value in WEAPONS; |
| 153 |
|
| 154 |
|
| 155 |
export function damage(id: WeaponId, zone: Zone, distance: number): number { |
| 156 |
const weapon = WEAPONS[id]; |
| 157 |
const band = weapon.bands.find((b) => distance <= b.upTo) ?? weapon.bands[weapon.bands.length - 1]!; |
| 158 |
return band[zone]; |
| 159 |
} |
| 160 |
|
| 161 |
|
| 162 |
|
| 163 |
|
| 164 |
|
| 165 |
|
| 166 |
export function applyDamage( |
| 167 |
hp: number, |
| 168 |
armour: number, |
| 169 |
amount: number, |
| 170 |
): { hp: number; armour: number } { |
| 171 |
const toArmour = Math.min(armour, Math.ceil(amount * (2 / 3))); |
| 172 |
return { hp: Math.max(0, hp - (amount - toArmour)), armour: armour - toArmour }; |
| 173 |
} |
| 174 |
|
| 175 |
|
| 176 |
export const isLethal = (hp: number, armour: number, amount: number): boolean => |
| 177 |
applyDamage(hp, armour, amount).hp <= 0; |
| 178 |
|
| 179 |
|
| 180 |
|
| 181 |
|
| 182 |
|
| 183 |
|
| 184 |
export function zoneFromMiss(miss: number, radius: number): Zone { |
| 185 |
if (miss <= radius * 0.33) return "head"; |
| 186 |
if (miss <= radius * 0.75) return "body"; |
| 187 |
return "legs"; |
| 188 |
} |
| 189 |
|
| 190 |
|
| 191 |
export const shotSpread = (id: WeaponId, speedFraction: number): number => |
| 192 |
WEAPONS[id].spread + WEAPONS[id].movePenalty * Math.min(1, Math.max(0, speedFraction)); |
| 193 |
|
| 194 |
export const SHIELD = { light: { cost: 400, armour: 25 }, heavy: { cost: 1000, armour: 50 } }; |