M7.5: Interact prompt + dialogue overlay
InteractionScreen — CanvasLayer overlay (Layer=50, WhenPaused) that
pauses the tree on open and runs a Core DialogueRunner against
playScreen-owned aggregates (Reputation, Flags, ContentResolver,
QuestEngine). Codex-themed centered panel ~760 px with header
(NPC name, role line "Innkeeper of Millhaven" via
LastIndexOf('.')-split TitleCase, bias profile · disposition tag
from EffectiveDisposition.Breakdown, optional Scent Literacy line
for scent_broker / scent_literacy / master_nose feature holders),
history scrollback (last C.DIALOGUE_HISTORY_LINES, per-speaker
text colour for NPC / PC / Narration), numbered option list
(skill checks prefixed [SKILL DC N]), footer hint. Input: 1-9
top-row + numpad, Enter to dismiss when over, Esc / F to leave.
Godot's Key enum is long-backed for unicode + modifier bits, so
the arithmetic cast through int is explicit.
Effect routing on each ChooseOption: drains
runner.Context.StartQuestRequests into _playScreen.QuestEngine.Start
with a freshly-rebound QuestContext (PlayerCharacter pinned each
call) — quest journal UI is M8 but the engine fires immediately.
runner.Context.ShopRequested raises a "Shop ships with M8" toast
via PlayScreen.Toast and clears the flag so re-entry doesn't loop.
Stub NPCs (no dialogue_id, missing tree, or null content) get the
"(They have nothing to say yet.)" panel + Goodbye button — same
fallback the MonoGame source ships.
PlayScreen interact tick. Tactical-mode _Process now polls
EncounterTrigger.FindInteractCandidate(_actors) and caches the
result in _interactCandidate (cleared at world-map zoom). HUD
appends "[F] Talk to {DisplayName}" when non-null. Edge-detected
F press → AddChild(new InteractionScreen(npc, this)), candidate
cleared immediately so a held F can't stack overlays. M8 will
wire real LOS into the FindInteractCandidate losBlocked callback;
M7.5 ships with the default AlwaysClear.
Added internal accessors PlayScreen now surfaces so the overlay
doesn't poke private state: Reputation, Flags, QuestEngine, World,
WorldSeed, ClockSeconds, PlayerPosition, Content,
BuildQuestContextForDialogue(), Toast(text). All scoped internal —
not part of any public API.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -62,6 +62,13 @@ public partial class PlayScreen : Control
|
||||
private readonly QuestEngine _questEngine = new();
|
||||
private QuestContext? _questCtx;
|
||||
|
||||
// M7.5 — interact candidate cached per tick. Cleared when no
|
||||
// friendly/neutral NPC is in range; the HUD shows "[F] Talk to ..."
|
||||
// while non-null.
|
||||
private NpcActor? _interactCandidate;
|
||||
// Edge-detect F for the talk handler so a held key doesn't fire twice.
|
||||
private bool _fWasDown;
|
||||
|
||||
// M7.3 — save round-trip plumbing
|
||||
private readonly Dictionary<ChunkCoord, HashSet<int>> _killedByChunk = new();
|
||||
// Phase 5 M5: mid-combat encounter snapshot waiting for the CombatHUD
|
||||
@@ -231,6 +238,24 @@ public partial class PlayScreen : Control
|
||||
if (tactical)
|
||||
_streamer.EnsureLoadedAround(p.Position, C.TACTICAL_WINDOW_WORLD_TILES);
|
||||
|
||||
// M7.5 — friendly / neutral interact candidate. Only computed in
|
||||
// tactical mode; world-map scale doesn't surface NPC interactions.
|
||||
if (tactical)
|
||||
_interactCandidate = EncounterTrigger.FindInteractCandidate(_actors);
|
||||
else
|
||||
_interactCandidate = null;
|
||||
|
||||
// F-press → push dialogue overlay. Edge-detect so a held key doesn't
|
||||
// re-open the screen while the previous one is still up.
|
||||
bool fNow = Godot.Input.IsKeyPressed(Key.F);
|
||||
bool fJustDown = fNow && !_fWasDown;
|
||||
_fWasDown = fNow;
|
||||
if (fJustDown && _interactCandidate is not null)
|
||||
{
|
||||
AddChild(new InteractionScreen(_interactCandidate, this));
|
||||
_interactCandidate = null;
|
||||
}
|
||||
|
||||
// Counter-scale markers so on-screen size stays constant.
|
||||
float zoom = _render.Camera.Zoom.X;
|
||||
if (zoom > 0f)
|
||||
@@ -318,6 +343,34 @@ public partial class PlayScreen : Control
|
||||
public Theriapolis.Core.Rules.Character.Character? PlayerCharacter()
|
||||
=> _actors?.Player?.Character;
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// M7.5 accessors — let InteractionScreen build a DialogueContext +
|
||||
// DialogueRunner from PlayScreen-owned aggregates without copying them.
|
||||
|
||||
internal Theriapolis.Core.Rules.Reputation.PlayerReputation Reputation => _reputation;
|
||||
internal Dictionary<string, int> Flags => _flags;
|
||||
internal QuestEngine QuestEngine => _questEngine;
|
||||
internal WorldState World => _ctx.World;
|
||||
internal ulong WorldSeed => _ctx.World.WorldSeed;
|
||||
internal long ClockSeconds => _clock.InGameSeconds;
|
||||
internal Vec2 PlayerPosition => _actors?.Player?.Position ?? new Vec2(0, 0);
|
||||
internal ContentResolver? Content => _content;
|
||||
|
||||
/// <summary>Hand back a quest context wired to current actors/rep/flags
|
||||
/// — used by InteractionScreen to fire start_quest effects after a
|
||||
/// dialogue option resolves.</summary>
|
||||
internal QuestContext? BuildQuestContextForDialogue()
|
||||
{
|
||||
if (_content is null || _questCtx is null) return null;
|
||||
_questCtx.PlayerCharacter = _actors?.Player?.Character;
|
||||
return _questCtx;
|
||||
}
|
||||
|
||||
/// <summary>Surfaced to the toast layer so InteractionScreen can flash
|
||||
/// "Shop ships with M8" without poking PlayScreen's private save-flash
|
||||
/// machinery directly. M8 will swap this for a real ShopScreen push.</summary>
|
||||
public void Toast(string text) => FlashSavedToast(text);
|
||||
|
||||
private Vector2 ScreenToWorld(Vector2 screenPos)
|
||||
=> _render.Camera.GetCanvasTransform().AffineInverse() * screenPos;
|
||||
|
||||
@@ -766,6 +819,10 @@ public partial class PlayScreen : Control
|
||||
? "Mouse-wheel out to leave tactical."
|
||||
: "Mouse-wheel in for tactical.";
|
||||
|
||||
string interactBlock = _interactCandidate is { } npc
|
||||
? $"\n[F] Talk to {npc.DisplayName}"
|
||||
: "";
|
||||
|
||||
_hudLabel.Text =
|
||||
charBlock +
|
||||
$"Seed: 0x{_ctx.World.WorldSeed:X}\n" +
|
||||
@@ -773,7 +830,7 @@ public partial class PlayScreen : Control
|
||||
$"{viewBlock}\n" +
|
||||
$"Time: Day {_clock.Day}, {_clock.Hour:D2}:{_clock.Minute:D2}\n" +
|
||||
$"{status}\n" +
|
||||
"F5 quicksaves · Esc opens pause";
|
||||
"F5 quicksaves · Esc opens pause" + interactBlock;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user