release: v0.50.0#1
Open
yanekyuk wants to merge 58 commits into
Open
Conversation
… 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
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
jurnalis-engine v0.50.0
Tests: 1,588 passing
Changelog
Features
buffscommandtake all/drop allbulk item commandswait,pass,skipas end-turn aliases (SRD: doing nothing on your turn is valid)hp/healthcommand alias for compact health displaysleep,camp,napas rest command aliasesFixes