feat(consumables): eating, drinking, and the three clocks #152

Merged
sickday merged 1 commit from feat/consumables into main 2026-08-06 23:04:10 +00:00
Owner

Food and potions, which is what finally eats what Cooking makes. 163 rows
over one item op: 79 foods and 21 potion families in 84 doses.

What ships

Food — the cooked fish and meat, bread, the pie, pizza and cake chains
(a cake leaves two thirds, a pie leaves the dish), the two bowls, the seven
potatoes, kebabs, and the jug of wine. Pies carry their own skill boosts:
summer is +5 Agility and 10% run energy, wild is +5 Slayer and +4 Ranged,
admiral is +5 Fishing.

Potions — the four single-stat boosts and their supers, combat and super
combat, magic, the four flat non-combat boosts (agility, fishing, hunter,
magic essence), restore, super restore, prayer, energy, super energy,
stamina, and the Zamorak brew. A dose chain walks four down to one and leaves
a vial.

World.Energy.stamina/1 and Energy.gain/2 have had no caller since June and
now have one, and World.Content.StatBoost — the clock that walks a boosted
skill back down — has had nothing to decay and now has tenants.

Three things worth a reviewer's attention

A potion's duration is not stored anywhere, and that was already true.
World.Stats.boost/4 computes the percent of the base level and caps at
base-plus-boost, so a second dose of the same potion does nothing rather than
stacking; StatBoost then walks every boosted skill down one level a minute.
A super attack at 60 is +14 and therefore lasts fourteen minutes. Both
halves were written in June against no consumer. A stat potion is one line of
data here because of it.

The attack delay is neither max/2 nor a plain sum, and each of those is
wrong in a different place. Combo eating is published as stacking — a shark
and a karambwan on the same tick is 3 + 2 = 5, which max/2 answers as
three — while a player who eats during an ordinary weapon cooldown does not
have the cooldown lengthened by it, which a plain sum would do.
Contract.Consumable.swing/4 keys on whether anything was already consumed
this same tick
, which is the only reason the clocks carry a fourth field.
Six tests, because the wrong answers look right in the common case.

A row's kind and its verb are separate fields. kind (:food or
:potion) picks the clock and the shape of the sentence; verb is what the
item's own definition says and is what a click has to match. They come apart:
a jug of wine is food in every mechanical sense — heals 11, food clock,
gone in one gesture — and 1993's inventory actions are ["Drink", "Drop"].
Inferring the verb from the kind shipped it silently unclickable, falling
through to "Nothing interesting happens" and so indistinguishable from an
item nobody has written yet. The table's own cache test — every row's verb
must be one the item actually carries — caught it on its first run.

The one structural change

Eating is the first input in this engine whose rule is about when, and the
three clocks are absolute ticks. Player.Dispatch.run/2 has no tick and did
not need one, so rather than hand forty clauses an argument one of them uses,
every backpack op now routes through a new Dispatch.item_op/3 — following
the precedent the drop op already set in Player.Host — and
Content.Item.interact/4 becomes /5.

One consequence worth knowing: run/2's remaining :item_op clause is now
reached only by a container this server has not bound, which is what it
always meant.

The clocks live in player.combat, beside next_swing and in_combat_until
— the absolute-tick clocks are already there, and three separate counters is
what makes combo eating possible, so it is where the concept belongs. No new
top-level field on World.Player, which is at the 32-key cliff.

Declined, with reasons

All of it is in priv/data/consumables.exs's own header, where somebody
adding a row will be standing. None of the reasons is scheduling:

  • Anything that overheals — anglerfish, both brews, Guthix rest. They heal
    above the hitpoints base, and StatBoost skips :hitpoints deliberately.
    Shipping the heal without a decay leaves a player permanently over their
    maximum. This is the largest single follow-on and it is one clock.
  • Anything whose heal is published only as a range — cooked sweetcorn
    (1–10), cave eel (7–11), the snail meats; the cooked slimy eel is not stated
    at all. Four of Cooking's own outputs land here and each is listed with its
    reason in a test.
  • Every antipoison, antidote, anti-venom, Sanfew serum and antifire —
    there is no poison, venom or dragonfire, so there is nothing to cure and
    nothing to hold off. A stub that says "you feel better" and does nothing is
    worse than the engine admitting it has no behaviour: the player cannot
    detect the first.
  • Overload, Absorption, the dome's super ranging and super magic, and the
    raid potions
    — minigame-bound, and neither minigame is built.
  • The barbarian mixes — on the food clock rather than the potion one,
    and from content that does not exist. A data change the day it does.
  • Ales, cocktails and the plain kebab — the drunk mechanic, a stateful
    shaker, and a roll that includes poisoning the eater.

Sourcing

