PvP, and the wilderness it happens in #143

Merged
sickday merged 9 commits from feat/pvp into main 2026-08-06 16:56:34 +00:00
Owner

PvP, and the wilderness it happens in. Seven commits, built 2026-08-04 and
sat with in front of a real client the same night; rebased onto current main
(28 commits, five shared files — the object-action dispatch was restructured
into skilled/3 + screen/3 on main, so the ditch check now rides in front
of that chain rather than in its own copy of the old cond).

The wilderness is the client's own statement. Clientscript 384's three
rectangles, applied in order, all four planes — surface, God Wars Dungeon,
caves. level/1 stays faithful to the script because the bracket is computed
from it; in?/1 is the different question — may I be attacked here — and
begins where the ditch ends, at y=3523. The two are pinned to each other by a
test. The overlay draws itself: interface 90's on_timer runs the script
client-side, so the server's whole part is the IF_OPENSUB.

Who may attack whom is Contract.Pvp: the bracket from the lower of the
two tiles' levels (symmetric by construction, guarded by test), the claim
window for single-way, and every swing re-asks — which buys the era's escape
mechanics for free in both directions.

A swing crosses two seats with no new machinery: the same
World.Combat.swing/0 a creature sends, the same defend/4 resolving it,
sent as a send/2 to the other session per the standard. The defender casts
the landed number back so the attacker can pay its own experience. Specials
cross as a hit pattern the defender expands, because every hit in a cascade
turns on a roll only the defender can make.

A death has a killer: the pile goes to the top damager on the ordinary
drop clock, a skull takes the kept three to zero, and rest/1 clears tally,
claim, stamp and skull in one call.

The ditch is real: loc 23271, its 325 placements read for axis, the
20-cycle slide and animation 6132 (confirmed by eye in the sitting).

Bots need no privileged path: Bot.Goals.Pk is HuntNpcs with the
quarry changed, clicking the option a person clicks and refused by the same
gates. A pker rises at the bank nearest the wilderness and walks in;
confirmed in play — three bots crossed on their own, fought at depth 10, one
died and came back.

Merge note: the rebase resolved Bot.Kinds against the fisher/trader/
runecrafter kinds and config-aware seed levels from main (pker keeps
fighting_levels/0 shared with the fighter), and dropped a duplicate
Bot.Act.player_action/3 both lines had added independently.

Two commits ride on top of the seven. The drop entry has carried its owner
since the pile learned to belong to a killer, so the nest and pack-light
tests written on main against the three-element shape now state the fourth.
And the third fight pairing gets its world test: bot against bot and player
against player each had one, bot against person had only a live sitting —
now a pker claims a seated person at the kind's own seed levels and blood
crosses, with retaliation left to the engine.

Gates: suite green both before and after the additions (4956+ tests, the
only reds being the two #131 machine-local ones — login_packet.ex format
and the two Dialyzer guard_fails — left untouched per that issue).

PvP, and the wilderness it happens in. Seven commits, built 2026-08-04 and sat with in front of a real client the same night; rebased onto current main (28 commits, five shared files — the object-action dispatch was restructured into `skilled/3` + `screen/3` on main, so the ditch check now rides in front of that chain rather than in its own copy of the old cond). **The wilderness is the client's own statement.** Clientscript 384's three rectangles, applied in order, all four planes — surface, God Wars Dungeon, caves. `level/1` stays faithful to the script because the bracket is computed from it; `in?/1` is the different question — may I be attacked here — and begins where the ditch ends, at y=3523. The two are pinned to each other by a test. The overlay draws itself: interface 90's `on_timer` runs the script client-side, so the server's whole part is the `IF_OPENSUB`. **Who may attack whom** is `Contract.Pvp`: the bracket from the lower of the two tiles' levels (symmetric by construction, guarded by test), the claim window for single-way, and every swing re-asks — which buys the era's escape mechanics for free in both directions. **A swing crosses two seats** with no new machinery: the same `World.Combat.swing/0` a creature sends, the same `defend/4` resolving it, sent as a `send/2` to the other session per the standard. The defender casts the landed number back so the attacker can pay its own experience. Specials cross as a hit pattern the defender expands, because every hit in a cascade turns on a roll only the defender can make. **A death has a killer**: the pile goes to the top damager on the ordinary drop clock, a skull takes the kept three to zero, and `rest/1` clears tally, claim, stamp and skull in one call. **The ditch is real**: loc 23271, its 325 placements read for axis, the 20-cycle slide and animation 6132 (confirmed by eye in the sitting). **Bots need no privileged path**: `Bot.Goals.Pk` is `HuntNpcs` with the quarry changed, clicking the option a person clicks and refused by the same gates. A pker rises at the bank nearest the wilderness and walks in; confirmed in play — three bots crossed on their own, fought at depth 10, one died and came back. Merge note: the rebase resolved `Bot.Kinds` against the fisher/trader/ runecrafter kinds and config-aware seed levels from main (pker keeps `fighting_levels/0` shared with the fighter), and dropped a duplicate `Bot.Act.player_action/3` both lines had added independently. Two commits ride on top of the seven. The drop entry has carried its owner since the pile learned to belong to a killer, so the nest and pack-light tests written on main against the three-element shape now state the fourth. And the third fight pairing gets its world test: bot against bot and player against player each had one, bot against person had only a live sitting — now a pker claims a seated person at the kind's own seed levels and blood crosses, with retaliation left to the engine. Gates: suite green both before and after the additions (4956+ tests, the only reds being the two #131 machine-local ones — `login_packet.ex` format and the two Dialyzer `guard_fail`s — left untouched per that issue).
Clientscript 384 computes the wilderness level for the overlay the client
draws itself, which makes it the one statement of the boundary that cannot
be out of era -- it ships in the operator's cache. Three rectangles, applied
in order, all four planes.

