Milestones and constrained progression

Include <vhyl-rpg-progression.t> after the core. It works without condition, conflict, language, dialogue or reporting modules. PCs and NPCs use the same point and development model; publication remains an explicit author decision.

Progression is driven by authored objectives and learning opportunities. It does not reward repeated rolls, command counts, failed-treatment grinding or unbounded encounter loops.

Milestone awards

RpgProfile gains earnedPoints, unspentPoints, developmentOpen and developmentRevision. Points are nonnegative integers. Earned points are a lifetime total capped at one million; unspent points cannot exceed that total. An authored starting bank must initialise both counters consistently.

An RpgMilestone has a stable unique ID, 1–10000 points and an observational available(profile) gate returning a reason ID or nil. Predeclare one RpgAward receipt for each allowed profile/milestone pair. Missing or duplicate receipts are configuration errors.

safePassage: RpgMilestone id = 'chapter.safe-passage' points = 6;
passageReceipt: RpgAward profile = protagonist milestone = safePassage;

Call rpgAwardMilestone(profile, milestone) from an authored committed objective consequence. A fresh eligible award claims the receipt and increases both point counters before milestone.apply(profile) runs. That hook may unlock learning opportunities. A repeated award returns reused, adds nothing and does not invoke the hook. Exceptions roll back the receipt, counters and consequences together. Undoing the original objective also undoes its award, so redoing it reproduces one consistent history rather than generating extra points.

Authored development offers

An RpgDevelopment belongs to one profile and has a unique ID. Set unlocked when the fiction supplies a mentor, relevant discovery, practice or meaningful experience. By default it also requires profile.developmentOpen, which the game sets at a suitable pause. Its independent available(profile) gate can enforce further story requirements. Unlocking does not itself spend points.

kind Target Price
skill An owned RpgTraining slot and target point investment Target investment minus existing investment
attribute One attribute identifier and target score Score difference × per-level price
trait An authored RpgTrait and positive cost Explicit authored cost
remove-trait A currently owned limitation trait and positive cost Explicit authored buy-off cost

Attribute prices are 10 for strength/health, 20 for coordination/intelligence, and 5 for Will/Perception per point. Attribute targets are 1–100. Skill targets are 1–10000 invested points, not desired effective skill scores. Trait prices are 1–10000. The library does not infer trait prices or refund removed limitations. Authors must price a buy-off consistently with the game's starting budget.

Reserve a new skill using an owned RpgTraining with zero points in the profile's skills list. It remains untrained, using only an explicitly permitted default, until purchased. Mutable training slots cannot be shared between profiles when targeted by offers; validation rejects that configuration. No persistent runtime skill entities are allocated during play.

fieldMedicine: RpgTraining skill = firstAid points = 0;
lesson: RpgDevelopment id = 'learn.field-medicine' profile = protagonist
    training = fieldMedicine target = 2;

rpgAssessDevelopment(offer) is observational. It returns ready, reused or blocked, the current price and a reason where applicable. Queries consume no points, time, randomness, receipts or history. Check affordability after the current price is computed, so competing offers cannot spend a stale preview.

rpgDevelop(offer) commits a currently ready offer. It marks it purchased, deducts the current price, changes the owned value and increments the development revision before its apply(profile) consequence. An already purchased or already attained target returns reused without charging. A blocked, locked, unaffordable or out-of-downtime offer changes nothing. An exception rolls back the entire purchase and any game consequences.

Ordinary purchase gates return rpg.development-locked, rpg.downtime-required, rpg.insufficient-points or an authored reason. A dependent attribute exceeding the score ceiling returns rpg.attribute-limit. Invalid declarations throw a configuration error at startup.

Dependent attributes and existing attempts

Intelligence purchases raise Will and Perception by the same difference, preserving independently purchased or authored offsets. Health purchases preserve the missing-energy deficit when the default maximum energy rises. Strength purchases increase default HP capacity while leaving stored injury intact. Explicitly overridden maxHP or maxEnergy properties retain their authored capacity rules; the library does not replace them. A higher capacity does not resurrect a dead body or clear an unconscious flag automatically.

A purchase does not globally reset challenge attempts. An offer may declare a reopens list of related RpgChallenge objects. Only those revisions advance, once on a fresh purchase. A completed goal remains completed. This keeps unrelated failed checks settled and prevents free rerolls from buying an irrelevant skill. Previously recorded scores, modifiers, costs and faces remain historical snapshots; improvements affect fresh calculations, not old reports.

Progression does not implement training-hour accounting, free-form respecs, automatic advantages, attribute decreases for refunds or supernatural permission. The game authors which opportunities exist and when their development window is open. Close that window during a confrontation or other unsuitable activity.

Publication and persistence

rpgPublishProgress(value) explicitly emits rpg.progress:

version (1), actor, kind, ID, status, reason, points,
unspentPoints, earnedPoints, developmentRevision

Kind is award or purchase. Award points are gained; purchase points are spent or required by a ready/blocked assessment. Reused results have zero points. A purchase assessment and its committed result use the same shape. NPC banks and locked story opportunities must only be published when the author intends them to be visible.

Receipts, banks, unlocks, purchased flags, training, attributes, trait lists, related challenge revisions and consequence state all participate in undo and logical saves. Do not store temporary RpgProgressResult instances in persistent properties. The gatehouse example demonstrates objective awards, constrained purchases, new assessments and a safe downtime flow.