Wiki, per the ported-constant convention, and it mattered: a cross-check
against an era-adjacent implementation disagreed in three places. Cooked meat
and chicken heal 3 and it says 4; manta ray heals 22; and its super
combat potion is 10 + 10% against the published 5 + 15% — which agree at
99 and nowhere else, so a wrong table would have been believable the whole way
up. Dating: food heals have never been rebalanced and the two wiki pages agree
row for row where they overlap, which is the check that was run rather than a
claim. The only family whose changes section reaches into its own boost is the
Zamorak brew, last moved 2014-04-24, three years before this cache.

Gates

Full suite 3623 tests, 1561 doctests, 60 properties, 0 failures. mix credo clean. mix dialyzer reports exactly the two pre-existing
guard_fails of #131 and no new ones. mix format --check-formatted flags
only test/support/login_packet.ex, which is #131's other half and was
deliberately not touched.

Not done

No live sitting. The animation (829, ten frames over 73 client cycles
against a three-tick action), the two sounds (2393 and 2401, both present
in archive 4 and both resolving to a real head packet), and the exact message
strings are derived and tested but have not been watched from a real client.
Cache.Seq does not decode frame sounds, so if 829 carries one of its own a
player hears it twice — the same decoder gap already recorded for sound.

Bots cannot eat. Bot.Goals has no food step, so a fighter that runs out
of hitpoints still has no answer, which is a real gap now that pkers roam.

Herblore is unbuilt, so not one potion is made in this world — every
family is bank- or shop-only. The mirror of Cooking's finding that a skill's
coverage is not its reachability, from the other side: the consumable is built
and its source is not.

Food and potions, which is what finally *eats* what Cooking makes. 163 rows over one item op: **79 foods and 21 potion families in 84 doses.** ## What ships **Food** — the cooked fish and meat, bread, the pie, pizza and cake chains (a cake leaves two thirds, a pie leaves the dish), the two bowls, the seven potatoes, kebabs, and the jug of wine. Pies carry their own skill boosts: summer is +5 Agility and 10% run energy, wild is +5 Slayer and +4 Ranged, admiral is +5 Fishing. **Potions** — the four single-stat boosts and their supers, combat and super combat, magic, the four flat non-combat boosts (agility, fishing, hunter, magic essence), restore, super restore, prayer, energy, super energy, stamina, and the Zamorak brew. A dose chain walks four down to one and leaves a vial. `World.Energy.stamina/1` and `Energy.gain/2` have had no caller since June and now have one, and `World.Content.StatBoost` — the clock that walks a boosted skill back down — has had nothing to decay and now has tenants. ## Three things worth a reviewer's attention **A potion's duration is not stored anywhere, and that was already true.** `World.Stats.boost/4` computes the percent of the *base* level and caps at base-plus-boost, so a second dose of the same potion does nothing rather than stacking; `StatBoost` then walks every boosted skill down one level a minute. A super attack at 60 is `+14` and therefore lasts fourteen minutes. Both halves were written in June against no consumer. A stat potion is one line of data here because of it. **The attack delay is neither `max/2` nor a plain sum**, and each of those is wrong in a different place. Combo eating is published as stacking — a shark and a karambwan on the same tick is `3 + 2 = 5`, which `max/2` answers as three — while a player who eats during an ordinary weapon cooldown does not have the cooldown lengthened by it, which a plain sum would do. `Contract.Consumable.swing/4` keys on **whether anything was already consumed this same tick**, which is the only reason the clocks carry a fourth field. Six tests, because the wrong answers look right in the common case. **A row's `kind` and its `verb` are separate fields.** `kind` (`:food` or `:potion`) picks the clock and the shape of the sentence; `verb` is what the item's own definition says and is what a click has to match. They come apart: a **jug of wine is food in every mechanical sense** — heals 11, food clock, gone in one gesture — and `1993`'s inventory actions are `["Drink", "Drop"]`. Inferring the verb from the kind shipped it silently unclickable, falling through to *"Nothing interesting happens"* and so indistinguishable from an item nobody has written yet. The table's own cache test — every row's verb must be one the item actually carries — caught it on its first run. ## The one structural change Eating is the first input in this engine whose rule is about *when*, and the three clocks are absolute ticks. `Player.Dispatch.run/2` has no tick and did not need one, so rather than hand forty clauses an argument one of them uses, every backpack op now routes through a new `Dispatch.item_op/3` — following the precedent the drop op already set in `Player.Host` — and `Content.Item.interact/4` becomes `/5`. One consequence worth knowing: `run/2`'s remaining `:item_op` clause is now reached **only** by a container this server has not bound, which is what it always meant. The clocks live in `player.combat`, beside `next_swing` and `in_combat_until` — the absolute-tick clocks are already there, and three separate counters is what makes combo eating possible, so it is where the concept belongs. No new top-level field on `World.Player`, which is at the 32-key cliff. ## Declined, with reasons All of it is in `priv/data/consumables.exs`'s own header, where somebody adding a row will be standing. None of the reasons is scheduling: - **Anything that overheals** — anglerfish, both brews, Guthix rest. They heal *above* the hitpoints base, and `StatBoost` skips `:hitpoints` deliberately. Shipping the heal without a decay leaves a player permanently over their maximum. This is the largest single follow-on and it is one clock. - **Anything whose heal is published only as a range** — cooked sweetcorn (1–10), cave eel (7–11), the snail meats; the cooked slimy eel is not stated at all. Four of Cooking's own outputs land here and each is listed with its reason in a test. - **Every antipoison, antidote, anti-venom, Sanfew serum and antifire** — there is no poison, venom or dragonfire, so there is nothing to cure and nothing to hold off. A stub that says "you feel better" and does nothing is worse than the engine admitting it has no behaviour: the player cannot detect the first. - **Overload, Absorption, the dome's super ranging and super magic, and the raid potions** — minigame-bound, and neither minigame is built. - **The barbarian mixes** — on the *food* clock rather than the potion one, and from content that does not exist. A data change the day it does. - **Ales, cocktails and the plain kebab** — the drunk mechanic, a stateful shaker, and a roll that includes poisoning the eater. ## Sourcing Wiki, per the ported-constant convention, and it mattered: a cross-check against an era-adjacent implementation disagreed in three places. Cooked meat and chicken heal **3** and it says 4; manta ray heals **22**; and its super combat potion is `10 + 10%` against the published `5 + 15%` — which agree at 99 and nowhere else, so a wrong table would have been believable the whole way up. Dating: food heals have never been rebalanced and the two wiki pages agree row for row where they overlap, which is the check that was run rather than a claim. The only family whose changes section reaches into its own boost is the Zamorak brew, last moved 2014-04-24, three years before this cache. ## Gates Full suite **3623 tests, 1561 doctests, 60 properties, 0 failures**. `mix credo` clean. `mix dialyzer` reports **exactly** the two pre-existing `guard_fail`s of #131 and no new ones. `mix format --check-formatted` flags only `test/support/login_packet.ex`, which is #131's other half and was deliberately **not** touched. ## Not done **No live sitting.** The animation (`829`, ten frames over 73 client cycles against a three-tick action), the two sounds (`2393` and `2401`, both present in archive 4 and both resolving to a real head packet), and the exact message strings are derived and tested but have not been watched from a real client. `Cache.Seq` does not decode frame sounds, so if `829` carries one of its own a player hears it twice — the same decoder gap already recorded for sound. **Bots cannot eat.** `Bot.Goals` has no food step, so a fighter that runs out of hitpoints still has no answer, which is a real gap now that pkers roam. **Herblore is unbuilt**, so not one potion is made in this world — every family is bank- or shop-only. The mirror of Cooking's finding that a skill's coverage is not its reachability, from the other side: the consumable is built and its source is not.
feat(consumables): eating, drinking, and the three clocks
All checks were successful
ci / gates (pull_request) Successful in 1m1s
build / image (push) Successful in 32s
ci / gates (push) Successful in 1m5s
2e5ab025cf
Food and potions, which is what finally eats what Cooking makes. 163 rows
over one item op: 79 foods and 21 potion families in 84 doses.

