Adv 2.0 D&D Phase R R3-R5: Combat-link, zone loot, fishing, economy

R3 — Combat Event Integration:
- dnd_expedition_combat.go: Combat Interrupt rolls (§4.2) with
  threat-clock and Ranger-wilderness modifiers; Patrol Encounters
  scaled by threat level; recordZoneKill writer with monsterKillTags.
- Interrupt gate at head of handleHarvestCmd; patrol gate before
  resolveRoom in zoneCmdAdvance; kill writer wired into combat-win
  paths.

R4a — Zone Loot Tables:
- dnd_zone_loot.go: §5 loot drop tables for all 10 zones × 5 tiers
  with §8.1 sell-value bands. dropTierFromCR brackets + boss floor.
- Hooks on combat-win in resolveCombatRoom, resolveBossRoom,
  runHarvestInterrupt, tryPatrolEncounter.

R4b — Fishing Integration:
- dnd_zone_fish.go: fishingZones allow-list, fishingSkillBonus,
  rangerRareCatchUpgrade (§6.2), feywildFishDistortion narration.
- §6.1 fish entries added to resource registry for Forest, Sunken
  Temple, Underdark, Feywild zones.
- !fish wired through handleHarvestCmd; zoneItemFlavor matrix
  populated for all 10 zones × all loot items.

R5 — Economy Integration:
- dnd_economy.go: !sell (post-expedition gate, single CHA Persuasion
  DC 17 → +15% bump), !craft (§8.2 4 exemplar recipes), !lore
  (INT/Arcana DC 15 recipe discovery).
- dnd_known_recipe table for persistent recipe discovery.

Flavor reuse: HarvestInterrupt, PatrolEncounter, CombatVictory,
PlayerDeath, LootDrop*, FeywildTimeDistortion* — all existing pools.
No new flavor file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-05-08 18:31:27 -07:00
parent 83cdf07374
commit c170adaf05
13 changed files with 2507 additions and 0 deletions

View File

@@ -358,6 +358,17 @@ func (p *AdventurePlugin) zoneCmdAdvance(ctx MessageContext) error {
prev := run.CurrentRoomType()
prevIdx := run.CurrentRoom
// §4.1 Patrol Encounter: at Threat-Clock Alert+, patrols may move
// through cleared rooms. Roll on `!advance` *before* the next room's
// own resolution. Player KO ends the run.
patrolNarr, patrolEnded, perr := p.tryPatrolEncounter(ctx.Sender, run, zone)
if perr != nil {
return p.SendDM(ctx.Sender, "Couldn't resolve patrol: "+perr.Error())
}
if patrolEnded {
return p.SendDM(ctx.Sender, patrolNarr)
}
// Resolve the current room *before* clearing it, so combat results
// can decide whether the player advances or the run ends.
resolution, ended, err := p.resolveRoom(ctx.Sender, run, zone)
@@ -375,6 +386,10 @@ func (p *AdventurePlugin) zoneCmdAdvance(ctx MessageContext) error {
if next == "" {
_, _ = applyMoodEvent(run.RunID, MoodEventZoneComplete)
var b strings.Builder
if patrolNarr != "" {
b.WriteString(patrolNarr)
b.WriteString("\n\n")
}
if resolution != "" {
b.WriteString(resolution)
b.WriteString("\n\n")
@@ -396,6 +411,10 @@ func (p *AdventurePlugin) zoneCmdAdvance(ctx MessageContext) error {
nextIdx := run.CurrentRoom + 1
var b strings.Builder
if patrolNarr != "" {
b.WriteString(patrolNarr)
b.WriteString("\n\n")
}
if resolution != "" {
b.WriteString(resolution)
b.WriteString("\n\n")
@@ -493,6 +512,11 @@ func (p *AdventurePlugin) resolveCombatRoom(userID id.UserID, run *DungeonRun, z
b.WriteString("\n")
}
b.WriteString(fmt.Sprintf("✅ **%s** down (HP %d→%d).", monster.Name, result.PlayerStartHP, result.PlayerEndHP))
recordZoneKillForUser(userID, monster.ID)
if drop := p.dropZoneLoot(userID, zone.ID, monster, false); drop != "" {
b.WriteString("\n")
b.WriteString(drop)
}
return b.String(), false, nil
}
@@ -532,6 +556,11 @@ func (p *AdventurePlugin) resolveBossRoom(userID id.UserID, run *DungeonRun, zon
b.WriteString("\n")
}
b.WriteString(fmt.Sprintf("🏆 **%s** falls (HP %d→%d).", monster.Name, result.PlayerStartHP, result.PlayerEndHP))
recordZoneKillForUser(userID, monster.ID)
if drop := p.dropZoneLoot(userID, zone.ID, monster, true); drop != "" {
b.WriteString("\n")
b.WriteString(drop)
}
return b.String(), false, nil
}