The order is not incidental. The second rectangle sits inside the third and
subtracts one where the third adds one, so the same tile reads two levels
lower through it; applying them in any other order returns a plausible wrong
level for one map square, and a wrong level is a wrong combat bracket. The
spawn file names that square for free: 12 Spiritual rangers, 12 Spiritual
mages, 11 Spiritual warriors, 8 Orks and 8 Aviansie stand in it.

Level zero means outside, because every rectangle bottoms out at one -- the
same identity script 388 leans on when it writes an empty string rather than
"Level: 0".
Six gates, cheapest first, in the shape the creature side already has --
the rules that decide a fight belong outside the thing that would have to
be stood up to reach them.

The bracket is the one decision here that is not the era's own. The era
states it from the attacker's side; this takes the lower of the two tiles'
levels so the relation is symmetric. Two players in melee range are at most
one tile and therefore one level apart, so the rules almost never disagree
-- and where they do, the asymmetric one hands out a player who may be
attacked and may not attack back, which the era did not intend either. The
symmetry test is the guard on that argument.

Retaliation asks none of this, exactly as it asks none of the aggression
gates: walking out of the bracket does not end a fight already joined.
A soft clock at interval one, armed at login -- darkness's shape, and for
the same reason: being in the wilderness is a property of where you are
standing, and nothing else in the tick watches for a player crossing a line
that has no door on it.

The transition is the event. The clock carries inside? in its own arguments
(World.Player has no field left) and acts only when it flips, so a player
standing in the wilderness costs nothing per tick rather than two packets.
A fresh clock claims to be outside whatever the tile says, which is what
sends both packets to somebody logging in past the ditch.

Nothing states the level. Component 90:46 carries an on_timer onto script
388, which invokes 384 against the player's own coordinate, so the overlay
refreshes its own text for as long as it is open -- the skill guide's shape
again. What the cache does not say is which frame component to bind it into,
so the slot is a guess with a runtime setter beside it, exactly as the
darkness slot is, and deliberately not the same slot: the wilderness runs
underground, and one slot would have the two overlays close each other every
tick.
Against a creature, "the attacker states numbers and the defender rolls" is
a figure of speech -- the bestiary is persistent_term and one process does
both halves. Against another player it is literal, and it needs no new
machinery: what crosses is the same World.Combat.swing a creature sends and
the same defend/4 resolves it.

Three things had to be true for that to work.

The source is now tagged. An untagged index was two numberings sharing one
field, and a retaliation aimed at creature slot 7 instead of player slot 7
is a fight with a bystander.

The queue clock is one less from a player, and that is arithmetic rather
than tuning. The world posts every session its tick message before it waits
on any of them, so a swing sent during a player phase is already behind that
message in the target's mailbox whichever order the two run in -- it lands
after every tick-N pass. A creature's swing is sent from the NPC phase and
lands before them. Same landing tick, one fewer clock to reach it; the extra
one would be a hit a tick late, and one fewer for a creature is the flinch
bug the melee work measured.

The experience comes back. An attacker that does not roll the damage cannot
pay itself, so the defender casts the landed number back with the swing that
earned it and the attacker grants its own from the stance the swing states.
One message per landing, against the alternative of publishing a defence
roll on every player's row every tick -- which charges the whole population
for a feature two of them are using.

The snapshot row grew the two facts an attacker reads, and put_player takes
the tick because one of them cannot be answered without it: a claim falls
back to whoever last hit you, and that stamp means nothing outside its
window.
The death script has said "nothing here knows what killed the player" since
it was written. Now something does, and three things follow from it.

The pile is addressed to the top damager rather than to the corpse, read off
a tally that counts applied damage -- the same answer the creature side
gives, and for the same reason: finishing somebody another player brought to
one hitpoint does not win their belongings. Killed by the world it is still
yours, on the hour-long private clock; killed by a player it is theirs on the
ordinary one, so the spoils of a public fight go public after a minute. That
distinction is machinery that already existed; :death and :drop have meant
exactly this since ground items landed.

