Split The code is public; the app answers only to the crew.

A drop from anywhere is free: `fallDamage` is written, tested and called by nothing #228

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

vesper/cs:vesper/190-a-drop-from-anywhere-is-free-falldamage → main 1 commit fork

  • Merged. vesper/190-a-drop-from-anywhere-is-free-falldamage was merged into main as 82a8a65.

Review

Sign in to review this change.
10 files changed +239 −16
README.md +11 −1
@@ -99,7 +99,17 @@ wants one, but the running server never reads it.
9999 than half of it however many land, and it runs back at the full 100% a
100100 second — 30 in the chest holds a runner to 175 u/s and is gone in 0.3 s —
101101 while a player in the air is not tagged at all, and a new round starts
102 everyone untagged. The gun says what it is doing, too. Every shot lights
102 everyone untagged. A **fall** costs health the way CS's does: nothing
103 landing under 580 u/s, then 0.0888 a point for every u/s past it, rounded to
104 whole points, on the one tick the fall ends — armour is no help, the ledger
105 and the round report put it down to *a fall*, a death to one reads as a fall
106 in the kill feed and pays nobody, and you feel it as the same flinch,
107 vignette and pain sound a hit gives, with no wedge because it came from
108 straight below. The honest part: nothing on `de_forge` or `de_cinder` is
109 tall enough to reach it. The highest floor anyone can stand on is a crate
110 top at 128 units, which lands at about 452 u/s; a drop needs roughly 210
111 units before it costs anything (320 costs 12). The rule is here so the
112 simulation is whole, and so taller geometry gets it for free. The gun says what it is doing, too. Every shot lights
103113 a **muzzle flash** — a star of additive light at the end of the barrel, sized
104114 by the weapon and gone in three frames — on the gun in your hands and on
105115 every other gun on the map, so a firefight across the site reads as people
src/client/bodies.test.ts +1 −1
@@ -103,7 +103,7 @@ test("a body is decoration: nobody collides with it and no bullet stops on it",
103103 // only box in this world is the floor the body is lying on.
104104 world.boxes = [{ min: [-400, -400, -64], max: [400, 400, 0], mat: 0 }];
105105 expect(world.boxes.length).toBe(1);
106 const walker: Body = { pos: [0, -100, 0], vel: [0, 0, 0], onGround: true, ducking: false, duckAmount: 0 };
106 const walker: Body = { pos: [0, -100, 0], vel: [0, 0, 0], onGround: true, ducking: false, duckAmount: 0, landSpeed: 0 };
107107 const wish: Wish = { forward: 1, side: 0, yaw: 90, jump: false, duck: false, walk: false, maxSpeed: MOVE.maxSpeed };
108108 for (let i = 0; i < 128; i++) move(walker, wish, 1 / 64, world.boxes);
109109 expect(walker.pos[1]).toBeGreaterThan(0); // straight over where the body lies
src/client/hud.test.ts +20
@@ -107,6 +107,12 @@ test("a hit from behind puts the wedge below the crosshair, one from the left on
107107 expect(Math.abs(side[0].y - H / 2)).toBeLessThan(20);
108108 });
109109
110 test("a fall hurts from straight below: no wedge, since there is no way it came from", () => {
111 const frame = recorder();
112 draw(frame, hurtWorld(0, [0, 0, 0], 0));
113 expect(wedgeArcs(frame.calls).length).toBe(0);
114 });
115
110116 test("hits stack, and damage to anyone but the player being drawn is not drawn", () => {
111117 const two = recorder();
112118 const world = hurtWorld(0, [-500, 0, 40], 0);
@@ -272,6 +278,20 @@ test("dead, the HUD is the watched player's: their numbers, no crosshair, and wh
272278 expect(frame.calls.filter((c) => c.name === "moveTo" && c.args[1] === H / 2).length).toBe(0);
273279 });
274280
281 test("a fall that killed you says so, rather than naming you as your own killer", () => {
282 const world = spectatedWorld();
283 world.feed.push({
284 at: world.time, killer: world.me.name, killerId: world.me.id, victim: world.me.name, victimId: world.me.id,
285 weapon: "fall", headshot: false, killerTeam: "ct",
286 });
287 const mate = world.players.find((p) => p.alive && p.team === "ct") as Player;
288 const frame = recorder();
289 draw(frame, world, mate);
290 const texts = frame.calls.filter((c) => c.name === "fillText").map((c) => c.text).join("|");
291 expect(texts).toContain("You fell too far");
292 expect(texts).toContain(`${world.me.name} Fall ${world.me.name}`);
293 });
294
275295 test("a spectator does not inherit the dead player's hit markers", () => {
276296 const world = spectatedWorld();
277297 const mate = world.players.find((p) => p.alive && p.team === "ct") as Player;
src/client/hud.ts +7 −4
@@ -21,7 +21,7 @@ import { drawReport } from "./reportPanel.ts";
2121 import { deathNotice } from "./spectate.ts";
2222 import { inaccuracy, WEAPONS } from "./weapons.ts";
2323 import {
24 canSee, currentWeapon, deploying, eyePos, grenadeCount, grenadeKinds, offeredPickup, useProgress,
24 canSee, causeName, currentWeapon, deploying, eyePos, grenadeCount, grenadeKinds, offeredPickup, useProgress,
2525 type Player, type World,
2626 } from "./world.ts";
2727
@@ -115,6 +115,9 @@ function drawDamage(
115115 if (effect.kind !== "hurt" || effect.victim !== view.id) continue;
116116 const age = world.time - effect.at;
117117 if (age < 0 || age > HURT_FADE) continue;
118 // A fall hurts from straight below: no bearing, so no wedge — the
119 // vignette and the flinch are the whole of it.
120 if (Math.hypot(effect.from[0] - eye[0], effect.from[1] - eye[1]) < 1) continue;
118121 const bearing = (Math.atan2(effect.from[1] - eye[1], effect.from[0] - eye[0]) * 180) / Math.PI;
119122 // Yaw grows anticlockwise, so a source to the left is a positive angle off
120123 // the view; the canvas turns clockwise from +x, so the wedge sits at minus
@@ -534,7 +537,7 @@ export function drawHud(
534537 const y = pad + Math.round((20 + i * 22) * scale);
535538 ctx.globalAlpha = clamp(8 - age, 0, 1);
536539 ctx.fillStyle = entry.killerTeam === "t" ? T_COLOR : CT_COLOR;
537 const text = `${entry.killer} ${WEAPONS[entry.weapon].name}${entry.headshot ? " ⌖" : ""} ${entry.victim}`;
540 const text = `${entry.killer} ${causeName(entry.weapon)}${entry.headshot ? " ⌖" : ""} ${entry.victim}`;
538541 ctx.fillText(text, width - pad, y);
539542 ctx.globalAlpha = 1;
540543 });
@@ -589,8 +592,8 @@ export function drawHud(
589592 if (notice !== null) {
590593 ctx.font = mid;
591594 ctx.fillStyle = "#ff8f86";
592 const how = `${notice.killer} killed you with ${WEAPONS[notice.weapon].name}` +
593 (notice.headshot ? " — headshot" : "");
595 const how = notice.weapon === "fall" ? "You fell too far" :
596 `${notice.killer} killed you with ${causeName(notice.weapon)}` + (notice.headshot ? " — headshot" : "");
594597 ctx.fillText(how, width / 2, top + Math.round(30 * scale));
595598 }
596599 ctx.font = small;
src/client/physics.test.ts +47
@@ -11,6 +11,7 @@ const body = (pos: Vec3 = [0, 0, 0]): Body => ({
1111 onGround: true,
1212 ducking: false,
1313 duckAmount: 0,
14 landSpeed: 0,
1415 });
1516
1617 const wish = (over: Partial<Wish> = {}): Wish => ({
@@ -129,3 +130,49 @@ test("fall damage starts only after a long drop", () => {
129130 expect(fallDamage(400)).toBe(0);
130131 expect(fallDamage(1000)).toBeGreaterThan(30);
131132 });
133
134 /** Drop a body from `height` above the floor and tick until it lands; every landSpeed it reported, in order. */
135 const drop = (height: number): number[] => {
136 const b = body([0, 0, height]);
137 b.onGround = false;
138 const seen: number[] = [];
139 for (let i = 0; i < 64 * 5; i++) {
140 move(b, wish(), 1 / 64, [ground]);
141 seen.push(b.landSpeed);
142 }
143 return seen;
144 };
145
146 test("a fall reports the speed it landed at on the one tick it ends, and 0 on every other", () => {
147 const seen = drop(320);
148 const landings = seen.filter((v) => v > 0);
149 expect(landings).toHaveLength(1);
150 // v² = 2gh: 715.5 u/s from 320 units, give or take one tick of gravity.
151 expect(landings[0]).toBeGreaterThan(Math.sqrt(2 * MOVE.gravity * 320) - MOVE.gravity / 64);
152 expect(landings[0]).toBeLessThan(Math.sqrt(2 * MOVE.gravity * 320) + MOVE.gravity / 64);
153 expect(seen[seen.length - 1]).toBe(0);
154 });
155
156 test("the tallest crate on either map, 128 units, lands under the threshold", () => {
157 const landed = Math.max(...drop(128));
158 expect(landed).toBeGreaterThan(400);
159 expect(fallDamage(landed)).toBe(0);
160 });
161
162 test("a jump from flat ground and back down is a landing, and a harmless one", () => {
163 const b = body();
164 move(b, wish({ jump: true }), 1 / 64, [ground]);
165 expect(b.landSpeed).toBe(0);
166 let landed = 0;
167 for (let i = 0; i < 128 && landed === 0; i++) {
168 move(b, wish(), 1 / 64, [ground]);
169 landed = b.landSpeed;
170 }
171 expect(Math.abs(landed - MOVE.jumpVelocity)).toBeLessThan(MOVE.gravity / 64);
172 expect(fallDamage(landed)).toBe(0);
173 });
174
175 test("fall damage is exactly its formula past the threshold", () => {
176 expect(fallDamage(579.99)).toBe(0);
177 expect(fallDamage(680)).toBeCloseTo(8.88, 5);
178 });
src/client/physics.ts +11
@@ -49,6 +49,12 @@ export type Body = {
4949 onGround: boolean;
5050 ducking: boolean;
5151 duckAmount: number;
52 /**
53 * How fast the body was coming down on the tick a fall ended, in u/s, and 0
54 * on every other tick — `move` writes it each call, so standing still never
55 * reads a landing twice. What `fallDamage` is asked about.
56 */
57 landSpeed: number;
5258 };
5359
5460 export type Wish = {
@@ -166,6 +172,7 @@ export function move(body: Body, wish: Wish, dt: number, boxes: Box[]): void {
166172 if (wish.walk) wishSpeed *= MOVE.walkSpeed / MOVE.maxSpeed;
167173 if (body.duckAmount > 0.5) wishSpeed *= MOVE.duckSpeed / MOVE.maxSpeed;
168174
175 const wasOnGround = body.onGround;
169176 if (body.onGround) {
170177 applyFriction(body.vel, dt);
171178 accelerate(body.vel, wishDir, wishSpeed, MOVE.accelerate, dt);
@@ -203,6 +210,8 @@ export function move(body: Body, wish: Wish, dt: number, boxes: Box[]): void {
203210 }
204211
205212 if (!body.onGround) body.vel[2] -= MOVE.gravity * dt * 0.5;
213 // The speed a landing is taken at, read before a floor can zero it.
214 const falling = -body.vel[2];
206215 if (slideAxis(body.pos, 2, body.vel[2] * dt, height, boxes)) body.vel[2] = 0;
207216
208217 // On the ground means "a floor within two units of the feet" — and landing
@@ -211,8 +220,10 @@ export function move(body: Body, wish: Wish, dt: number, boxes: Box[]): void {
211220 if (support !== null && body.vel[2] <= 0) {
212221 body.pos[2] = support;
213222 body.vel[2] = 0;
223 body.landSpeed = wasOnGround ? 0 : Math.max(0, falling);
214224 body.onGround = true;
215225 } else {
226 body.landSpeed = 0;
216227 body.onGround = false;
217228 }
218229 }
src/client/report.test.ts +8
@@ -40,12 +40,20 @@ test("a report says nothing about a pair the person was not part of", () => {
4040
4141 test("damage nobody threw — the bomb — is named for what it is", () => {
4242 const world = createWorld(SHORT, "me", "ct", 5, 407);
43 world.bomb.state = "detonated";
4344 world.damageLog[world.players.length][world.me.id] = { damage: 100, hits: 1, killed: true };
4445
4546 const rep = report(world, world.me);
4647 expect(rep.taken).toEqual(["the bomb — 100 dmg, 1 hit (killed)"]);
4748 });
4849
50 test("damage nobody threw in a round the bomb never went off in is a fall", () => {
51 const world = createWorld(SHORT, "me", "ct", 5, 407);
52 world.damageLog[world.players.length][world.me.id] = { damage: 12, hits: 1, killed: false };
53
54 expect(report(world, world.me).taken).toEqual(["a fall — 12 dmg, 1 hit"]);
55 });
56
4957 test("a player with nothing dealt or taken gets a report that says so, not a blank one", () => {
5058 const world = createWorld(SHORT, "me", "ct", 5, 409);
5159 const rep = report(world, world.me);
src/client/report.ts +10 −1
@@ -12,8 +12,17 @@ export type Report = {
1212 total: string;
1313 };
1414
15 /**
16 * `ENVIRONMENT` is one row for the bomb and for falls. The bomb only hurts
17 * anyone the tick it goes off, so a round where it did not is a round whose
18 * environmental damage was all falls; one where it did names the bomb, which
19 * is the whole of it on any map there is today (no drop on either reaches the
20 * fall threshold).
21 */
1522 const nameOf = (world: World, id: number): string =>
16 id === ENVIRONMENT ? "the bomb" : world.players.find((p) => p.id === id)?.name ?? "?";
23 id === ENVIRONMENT
24 ? (world.bomb.state === "detonated" ? "the bomb" : "a fall")
25 : world.players.find((p) => p.id === id)?.name ?? "?";
1726
1827 const line = (name: string, damage: number, hits: number, killed: boolean): string =>
1928 `${name} — ${Math.round(damage)} dmg, ${hits} hit${hits === 1 ? "" : "s"}${killed ? " (killed)" : ""}`;
src/client/world.test.ts +81 −1
@@ -5,7 +5,7 @@ import { dist } from "./math.ts";
55 import { DE_FORGE, MAP_NAMES, MAPS, placeAt, siteAt } from "./map.ts";
66 import { REPEAT_GAP, SIDE_GAP } from "./callout.ts";
77 import { halfTime, SHORT, winReward, type Rules } from "./match.ts";
8 import { GAIT_CYCLE, STEP_STRIDE } from "./physics.ts";
8 import { fallDamage, GAIT_CYCLE, STEP_STRIDE } from "./physics.ts";
99 import { WEAPONS, type WeaponId } from "./weapons.ts";
1010 import { snapshot, type Saved } from "./saved.ts";
1111 import {
@@ -1507,6 +1507,86 @@ test("the planted bomb's damage lands in the ledger with nobody credited", () =>
15071507 expect(taken[0].entry.killed).toBe(true);
15081508 });
15091509
1510 // --- falling --------------------------------------------------------------
1511
1512 /**
1513 * Drop `p` from `height` onto a bare floor and keep them standing on it for
1514 * `after` seconds; the speed `move` said they landed at comes back.
1515 */
1516 function fall(world: World, p: Player, height: number, after = 2): number {
1517 world.boxes = [{ min: [-4000, -4000, -32], max: [4000, 4000, 0], mat: 0 }];
1518 world.match.phase = "live";
1519 p.pos = [0, 0, height];
1520 p.vel = [0, 0, 0];
1521 p.onGround = false;
1522 let landed = 0;
1523 for (let t = 0; t < 3 + after; t += TICK) {
1524 world.time += TICK;
1525 applyInput(world, p, NO_INPUT, TICK);
1526 if (p.landSpeed > 0) landed = p.landSpeed;
1527 }
1528 return landed;
1529 }
1530
1531 const hurts = (world: World): number => world.effects.filter((e) => e.kind === "hurt").length;
1532
1533 test("a fall off the tallest crate on either map, 128 units, costs exactly nothing", () => {
1534 const world = createWorld(SHORT, "me", "ct", 5, 190);
1535 const landed = fall(world, world.me, 128);
1536 expect(landed).toBeGreaterThan(400);
1537 expect(world.me.health).toBe(100);
1538 expect(damageTaken(world, world.me.id)).toEqual([]);
1539 expect(hurts(world)).toBe(0);
1540 });
1541
1542 test("a 320-unit fall costs what fallDamage says for its impact speed, once, and the person feels it", () => {
1543 const world = createWorld(SHORT, "me", "ct", 5, 191);
1544 world.me.armor = 100;
1545 const landed = fall(world, world.me, 320, 3); // three seconds stood on the floor after
1546 const cost = Math.round(fallDamage(landed));
1547 expect(cost).toBe(12);
1548 expect(world.me.health).toBe(100 - cost);
1549 expect(world.me.armor).toBe(100); // armour is no help against the ground
1550 const taken = damageTaken(world, world.me.id);
1551 expect(taken).toEqual([{ id: ENVIRONMENT, entry: { damage: cost, hits: 1, killed: false } }]);
1552 expect(hurts(world)).toBe(1);
1553 });
1554
1555 test("a fall that kills credits the environment, reads as a fall in the feed, and pays nobody", () => {
1556 const world = createWorld(SHORT, "me", "ct", 5, 192);
1557 world.me.health = 5;
1558 const money = world.me.money;
1559 fall(world, world.me, 320);
1560 expect(world.me.alive).toBe(false);
1561 expect(world.me.health).toBe(0);
1562 expect(world.me.deaths).toBe(1);
1563 expect(world.me.kills).toBe(0);
1564 expect(world.me.money).toBe(money);
1565 const taken = damageTaken(world, world.me.id);
1566 expect(taken).toEqual([{ id: ENVIRONMENT, entry: { damage: 5, hits: 1, killed: true } }]);
1567 const line = world.feed[world.feed.length - 1];
1568 expect(line?.weapon).toBe("fall");
1569 expect(line?.victimId).toBe(world.me.id);
1570 expect(line?.killerId).toBe(world.me.id);
1571 });
1572
1573 test("a bot's fall costs it the same health and pushes no hurt effect nobody reads", () => {
1574 const world = createWorld(SHORT, "me", "ct", 5, 193);
1575 const bot = world.players.find((p) => p.bot) as Player;
1576 fall(world, bot, 320);
1577 expect(bot.health).toBe(88);
1578 expect(damageTaken(world, bot.id)[0]?.id).toBe(ENVIRONMENT);
1579 expect(hurts(world)).toBe(0);
1580 });
1581
1582 test("a hurt from straight below flinches up only, never sideways", () => {
1583 const world = createWorld(SHORT, "me", "ct", 5, 194);
1584 world.me.punch = [0, 0];
1585 flinch(world.me, [world.me.pos[0], world.me.pos[1], world.me.pos[2]], 20);
1586 expect(world.me.punch[0]).toBe(0);
1587 expect(world.me.punch[1]).toBeGreaterThan(0);
1588 });
1589
15101590 // --- the radio ------------------------------------------------------------
15111591
15121592 test("a call is one line of data on the person's own side, and the last one wins", () => {
src/client/world.ts +43 −8
@@ -21,7 +21,7 @@ import { DE_FORGE, MAPS, placeAt, siteAt, type MapDef } from "./map.ts";
2121 export { DEFAULT_MAP, MAP_NAMES, mapFor } from "./map.ts";
2222 import { angleVectors, angleDelta, clamp, dist, dist2d, normalize, rng, sub, type Vec3 } from "./math.ts";
2323 import * as rules from "./match.ts";
24 import { eyeHeight, GAIT_CYCLE, hullHeight, move, STEP_STRIDE, type Body, type Wish, MOVE } from "./physics.ts";
24 import { eyeHeight, fallDamage, GAIT_CYCLE, hullHeight, move, STEP_STRIDE, type Body, type Wish, MOVE } from "./physics.ts";
2525 import { callLabel, isCall, roundPlan, type Call, type Plan, type RadioCall } from "./plan.ts";
2626 import type { Saved, SavedPlayer } from "./saved.ts";
2727 import { visible } from "./trace.ts";
@@ -136,6 +136,14 @@ export type Effect =
136136 // the blast that ends it belong to nobody, so everyone may hear them.
137137 | { kind: "sound"; at: number; pos: Vec3; name: SoundName; pitch?: number; by?: number };
138138
139 /**
140 * What a feed line says did it: a weapon, or a fall. A fall is not a row in
141 * `WEAPONS` — it is never held, bought or aimed — so it is named here instead.
142 */
143 export type Cause = WeaponId | "fall";
144
145 export const causeName = (cause: Cause): string => (cause === "fall" ? "Fall" : WEAPONS[cause].name);
146
139147 // The ids are here as well as the names because the HUD asks the feed who
140148 // killed the person, and two players may answer to the same name.
141149 export type FeedEntry = {
@@ -144,7 +152,7 @@ export type FeedEntry = {
144152 killerId: number;
145153 victim: string;
146154 victimId: number;
147 weapon: WeaponId;
155 weapon: Cause;
148156 headshot: boolean;
149157 killerTeam: Team;
150158 };
@@ -153,8 +161,8 @@ export type FeedEntry = {
153161 export type DamageEntry = { damage: number; hits: number; killed: boolean };
154162
155163 /**
156 * The pseudo-attacker for damage nobody threw: the planted bomb going off.
157 * Not a player id — `damageTaken` hands it back so a report can still say
164 * The pseudo-attacker for damage nobody threw: the planted bomb going off, or
165 * the floor coming up too fast. Not a player id — `damageTaken` hands it back so a report can still say
158166 * where the damage came from.
159167 */
160168 export const ENVIRONMENT = -1;
@@ -260,7 +268,7 @@ function makePlayer(id: number, team: Team, name: string, bot: boolean): Player
260268 return {
261269 id, team, name, bot,
262270 pos: [0, 0, 0], vel: [0, 0, 0], yaw: 0, pitch: 0,
263 onGround: true, ducking: false, duckAmount: 0,
271 onGround: true, ducking: false, duckAmount: 0, landSpeed: 0,
264272 health: 100, armor: 0, helmet: false, alive: true,
265273 money: 800,
266274 weapon: secondary, primary: null, secondary,
@@ -834,7 +842,8 @@ const FLINCH_MAX = 6;
834842 export function flinch(victim: Player, from: Vec3, amount: number): void {
835843 const to = sub(from, eyePos(victim));
836844 const bearing = (Math.atan2(to[1], to[0]) * 180) / Math.PI;
837 const rel = (angleDelta(victim.yaw, bearing) * Math.PI) / 180;
845 // Straight below (a fall) has no side to kick away from: up only.
846 const rel = Math.hypot(to[0], to[1]) < 1 ? 0 : (angleDelta(victim.yaw, bearing) * Math.PI) / 180;
838847 const kick = clamp(amount, 0, 100) * FLINCH_PER_DAMAGE;
839848 victim.punch = [
840849 clamp(victim.punch[0] + kick * 0.5 * Math.sin(rel), -FLINCH_MAX, FLINCH_MAX),
@@ -936,14 +945,14 @@ export function shoot(world: World, p: Player): boolean {
936945 return true;
937946 }
938947
939 function onKill(world: World, killer: Player, victim: Player, weapon: WeaponId, headshot: boolean): void {
948 function onKill(world: World, killer: Player, victim: Player, weapon: Cause, headshot: boolean): void {
940949 victim.alive = false;
941950 victim.deaths += 1;
942951 victim.scoped = false;
943952 addBody(world.bodies, victim, world.boxes, world.time);
944953 // Only a grenade can reach the person who threw it, and killing yourself is
945954 // not a kill: the death and the feed line stand, the credit does not.
946 if (killer !== victim) {
955 if (killer !== victim && weapon !== "fall") {
947956 killer.kills += 1;
948957 if (headshot) killer.headshots += 1;
949958 killer.money = rules.capMoney(killer.money + WEAPONS[weapon].killReward, world.match.rules);
@@ -1295,6 +1304,30 @@ function footsteps(world: World, p: Player, wish: Wish, before: Vec3): void {
12951304
12961305 // --- the tick -------------------------------------------------------------
12971306
1307 /**
1308 * The landing tick of a fall: `fallDamage` of the speed `move` says the body
1309 * hit the floor at, taken the way the bomb's is — `ENVIRONMENT` in the ledger,
1310 * the victim as their own killer in the feed. Whole points, as every other
1311 * damage here is. Armour does nothing for it, as in CS. The tallest thing
1312 * anyone can stand on today is a 128-unit crate, which lands at ~452 u/s, so
1313 * on the maps there are this never costs anything; it is here so the
1314 * simulation is whole, and any taller floor gets it for free.
1315 */
1316 function land(world: World, p: Player): void {
1317 const amount = Math.round(fallDamage(p.landSpeed));
1318 if (amount <= 0) return;
1319 const dealt = Math.min(amount, p.health);
1320 p.health -= amount;
1321 const killed = p.health <= 0;
1322 // From the feet: straight below, so the hurt has no bearing to point at.
1323 hurt(world, p, [p.pos[0], p.pos[1], p.pos[2]], dealt);
1324 recordDamage(world, ENVIRONMENT, p.id, dealt, killed);
1325 if (killed) {
1326 p.health = 0;
1327 onKill(world, p, p, "fall", false);
1328 }
1329 }
1330
12981331 /** Is `id` something `p` can put in their hands right now? */
12991332 const holds = (p: Player, id: WeaponId): boolean =>
13001333 id === "knife" || id === p.primary || id === p.secondary ||
@@ -1321,6 +1354,8 @@ export function applyInput(world: World, p: Player, input: Input, dt: number): v
13211354 };
13221355 const before: Vec3 = [p.pos[0], p.pos[1], p.pos[2]];
13231356 move(p, wish, dt, world.boxes);
1357 if (p.landSpeed > 0) land(world, p);
1358 if (!p.alive) return;
13241359 footsteps(world, p, wish, before);
13251360 finishReload(world, p);
13261361 if (input.reload) reload(world, p);

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs