Skip to content

release: v0.50.0#1

Open
yanekyuk wants to merge 58 commits into
mainfrom
release/0.50.0
Open

release: v0.50.0#1
yanekyuk wants to merge 58 commits into
mainfrom
release/0.50.0

Conversation

@yanekyuk

Copy link
Copy Markdown
Contributor

jurnalis-engine v0.50.0

Tests: 1,588 passing

Changelog

Features

  • Rogue Cunning Action — Dash, Disengage, Hide as Bonus Action at level 2 (level-gated)
  • Merchant NPC shop interaction with seeded inventory generation
  • Active buff/condition display in combat status line and buffs command
  • Advantage/disadvantage reason shown inline in combat output
  • NPC disposition tags in room entry and examine narration
  • take all / drop all bulk item commands
  • wait, pass, skip as end-turn aliases (SRD: doing nothing on your turn is valid)
  • hp / health command alias for compact health display
  • sleep, camp, nap as rest command aliases
  • Examine NPC command

Fixes

  • Barbarian Rage damage bonus (+2 STR melee) — was never applied
  • Barbarian Rage damage resistance (halve BPS damage) — was never applied
  • Rogue Expertise (double proficiency for two chosen skills) — was never applied
  • Sneak Attack ally check no longer counts non-combatant NPCs (merchants)
  • Ranged spell attacks now apply hostile-within-5ft disadvantage
  • Spell cast targeting — no-"at" syntax and two-step target prompt
  • Death save prompt — explicit unconscious message + tally each tick
  • "You fall unconscious!" emitted immediately when HP drops to 0
  • Wait-flavored narration when main action unused (even after bonus action spent)
  • Healing potion usable as Bonus Action on active combat turns
  • NPC aggro indicator before combat begins on room entry
  • Point-buy error shows remaining/overspent budget
  • NPC dialogue doubled definite article ("the the") fixed

yanekyuk added 30 commits April 26, 2026 07:49
… engine code

- Updates all inline comments, docstrings, markdown docs, and spec files
to reference SRD 2024 (and SRD 5.2.1 where the licensed text is cited)
instead of the outdated SRD 5.1 label.
- Removes legacy .opencode/agents, .opencode/skills, and
.opencode/commands files that referenced the old SRD version.
- Adds missing QA reports v0.38.4 and v0.40.1.
Rogue Sneak Attack previously fired unconditionally on any non-
disadvantaged hit because the ally-adjacency trigger (SRD 5.2.1
path 2) was approximated as "always true when no Disadvantage."

Now the two SRD trigger conditions are properly enforced:
  1. Advantage on the attack roll, OR
  2. A non-incapacitated Friendly NPC in the same location AND
     no Disadvantage on the roll.

Adds has_adjacent_ally() helper in lib.rs to check for Friendly,
non-incapacitated NPCs at the player's current location. Updates
narration to indicate which trigger fired ("advantage" or
"adjacent ally").

Closes #312
Per SRD 5.2.1, any hit against a character at 0 HP adds 1 death save
failure (2 on a critical hit). The existing `apply_damage_while_dying`
method on `CombatState` already implements this logic — the gap was that
`resolve_npc_spell` in `lib.rs` was not calling it after subtracting spell
damage from a dying player.

All three NPC spell paths (Fire Bolt, Magic Missile, Scorching Ray) now
capture `was_dying` before deducting damage and call
`apply_damage_while_dying` + `narrate_damage_while_dying_outcome` when
the player was already at 0 HP. Bumps engine to 0.45.3.

Closes #311
fix(engine): require Advantage or adjacent ally for Sneak Attack
fix(engine): apply death-save failures on incoming damage at 0 HP
Returns the NPC name with a [hostile], [neutral], or [merchant] tag
derived from the NPC's role and disposition fields.
narrate_enter_location and narrate_look now use npc.display_name()
instead of npc.name.clone(), surfacing [hostile], [neutral], or
[merchant] tags next to NPC names.
…output

Add roll_mode_reason field to AttackResult. Both resolve_player_attack and
resolve_npc_attack now collect a short reason string (<=5 words) for whichever
roll mode wins after SRD cancellation. format_attack_roll_details renders it
as "(disadvantage: hostile within 5 ft — keeping N)" instead of the bare
"(disadvantage — keeping N)".

Covers: target dodging, grappled, hostile within 5 ft, beyond normal range,
nonproficient armor, all attacker conditions (poisoned, blinded, prone,
frightened, restrained, invisible), all defender conditions (prone, stunned,
paralyzed, petrified, restrained, unconscious, blinded, invisible), vex
mastery, and reckless attack.

Closes #314
Change the take-all output from "You pick up everything: ..." to
"You pick up: ..." to match the acceptance criteria in #319. Add two
new tests: one verifying the exact output format lists each item name,
and one confirming take-single still works alongside take-all.
`sleep`, `camp`, and `nap` now route into the existing rest flow:
- Bare form prompts "Short rest or long rest?" (same as bare `rest`)
- `short sleep`, `long camp`, `nap long`, etc. map to ShortRest/LongRest
- Both `<qualifier> <alias>` and `<alias> <qualifier>` orders accepted

Closes #320
Fix test_look_at_npc_returns_rich_description which was vacuously
passing (no NPC at location 0 for seed 42). Place a known NPC in the
test room and assert the disposition tag appears on the first line.

Add four new integration tests covering all acceptance criteria from
issue #317:
- examine <npc> returns description with [merchant] disposition tag
- fuzzy name matching (e.g. "examine petra" matches "Petra the Keeper")
- NPC not in current room returns "You don't see any" message
- friendly NPCs show [neutral] tag with "friendly" disposition sentence
feat(parser): add sleep, camp, nap as rest command aliases
feat(engine): examine <npc> command — integration tests
Call `generate_merchant_items` from `generate_world` after item
generation, merging merchant stock into WorldState.items with
non-overlapping IDs (starting at item_count). Each Merchant NPC
now carries 8-12 SRD-drawn items at world creation, deterministically
seeded.

Closes #316
yanekyuk added 28 commits April 27, 2026 15:19
…t (#330, #331)

Issue #330: `cast magic missile Rowan` (no "at") now works.
Add `parser::split_spell_and_target` for longest-prefix matching against
the spell catalog; both the exploration and combat Cast handlers call
`try_resplit_spell_name` when the raw spell name fails lookup so that
the remainder becomes the target.

Issue #331: `cast magic missile` (no target) stores the spell name in
`CombatState::pending_spell`; the next combat input is treated as the
target name. `cancel` / `nevermind` aborts without consuming the action.
Leveled spells refund their slot at the prompt and re-consume it on fire.

Add 16 new tests: unit tests for the parser helper and integration tests
covering the single-turn no-"at" path and the two-step flow for Wizard
(Fire Bolt, Magic Missile) and Cleric (Sacred Flame, Guiding Bolt).
All 47 spell-casting tests pass.
…tacks

Ranged spell attacks (Fire Bolt, Scorching Ray, Guiding Bolt, Eldritch
Blast) now respect SRD 2024 "Ranged Attacks in Close Combat": when a
hostile creature is within 5 ft, the attack roll is made with
disadvantage (two d20s, keep lower).

Changes:
- SpellAttackResult gains roll_second, disadvantage fields for dual-roll
- roll_spell_attack accepts disadvantage param, rolls 2d20 when true
- New format_spell_attack_details shows "(disadvantage: hostile within
  5 ft �� keeping N)" label matching weapon attack output
- Orchestrator computes hostile_within_5ft before each player ranged
  spell attack and passes it to resolve functions
- NPC spell paths pass false (no change to NPC behavior)
- Two integration tests: Fire Bolt at 5 ft shows disadvantage label,
  Fire Bolt at 30 ft does not

Closes #332
The ally-adjacency path in apply_sneak_attack previously assumed allies
were always present when the attacker had no disadvantage. This caused
Sneak Attack to fire even when the only friendly NPC in the room was a
Merchant bystander — not a combat ally.

Add NpcRole::is_combatant() to distinguish combat-capable roles (Guard,
Adventurer) from civilian bystanders (Merchant, Hermit). The new
has_friendly_combatant_ally() helper checks for an actual friendly
combatant in the current room before allowing the ally path.

Closes #333
…ly_combatant_ally

- Merge srd-classes.md conflict: use PR's combatant-aware Path 2 description
  while keeping next's SRD 5.2.1 reference and incapacitated-check note.
- Merge lib.rs conflicts: keep weapon eligibility check first (next structure),
  replace has_adjacent_ally with has_friendly_combatant_ally throughout.