The skull is a countdown rather than a deadline, and that is the shape
decision worth stating: the two places that ask -- the respawn, and the block
that draws the icon -- are a World.Script and a struct, neither of which is
handed a tick. A countdown is answerable by anybody holding the player, and
the wilderness clock spending it is already running for everybody. It follows
a player south of the ditch and out of the game on purpose, because a skull
that ended at the ditch would deter nothing.

Everything the fight left behind is cleared in one call rather than four. A
stale tally addresses the next death to somebody who was not there.
Two halves of the same thing: a way in, and somebody to meet there.

The ditch is a solid loc -- measured, not assumed: a step from the tile south
of it onto it is refused by the collision map -- so without a Cross that
carries the player over, every rule on this branch is unreachable except by
teleport. It is force_move/5's first production caller, which is what #62 has
been waiting for. Everything about the loc comes from the operator's own
cache: 23271 is the only one named Wilderness Ditch, Cross is its only
action, it is 1x2 and interact_type 1, and it is placed 325 times -- 321 in
one line at y=3521 and four on their own with an odd rotation, which is why
the axis is read off the placement rather than guessed from where the player
happens to stand.

The spans fall out of the footprint: two deep, so the far side is the anchor
plus two from the south and minus one from the north. Two numbers are ported
and neither is in the cache -- the twenty-cycle slide, and animation 6132,
which is the one thing here a live sitting still has to confirm.

The PK bot is HuntNpcs with the quarry changed, and that it is only the
quarry changing is the point: a bot is the engine's fourth seat, so it clicks
the option a person clicks and is refused by the same six gates. Bot against
bot and bot against player are the same code on both sides. It asks
Contract.Pvp before it clicks rather than after, because a socketless bot
never reads the refusal -- a chat line it cannot see is a fight that silently
never starts.

Every pker gets the same levels, which is not laziness: the bracket is a
level difference, so a fleet of mixed levels is a fleet that mostly may not
attack itself. A pker raised south of the ditch is given a ground north of
it, computed from the wilderness's own geometry rather than typed.
Six things went wrong in front of a real client, and two of them were the
same thing seen from opposite ends.

**The bank is not the wilderness.** Clientscript 384's rectangle starts at
y=3520, the tile *south* of the ditch, so the client draws "Level: 1" for
somebody standing safely on the bank. Taking that as the rule put an Attack
option on a player nothing should be able to touch -- and, from the other
side, stopped every bot from ever crossing: `crossing_needed?` asks "am I not
in the wilderness", and standing on the bank answered "you are", so they
walked into the ditch and the router returned no steps forever. `level/1`
stays faithful to the client because the *bracket* is computed from it;
`in?/1` is now the different question -- may I be attacked here -- and it
begins where the ditch ends, at 3523, which is the tile a crossing lands on.
The two are pinned to each other by a test.

**Specials did nothing in PvP.** The toggle armed, the energy never spent,
the button looked broken. A special's hit *pattern* now crosses in the swing
and the **defender** expands it, which is the same division of labour as
everything else here one level down: every hit in a claw cascade turns on an
accuracy roll only the defender can make.

**A restored bot lost its ground.** A goal config is stored atom-keyed and
restored through String.to_existing_atom/1, which drops a key whose atom is
not interned yet at boot -- the trap the exchange work already paid for. The
goal defaulted the missing ground to *here*, so a pker restored in Lumbridge
decided Lumbridge was the wilderness and hunted nobody, forever, logging
nothing. It re-derives now.

**A pker rises at Edgeville and walks in**, which is the operator's call and
the more truthful one. It is also the only shape that works: Travel plans a
long leg as a straight-line hop 80 tiles ahead, which lands in whatever
building or river is there, so the 270 tiles from Lumbridge cannot be walked
and the 27 from the bank can. Fighting depth is 10 for two reasons that
agree -- a bracket wide enough to get a fight, and one leg from the crossing.
Death closes the loop with no new mechanism: the crossing fails fast, and the
brain's recovery relocates to the nearest bank to the anchor, which is the
bank it started from.

Confirmed in play afterwards: three bots walked from the bank, crossed on
their own, ran to level 10, fought each other, and one died and came back.
A drop has carried its owner since the pile learned to belong to a
killer: queue_drop/4 stamps the dropper unless a killer is named. The
nest and pack-light tests were written against the three-element entry
on a main that had not met the fourth, so the rebase left them
asserting a shape the engine no longer produces. They now state the
owner -- the dropper -- rather than pattern-matching around it.
test(bot): a pker picks a fight with a person, on the same gates
All checks were successful
ci / gates (pull_request) Successful in 57s
build / image (push) Successful in 31s
ci / gates (push) Successful in 1m11s
6efb7b7d8e
Bot against bot and player against player each had a world test; bot
against person had only the sitting. The missing pairing is the cheap
one: seat a person at the kind's own seed levels -- read from the kind
rather than restated, since a fresh account forty combat levels below
the bot is refused by the bracket before a byte crosses -- and wait for
the pker to claim them. Blood in either direction proves the crossing,
because retaliation is engine-owned.
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Revenant/Server!143
No description provided.