Food covers the cooked fish and meat, bread, the pie, pizza and cake chains,
the bowls, the seven potatoes, kebabs and the jug of wine, with the pies'
own skill boosts. Potions cover the four single-stat boosts and their
supers, combat and super combat, magic, the four flat non-combat boosts,
restore, super restore, prayer, energy, super energy, stamina and the
Zamorak brew. A dose chain walks four down to one and leaves a vial.

Two things were already built and had been waiting for a caller:
World.Stats.boost/4 caps against the base, so a second dose of the same
potion does nothing, and World.Content.StatBoost walks boosts down a level a
minute -- which means a potion's duration is not stored anywhere. It is the
decay clock and the size of the boost between them.

The timing is the part with a rule worth reading. Three separate counters --
food, karambwan, potion -- are the whole of combo eating, and the attack
delay they impose is neither max/2 nor a plain sum: two consumes on the same
tick stack (3 + 2 = 5), while eating during a weapon cooldown does not
lengthen it. Contract.Consumable.swing/4 keys on whether anything was
consumed this same tick, which is why the clocks carry a fourth field.

This is the first input whose rule is about *when*, so every backpack op now
routes through Player.Dispatch.item_op/3 with the tick, following the
precedent the drop op already set, and Content.Item.interact/4 becomes /5.
The clocks live in player.combat, which is where the absolute-tick clocks
already are and where combo eating belongs.

A row states both what it is (:food or :potion, which picks the clock and
the sentence) and what the item's own definition says (Eat or Drink). Those
are separate fields because they come apart: a jug of wine is food in every
mechanical sense and its action is Drink. Inferring one from the other made
it silently unclickable, and the table's own cache test is what found it.

Declined with reasons, all recorded in the data file's header: anything that
overheals (no clock for hitpoints above base), anything whose heal is
published only as a range, every antipoison and antifire (nothing to cure),
and the minigame-bound potions.
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!152
No description provided.