- Fix advantage_path to gate on !result.disadvantage (PR fix).
- Remove erroneously added guard from test_rogue_sneak_attack_does_not_fire_
  without_advantage_or_ally (conflict artifact).
- Add inventory: vec![] to 6 Npc struct literals missing the field (5 pre-existing
  on next + 1 new in test_rogue_sneak_attack_fires_via_ally_path).
- Push guard ally to loc.npcs in test_rogue_sneak_attack_fires_via_ally_path so
  has_friendly_combatant_ally can find it via location index.
fix(engine): exclude non-combatant NPCs from Sneak Attack ally check
- Add docs/reports/v0.45.2.md (drove PRs #334/#335/#336)
- Add missing inventory field to Npc test fixture so tests compile
  after merchant inventory landed in #329
… while raging)

When rage_active is true, incoming bludgeoning, piercing, and slashing damage
is now halved (floor division) before being applied to the player's current_hp
at both player HP reduction sites in combat/mod.rs: the main NPC attack loop
and the opportunity attack loop. Rage resistance stacks independently with
Uncanny Dodge per SRD 5.2.1. Adds three unit tests covering the halving
behaviour, the floor division invariant, and the no-op path when not raging.
Bumps version to 0.48.0. Closes #339.
… raging)

Adds `apply_rage_damage_bonus` helper in `lib.rs` (mirroring the
`apply_magic_weapon_bonuses` / `apply_sneak_attack` pattern) and calls
it at all relevant `resolve_player_attack` call sites. The +2 flat bonus
is gated on class == Barbarian, rage_active == true, melee attack (not
AMMUNITION/ranged), and STR-based damage (FINESSE weapons only qualify
when STR >= DEX). Unarmed strikes always qualify. Appends a
"(+2 rage damage.)" narration line on eligible hits.

Also documents the Rage damage bonus in docs/domain/srd-5e-mechanics.md
and bumps jurnalis-engine to v0.48.0. Closes #338.
fix(engine): apply Barbarian Rage damage bonus (+2 to STR melee while raging)
…chosen skills)

Implement Rogue Expertise as per SRD 5.2.1: at level 1, the Rogue chooses two
skill proficiencies to gain Expertise, doubling the proficiency bonus for those
skills. The fix scales with level (PB is computed at check time, not stored).

Changes:
- Add `expertise_skills: Vec<Skill>` field to Character (serde-defaulted)
- Update `skill_modifier()` to double PB when skill is in expertise list AND proficient
- Add `has_expertise()` helper to Character
- Insert `ChooseExpertiseSkills` creation step between ChooseSkills and ChooseAlignment for Rogue
- Update `skill_check()` to accept expertise_skills and double PB at check time
- Update `passive_check()` to accept has_expertise and apply doubling
- Update all 5 skill_check and 2 passive_check call sites in lib.rs
- Add 8 new tests covering the fix and regression case
- Update 4 Rogue creation test flows to include the expertise selection step

The stats display automatically reflects the doubled modifier since it calls
`skill_modifier()` directly.

Closes #340
…ion at level 2

- Add level gate (class == Rogue && level >= 2) to existing BonusDash and
  BonusDisengage; level-1 Rogues now receive a clear rejection message.
- Add Command::BonusHide with parser aliases: bonus hide, hide bonus,
  cunning hide, hide as bonus.
- Add player_hidden: bool to CombatState (#[serde(default)], reset in
  advance_turn). While true: player's next attack has advantage (cleared
  after roll); NPC attacks against player have disadvantage.
- Add BonusHide handler in lib.rs: Stealth check vs DC 15, sets
  player_hidden on success, consumes bonus action regardless.
- Add Command::Hide for exploration: free Stealth check, narration only.
- Update bonus action hint line to list rogue cunning actions at level 2+.
- Add 13 new tests covering level gates, BonusHide mechanics, player_hidden
  advantage/disadvantage, flag reset, and exploration hide.
- Update docs/specs/flee-disengage.md and conditions-system.md.

Closes #337
feat(engine): Rogue Cunning Action — Dash/Disengage/Hide as bonus action at level 2
… next

Pass `&[]` (no expertise skills) to the two `rules::checks::skill_check`
calls in the Hide command handlers (exploration and combat), which were
left with 8 arguments after the function signature was extended to 9 to
support Expertise. Unblocks compilation and all 1430 engine tests pass.

