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

A bullet that lands does not slow the player it hits: no tagging, so trading a shot costs the enemy nothing #226

Merged opened this

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

Merged

A bullet that lands now tags the player it hits. Each point of damage takes **1%** of their ground speed (`TAG_PER_DAMAGE = 0.01`). It never takes more than half, however many hits land (`TAG_FLOOR = 0.5`). It wears off at **100% of full speed per second** (`TAG_RECOVERY = 1`). So 30 in the chest holds a runner to 175 u/s and is gone in 0.3 s, and the deepest tag is gone in 0.5 s. It is meant as a…

shipped merged

opened this

A bullet that lands now tags the player it hits. Each point of damage takes 1% of their ground speed (TAG_PER_DAMAGE = 0.01). It never takes more than half, however many hits land (TAG_FLOOR = 0.5). It wears off at 100% of full speed per second (TAG_RECOVERY = 1). So 30 in the chest holds a runner to 175 u/s and is gone in 0.3 s, and the deepest tag is gone in 0.5 s. It is meant as a hesitation, not a stun.

How it works. Player.tag is the share of speed a hit has taken off. tag() in world.ts sets it, called from shoot for every hit, so a bot shooting the person and the person shooting a bot go through the same code. applyInput multiplies the wish speed by (1 - p.tag) and winds the tag down each tick; vel is never edited. A player in the air is not tagged. startRound clears the tag next to punch, and so does the arena's equip. flinch, the damage numbers and the hitgroup table are unchanged.

Tests, in world.test.ts:

  • A runner hit for 30 covers more than 10 units less ground over the next half second than the same runner not hit.
  • The tag is gone by the stated times, and full speed comes back.
  • Two hits cap at half speed, and a runner hit every tick for a second holds about 125 u/s.
  • A player in the air is not tagged.
  • startRound clears everyone's tag.
  • A bot's bullet in the person and the person's bullet in a bot produce the same tag through shoot.

With the two call sites removed, the distance, cap and symmetry tests fail. The decay, air and round-start tests call tag() directly, so they break only when their own piece of the rule goes.

One test changed on purpose. In src/rl/arena.test.ts, the test that pins the shipped policy's episodes action for action had its expected values updated: 1080 → 810 actions, hash 3779f480fec06e8f → 4b4b2fc68a8a1af1. Its own comment asks for a deliberate update when a physics change is meant to move it, and this one is. The episodes are shorter because fights end sooner.

Measured, same seeds before and after:

MeasurementBeforeAfter
scripts/duel.ts, de_forge, shipped policy175/29/36, 73% won170/32/38, 71% won
scripts/duel.ts, de_forge, scripted93/74/73, 39%94/76/70, 39%
scripts/duel.ts --map de_cinder, shipped policy181/31/28, 75%174/40/26, 73%
scripts/duel.ts --map de_cinder, scripted103/70/67, 43%112/65/63, 47%
scripts/measure.ts, plant rate62%64%
scripts/measure.ts, defuse rate13%19%
scripts/measure.ts, T/CT split60/4056/44

The shipped policy loses 2 points on each map, which is within the couple of points the issue allowed. The scripted baseline gains 4 on de_cinder. I did not retrain.

Left out:

  • HE grenades and the bomb blast do not tag. The issue is about bullets, and the arena the policy was trained in has no grenades.
  • The tag is not saved in a snapshot, the same as punch.
  • It is not one of the policy's inputs; observe.ts is untouched.

The README's What is real and what is not has the rule and its numbers next to the hit-feedback text. bun run check passes (797 tests) and / answers 200. To try it in the preview, sprint into a bot's line of fire: you drop to a jog for a moment after each hit, and a bot you shoot mid-push stutters the same way.

🤖 Generated with Claude Code

Closes #189

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs