Retune the case ramp, and let leading a phase earn experience

The ramp made every phase after the first Difficult (halving the lead's skill) and
stacked a cumulative -5% on top. Separated in phaseRampFor() in rules.mjs:
difficulty belongs to the phase (only Containment is Difficult, and it does not
also ramp), the modifier is attrition (-5 a phase, capped at -15).

  lead 55%   was 55 -> 22 -> 17 -> 12 -> 7      now 55 -> 50 -> 45 -> 27 -> 40

Over 20,000 simulated cases with destitution modelled, phases passed roughly
double and destitution incidence roughly halves.

R-222: resolveLegDialog passed no skillFamilyId, so the choke point could not
resolve the skill item and leading a phase never earned a tick — Tradecraft, the
skill the job most uses, could never improve.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
slaguru666
2026-08-10 21:12:04 +01:00
co-authored by Claude Opus 5
parent 1a0bdbd2df
commit 1b44fccb79
31 changed files with 171 additions and 82 deletions
+46
View File
@@ -3717,3 +3717,49 @@ players did. This is a design decision rather than a defect — a case is meant
survived rather than won — but if phases are supposed to be passable, the ramp
(−5 per phase, on top of `difficult`) is the dial, not `DEFENCE_STEP` and not the
skill ratings.
## The case ramp, retuned
The old ramp made every phase after the first Difficult — which HALVES the lead's
skill — and then stacked a cumulative −5% on top of that. The two pressures were
never the same idea, and they are now separated in `phaseRampFor(index, kindId)` in
`rules.mjs`, with six spot-checks:
- **Difficulty is about the phase.** Only Containment is Difficult — the step the case
is actually about — and it does not also take the ramp. It is hard because it is
containment, not because you are tired.
- **The modifier is attrition.** −5 per phase, capped at −15, because a fifth phase is
not four times worse than a second.
```
was now
lead 40% 40 → 15 → 10 → 5 → 1 40 → 35 → 30 → 20 → 25
lead 55% 55 → 22 → 17 → 12 → 7 55 → 50 → 45 → 27 → 40
lead 70% 70 → 30 → 25 → 20 → 15 70 → 65 → 60 → 35 → 55
```
Simulated over 20,000 full five-phase cases per row, WITH the destitution rule
modelled (a spent track upgrades every remaining phase to Difficult):
```
lead 40% passed 0.72 → 1.37 of 5 hit destitution 87% → 63% of cases
lead 55% passed 1.14 → 2.06 of 5 hit destitution 72% → 40%
lead 70% passed 1.60 → 2.78 of 5 hit destitution 54% → 20%
```
Destitution almost always lands on the final phase rather than compounding, so it
stings rather than spirals — my first description of it as a death spiral was
overstated.
Played two cases on the new numbers. SHORT ORCHARD passed 2 of 5 and ended scraped
clean at Resources 0 but Containment 7. PALE VERGE passed 4 of 5 including two
specials, failed the Containment step, and came home at Containment 9, Resources 4.
Both read as cases rather than as arithmetic.
**R-222 · leading a phase earned no experience.** `resolveLegDialog` called
`skillRoll` with an actor and a rating but **no skill family**, so the choke point
could not resolve the skill item and the tick never fired. This is the same fault the
internal-callers fix addressed — that fix resolves the item FROM the family, and this
caller supplied none — so Tradecraft, the skill the job most uses, was still the one
that could never improve. Fixed and verified: PALE VERGE's lead ticked Tradecraft and
took it 62% → 66% at close.
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/10-20:41:41.141023 174647000 Recovering log #3
2026/08/10-20:41:41.141046 174647000 Level-0 table #5: started
2026/08/10-20:41:41.141166 174647000 Level-0 table #5: 1902 bytes OK
2026/08/10-20:41:41.141467 174647000 Delete type=0 #3
2026/08/10-20:41:41.141512 174647000 Delete type=3 #2
2026/08/10-21:10:35.033759 175517000 Recovering log #3
2026/08/10-21:10:35.033780 175517000 Level-0 table #5: started
2026/08/10-21:10:35.033929 175517000 Level-0 table #5: 1902 bytes OK
2026/08/10-21:10:35.034226 175517000 Delete type=0 #3
2026/08/10-21:10:35.034283 175517000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/10-20:41:32.871312 16fbcb000 Delete type=3 #1
2026/08/10-21:10:26.742472 172977000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/10-20:41:41.147740 175e6b000 Recovering log #3
2026/08/10-20:41:41.147761 175e6b000 Level-0 table #5: started
2026/08/10-20:41:41.147869 175e6b000 Level-0 table #5: 4129 bytes OK
2026/08/10-20:41:41.148109 175e6b000 Delete type=0 #3
2026/08/10-20:41:41.148134 175e6b000 Delete type=3 #2
2026/08/10-21:10:35.042390 175d23000 Recovering log #3
2026/08/10-21:10:35.042410 175d23000 Level-0 table #5: started
2026/08/10-21:10:35.042554 175d23000 Level-0 table #5: 4129 bytes OK
2026/08/10-21:10:35.042821 175d23000 Delete type=0 #3
2026/08/10-21:10:35.042870 175d23000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/10-20:41:32.894989 16ebb3000 Delete type=3 #1
2026/08/10-21:10:26.766828 173183000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/10-20:41:41.142024 175e6b000 Recovering log #3
2026/08/10-20:41:41.142110 175e6b000 Level-0 table #5: started
2026/08/10-20:41:41.142362 175e6b000 Level-0 table #5: 27954 bytes OK
2026/08/10-20:41:41.142565 175e6b000 Delete type=0 #3
2026/08/10-20:41:41.142606 175e6b000 Delete type=3 #2
2026/08/10-21:10:35.034917 175d23000 Recovering log #3
2026/08/10-21:10:35.035011 175d23000 Level-0 table #5: started
2026/08/10-21:10:35.035308 175d23000 Level-0 table #5: 27954 bytes OK
2026/08/10-21:10:35.035591 175d23000 Delete type=0 #3
2026/08/10-21:10:35.035641 175d23000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/10-20:41:32.873111 1703d7000 Delete type=3 #1
2026/08/10-21:10:26.744343 17419b000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/10-20:41:41.134409 174647000 Recovering log #3
2026/08/10-20:41:41.134636 174647000 Level-0 table #5: started
2026/08/10-20:41:41.135086 174647000 Level-0 table #5: 60660 bytes OK
2026/08/10-20:41:41.135295 174647000 Delete type=0 #3
2026/08/10-20:41:41.135327 174647000 Delete type=3 #2
2026/08/10-21:10:35.025087 175517000 Recovering log #3
2026/08/10-21:10:35.025512 175517000 Level-0 table #5: started
2026/08/10-21:10:35.026438 175517000 Level-0 table #5: 60660 bytes OK
2026/08/10-21:10:35.026776 175517000 Delete type=0 #3
2026/08/10-21:10:35.026817 175517000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/10-20:41:32.883213 16f3bf000 Delete type=3 #1
2026/08/10-21:10:26.754647 172977000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/10-20:41:41.138725 174647000 Recovering log #3
2026/08/10-20:41:41.138746 174647000 Level-0 table #5: started
2026/08/10-20:41:41.138865 174647000 Level-0 table #5: 3295 bytes OK
2026/08/10-20:41:41.139133 174647000 Delete type=0 #3
2026/08/10-20:41:41.139175 174647000 Delete type=3 #2
2026/08/10-21:10:35.031149 1744ff000 Recovering log #3
2026/08/10-21:10:35.031171 1744ff000 Level-0 table #5: started
2026/08/10-21:10:35.031288 1744ff000 Level-0 table #5: 3295 bytes OK
2026/08/10-21:10:35.031521 1744ff000 Delete type=0 #3
2026/08/10-21:10:35.031553 1744ff000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/10-20:41:32.889425 16f3bf000 Delete type=3 #1
2026/08/10-21:10:26.760755 17419b000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/10-20:41:41.132366 174647000 Recovering log #3
2026/08/10-20:41:41.132578 174647000 Level-0 table #5: started
2026/08/10-20:41:41.133060 174647000 Level-0 table #5: 54921 bytes OK
2026/08/10-20:41:41.133331 174647000 Delete type=0 #3
2026/08/10-20:41:41.133365 174647000 Delete type=3 #2
2026/08/10-21:10:35.021326 175517000 Recovering log #3
2026/08/10-21:10:35.021545 175517000 Level-0 table #5: started
2026/08/10-21:10:35.022420 175517000 Level-0 table #5: 54921 bytes OK
2026/08/10-21:10:35.022844 175517000 Delete type=0 #3
2026/08/10-21:10:35.022893 175517000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/10-20:41:32.878370 16fbcb000 Delete type=3 #1
2026/08/10-21:10:26.750048 17419b000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/10-20:41:41.144398 175e6b000 Recovering log #3
2026/08/10-20:41:41.144451 175e6b000 Level-0 table #5: started
2026/08/10-20:41:41.144690 175e6b000 Level-0 table #5: 23316 bytes OK
2026/08/10-20:41:41.144892 175e6b000 Delete type=0 #3
2026/08/10-20:41:41.144919 175e6b000 Delete type=3 #2
2026/08/10-21:10:35.037687 175d23000 Recovering log #3
2026/08/10-21:10:35.037745 175d23000 Level-0 table #5: started
2026/08/10-21:10:35.038003 175d23000 Level-0 table #5: 23316 bytes OK
2026/08/10-21:10:35.038279 175d23000 Delete type=0 #3
2026/08/10-21:10:35.038312 175d23000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/10-20:41:32.893484 16ebb3000 Delete type=3 #1
2026/08/10-21:10:26.765165 173183000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/10-20:41:41.136311 174647000 Recovering log #3
2026/08/10-20:41:41.136348 174647000 Level-0 table #5: started
2026/08/10-20:41:41.136491 174647000 Level-0 table #5: 6679 bytes OK
2026/08/10-20:41:41.136799 174647000 Delete type=0 #3
2026/08/10-20:41:41.136837 174647000 Delete type=3 #2
2026/08/10-21:10:35.028347 175517000 Recovering log #3
2026/08/10-21:10:35.028398 175517000 Level-0 table #5: started
2026/08/10-21:10:35.028587 175517000 Level-0 table #5: 6679 bytes OK
2026/08/10-21:10:35.028935 175517000 Delete type=0 #3
2026/08/10-21:10:35.029009 175517000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/10-20:41:32.865001 16fbcb000 Delete type=3 #1
2026/08/10-21:10:26.736502 172977000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/10-20:41:41.145560 174e53000 Recovering log #3
2026/08/10-20:41:41.145600 174e53000 Level-0 table #5: started
2026/08/10-20:41:41.145824 174e53000 Level-0 table #5: 16892 bytes OK
2026/08/10-20:41:41.146032 174e53000 Delete type=0 #3
2026/08/10-20:41:41.146059 174e53000 Delete type=3 #2
2026/08/10-21:10:35.039746 1744ff000 Recovering log #3
2026/08/10-21:10:35.039788 1744ff000 Level-0 table #5: started
2026/08/10-21:10:35.040037 1744ff000 Level-0 table #5: 16892 bytes OK
2026/08/10-21:10:35.040363 1744ff000 Delete type=0 #3
2026/08/10-21:10:35.040421 1744ff000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/10-20:41:32.887732 16ebb3000 Delete type=3 #1
2026/08/10-21:10:26.759088 173183000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/10-20:41:41.146606 174647000 Recovering log #3
2026/08/10-20:41:41.146628 174647000 Level-0 table #5: started
2026/08/10-20:41:41.146719 174647000 Level-0 table #5: 3739 bytes OK
2026/08/10-20:41:41.146990 174647000 Delete type=0 #3
2026/08/10-20:41:41.147036 174647000 Delete type=3 #2
2026/08/10-21:10:35.041184 175517000 Recovering log #3
2026/08/10-21:10:35.041207 175517000 Level-0 table #5: started
2026/08/10-21:10:35.041346 175517000 Level-0 table #5: 3739 bytes OK
2026/08/10-21:10:35.041618 175517000 Delete type=0 #3
2026/08/10-21:10:35.041664 175517000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/10-20:41:32.885357 1703d7000 Delete type=3 #1
2026/08/10-21:10:26.757057 173183000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/10-20:41:41.137507 17565f000 Recovering log #3
2026/08/10-20:41:41.137545 17565f000 Level-0 table #5: started
2026/08/10-20:41:41.137683 17565f000 Level-0 table #5: 5105 bytes OK
2026/08/10-20:41:41.137984 17565f000 Delete type=0 #3
2026/08/10-20:41:41.138042 17565f000 Delete type=3 #2
2026/08/10-21:10:35.029938 174d0b000 Recovering log #3
2026/08/10-21:10:35.029970 174d0b000 Level-0 table #5: started
2026/08/10-21:10:35.030128 174d0b000 Level-0 table #5: 5105 bytes OK
2026/08/10-21:10:35.030410 174d0b000 Delete type=0 #3
2026/08/10-21:10:35.030446 174d0b000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/10-20:41:32.867714 16fbcb000 Delete type=3 #1
2026/08/10-21:10:26.739179 173183000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/10-20:41:41.143254 174647000 Recovering log #3
2026/08/10-20:41:41.143279 174647000 Level-0 table #5: started
2026/08/10-20:41:41.143426 174647000 Level-0 table #5: 5012 bytes OK
2026/08/10-20:41:41.143706 174647000 Delete type=0 #3
2026/08/10-20:41:41.143760 174647000 Delete type=3 #2
2026/08/10-21:10:35.036441 175517000 Recovering log #3
2026/08/10-21:10:35.036467 175517000 Level-0 table #5: started
2026/08/10-21:10:35.036615 175517000 Level-0 table #5: 5012 bytes OK
2026/08/10-21:10:35.036909 175517000 Delete type=0 #3
2026/08/10-21:10:35.036961 175517000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/10-20:41:32.874846 1703d7000 Delete type=3 #1
2026/08/10-21:10:26.746192 173183000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/10-20:41:41.139842 175e6b000 Recovering log #3
2026/08/10-20:41:41.139878 175e6b000 Level-0 table #5: started
2026/08/10-20:41:41.140047 175e6b000 Level-0 table #5: 7240 bytes OK
2026/08/10-20:41:41.140366 175e6b000 Delete type=0 #3
2026/08/10-20:41:41.140415 175e6b000 Delete type=3 #2
2026/08/10-21:10:35.032367 175d23000 Recovering log #3
2026/08/10-21:10:35.032410 175d23000 Level-0 table #5: started
2026/08/10-21:10:35.032599 175d23000 Level-0 table #5: 7240 bytes OK
2026/08/10-21:10:35.032954 175d23000 Delete type=0 #3
2026/08/10-21:10:35.033016 175d23000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/10-20:41:32.869495 16ebb3000 Delete type=3 #1
2026/08/10-21:10:26.740769 17419b000 Delete type=3 #1
+7
View File
@@ -5015,6 +5015,13 @@ async function resolveLegDialog(expedition) {
actor: best.actor,
label: game.i18n.format("RINGBRP.Expedition.LegRoll", { n: idx + 1, terrain: leg.terrain ?? "" }),
rating: best.rating,
// R-222. The family was never passed, so skillRoll could not resolve the skill
// item, and a phase led successfully earned no experience tick. This is the same
// fault the internal-callers fix addressed — it resolves the item FROM the
// family, and this caller supplied none. The skill the job most uses was still
// the one that could never improve.
skillFamilyId: best.skill,
categoryId: "mental",
difficulty,
situational: (cancelled ? 0 : Number(leg.modifier) || 0) + rewardBonus,
flavourLines: [
+29
View File
@@ -368,6 +368,35 @@ export function defencePenaltyFor(defencesUsed = 0) {
return DEFENCE_STEP * Math.max(0, Number(defencesUsed) || 0);
}
/**
* HOW HARD A CASE PHASE IS.
*
* The old ramp made every phase after the first Difficult — which HALVES the lead's
* skill — and then stacked a cumulative −5% on top. Measured across two played
* cases, a realistic lead of 40–55% (Tradecraft is a support skill on most postings
* that train it at all) faced 20% → 15% → 10% → 5% → 1%, and both cases were decided
* by Resources running out rather than by anything the players did.
*
* The two pressures are now separated, because they were never the same idea:
*
* DIFFICULTY is about the phase. Only Containment is Difficult — the step the case
* is actually about — and it does not also take the ramp. It is hard because it is
* containment, not because you are tired.
*
* The MODIFIER is about attrition. It accumulates as the case wears on, and it
* stops accumulating, because a fifth phase is not four times worse than a second.
*/
export const PHASE_RAMP = { step: -5, cap: -15, hardKinds: ["containment"] };
export function phaseRampFor(index, kindId = "") {
const hard = PHASE_RAMP.hardKinds.includes(String(kindId));
return {
difficulty: hard ? "difficult" : "average",
// A hard phase carries its difficulty INSTEAD of the ramp, never both.
modifier: hard ? 0 : Math.max(PHASE_RAMP.cap, PHASE_RAMP.step * (Number(index) || 0))
};
}
/* -------------------------------------------- */
/* Borrowed Authority */
/* -------------------------------------------- */
+7
View File
@@ -197,6 +197,13 @@ const checks = [
["defenceTypeAllowed('','parry')", RULES.defenceTypeAllowed("","parry"), "parry"],
// defences are graded: a defence stops its own quality or worse, and only
// shaves a better blow by as many steps as it was outclassed by
// the case ramp: attrition accumulates and stops; only Containment is Difficult
["phaseRampFor(0) is the way in", RULES.phaseRampFor(0,"approach").difficulty, "average"],
["phaseRampFor(0) modifier", RULES.phaseRampFor(0,"approach").modifier, 0],
["phaseRampFor(2) modifier", RULES.phaseRampFor(2,"search").modifier, -10],
["phaseRampFor(9) is capped", RULES.phaseRampFor(9,"search").modifier, -15],
["containment is Difficult", RULES.phaseRampFor(3,"containment").difficulty, "difficult"],
["containment does NOT also ramp", RULES.phaseRampFor(3,"containment").modifier, 0],
// access is a record: scope is a hard ceiling, not a difficulty
["scopeRank('routine')", RULES.scopeRank("routine"), 2],
["scopeRank(unknown)", RULES.scopeRank("nonesuch"), 0],
+4 -4
View File
@@ -19,6 +19,7 @@ import {
NAMES_FIRST, NAMES_LAST, NPC_ROLES, PHASE_KINDS, COMPLICATIONS, CLOCKS,
COHERENCE_HOOKS, RESOLUTIONS, DEBRIEF_QUESTIONS, FAILURE_STATES, GROUPS
} from "./mission-tables.mjs";
import { phaseRampFor } from "../rules.mjs";
/* ---------------- picking ---------------- */
const pick = (arr, r) => arr[Math.floor(r() * arr.length)];
@@ -111,13 +112,12 @@ function buildPhases(r, { anomaly, gap }) {
name: k.name,
terrain: pick(k.terrains, r),
about: k.verb,
difficulty: i === 0 ? "average" : "difficult",
// The classification modifier belongs to the CROSSING, not to every phase. Both
// applied, so a Wrong-class extraction came out at -35% on top of Difficult,
// which floors a 71% Tradecraft to 1% and makes the case unplayable rather than
// hard. Phases carry their own gentle ramp; the classification bites on the way
// home, where the fiction says it should.
modifier: -(i * 5),
// hard. Phases carry their own ramp; the classification bites on the way home,
// where the fiction says it should. The ramp itself lives in rules.mjs.
...phaseRampFor(i, k.id),
// Costs must be affordable from the starting budget. They totalled 7 against a
// starting 6 for every non-Near class, so half of all cases were arithmetically
// unfinishable before the extraction phase.