The healing-potion-as-Bonus-Action combat logic was already correctly
implemented (Command::Use and Command::Drink handlers at ~line 6170,
item_is_healing_potion helper, resolve_use_item for both ItemType::Potion
and ItemType::Consumable). Test coverage for the ItemType::Potion path
in combat context also already exists
(test_combat_magic_potion_of_healing_consumes_bonus_action). The dying
guard (HP <= 0 intercept at line 5074) correctly prevents Use from
firing while the character is unconscious, satisfying the SRD 2024
Incapacitated constraint.

Closes #343
… at 0 HP

When a player reaches 0 HP in combat, the engine now:

1. On the first death save tick (both counters at 0), emits an
   introductory message explaining the death saving throw mechanic.
2. Each death save roll shows the result with an updated tally:
   "[Death save: 14 — Success. 1/3 successes, 0/3 failures.]"
3. Acknowledges player input with "You are unconscious and cannot act."
   instead of silently consuming it.
4. Appends a [dying: X/3 successes, Y/3 failures] indicator after each
   save while still dying.

narrate_death_save_outcome() now accepts post-roll tally parameters
(successes, failures) to include the running count in non-terminal
outcome narration.

Closes #342
Adds `skip` as a parser alias for Command::EndTurn alongside the
existing `wait` and `pass` aliases. Updates docs/domain/game-loop.md
to list all three aliases and note that doing nothing on your turn is
a valid SRD 2024 choice. Adds test assertion for `skip` → EndTurn.

Closes #341
Hostile NPCs now emit a narrative transition line ("{name}'s eyes narrow
-- they reach for their weapon.") between the room description and
"=== COMBAT BEGINS ===" when the player enters a location with living
hostile combatants. Only NPCs with Disposition::Hostile and active
combat_stats produce this line; friendly/neutral NPCs are unaffected.

Adds NPC_AGGRO template constant to narration/templates.rs.
Includes a test verifying the aggro line appears before the combat
header and that friendly NPCs do not produce it.

Closes #344
fix(engine): add NPC aggro indicator before combat on room entry
Adds Command::HP variant parsed from "hp" | "health", handled in both
the exploration and combat orchestrators to emit "HP: current/max".
Also adds parser tests for the new aliases. Closes #346.
Implements Command::DropAll in the parser and orchestrator, mirroring
the existing TakeAll pattern. Dropping all inventory items clears
equipment slots, updates item ownership fields, places items on the
room floor, and returns a "You drop: ..." summary line.

Closes #345
feat(engine): add drop all / drop everything bulk item command
Closes #347. Both validate_point_buy() and the CreationStep::PointBuy
handler now produce delta-aware messages:
  underspend → "spent 22/27 points — 5 remaining"
  overspend  → "spent 29/27 points — 2 over budget"

Added test_point_buy_underspend_message and test_point_buy_overspend_message
to assert on exact message content.
The Hermit NPC role template "In the {} of {}" produced "In the the mural
of ..." because the `feature` variable was already built as "the <name>".
Drop the redundant "the" from the template. Audited all four role templates;
only Hermit was affected.

Adds regression test asserting no "the the" appears in any role's dialogue
when a room feature is present.

Closes #348
fix(engine): remove doubled definite article in Hermit dialogue template
…f bonus action

The EndTurn handler gated wait-flavored narration on both
!action_used && !bonus_action_used. When a player used only their
bonus action (e.g. Rage) then ended the turn, the generic "You end
your turn." fired instead of "You wait, letting your turn pass
without acting." because bonus_action_used was true.

Remove the bonus_action_used check -- only the main action flag
determines whether the player "waited." Add regression test covering
the bonus-action-used, main-action-unused case.

Closes #361
…drops to 0

When an NPC attack or opportunity attack drops the player from positive HP
to 0, the combat output now includes "You fall unconscious!" in the same
batch as the killing blow. Previously, only the next-turn explanation
("You are unconscious (0 HP)...") fired, leaving a gap in immediate feedback.

The narration is suppressed when the player was already dying (was_dying == true),
since the existing damage-while-dying handler covers that case.

Closes #362
…tion

fix(engine): emit "You fall unconscious!" when player HP drops to 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant