33from __future__ import annotations
44
55from dataclasses import dataclass , field
6- from typing import Dict , List , Optional
6+ from typing import Dict , List , Optional , Any
77
88try : # pragma: no cover - optional dependency for runtime
99 import yaml # type: ignore
2020
2121if TYPE_CHECKING : # pragma: no cover - only for type hints
2222 from .ui_overlay import UIOverlay
23+ from .locale_manager import LocaleManager
2324
2425
2526@dataclass
@@ -30,6 +31,10 @@ class DialogueOption:
3031 next : Optional [str ] = None
3132 condition : Optional [str ] = None
3233 set_flag : Optional [str ] = None
34+ requires_flag : Optional [str ] = None
35+ clear_flag : Optional [str ] = None
36+ requires_memory : Optional [str ] = None
37+ set_memory : Dict [str , Any ] = field (default_factory = dict )
3338
3439
3540@dataclass
@@ -43,6 +48,10 @@ class DialogueLine:
4348 next : Optional [str ] = None
4449 condition : Optional [str ] = None
4550 set_flag : Optional [str ] = None
51+ requires_flag : Optional [str ] = None
52+ clear_flag : Optional [str ] = None
53+ requires_memory : Optional [str ] = None
54+ set_memory : Dict [str , Any ] = field (default_factory = dict )
4655
4756
4857@dataclass
@@ -58,16 +67,23 @@ class Dialogue:
5867class DialogueEngine :
5968 """Manage dialogue trees and render them through :class:`UIOverlay`."""
6069
61- def __init__ (self , game_state : GameState , ui_overlay : Optional ["UIOverlay" ] = None ) -> None :
70+ def __init__ (
71+ self ,
72+ game_state : GameState ,
73+ ui_overlay : Optional ["UIOverlay" ] = None ,
74+ locale_manager : Optional ["LocaleManager" ] = None ,
75+ ) -> None :
6276 self .game_state = game_state
6377 self .ui_overlay = ui_overlay
78+ self .locale_manager = locale_manager
6479
6580 self .dialogues : Dict [str , Dialogue ] = {}
6681 self .active_dialogue_id : Optional [str ] = None
6782 self .current_line_index : int = 0
6883 self .awaiting_choice : bool = False
6984 self ._option_cache : List [DialogueOption ] = []
70- self ._memory : Dict [str , List [str ]] = {}
85+ self ._memory_history : Dict [str , List [str ]] = {}
86+ self ._memory_store : Dict [str , Dict [str , Any ]] = {}
7187 self ._branch_end : bool = False
7288
7389 # ------------------------------------------------------------------
@@ -123,6 +139,10 @@ def _parse_line(self, item: Dict) -> DialogueLine:
123139 next = opt .get ("next" ),
124140 condition = opt .get ("condition" ),
125141 set_flag = opt .get ("set_flag" ),
142+ requires_flag = opt .get ("requires_flag" ),
143+ clear_flag = opt .get ("clear_flag" ),
144+ requires_memory = opt .get ("requires_memory" ),
145+ set_memory = opt .get ("set_memory" , {}) or {},
126146 )
127147 )
128148 return DialogueLine (
@@ -133,6 +153,10 @@ def _parse_line(self, item: Dict) -> DialogueLine:
133153 next = item .get ("next" ),
134154 condition = item .get ("condition" ),
135155 set_flag = item .get ("set_flag" ),
156+ requires_flag = item .get ("requires_flag" ),
157+ clear_flag = item .get ("clear_flag" ),
158+ requires_memory = item .get ("requires_memory" ),
159+ set_memory = item .get ("set_memory" , {}) or {},
136160 )
137161
138162 # ------------------------------------------------------------------
@@ -154,7 +178,8 @@ def start(self, dialogue_id: str) -> None:
154178 if dlg .memory_flag :
155179 self .game_state .set_flag (dlg .memory_flag , True )
156180
157- self ._memory .setdefault (dialogue_id , [])
181+ self ._memory_history .setdefault (dialogue_id , [])
182+ self ._memory_store .setdefault (dialogue_id , {})
158183
159184 def is_active (self ) -> bool :
160185 return self .active_dialogue_id is not None
@@ -171,6 +196,38 @@ def _current_dialogue(self) -> Optional[Dialogue]:
171196 return self .dialogues .get (self .active_dialogue_id )
172197 return None
173198
199+ def _check_memory (self , expression : Optional [str ]) -> bool :
200+ """Evaluate a simple memory expression."""
201+ if not expression :
202+ return True
203+ expr = expression .strip ()
204+ op = None
205+ if "==" in expr :
206+ op = "=="
207+ elif "!=" in expr :
208+ op = "!="
209+ if op :
210+ left , right = expr .split (op , 1 )
211+ right = right .strip ().strip ("\" '" )
212+ else :
213+ left , right = expr , None
214+ left = left .strip ()
215+ if "." in left :
216+ dlg_id , key = left .split ("." , 1 )
217+ else :
218+ dlg_id , key = self .active_dialogue_id or "" , left
219+ value = self ._memory_store .get (dlg_id , {}).get (key )
220+ if op == "==" :
221+ return str (value ) == right
222+ if op == "!=" :
223+ return str (value ) != right
224+ return value is not None
225+
226+ def _set_memory (self , data : Dict [str , Any ], dialogue_id : Optional [str ] = None ) -> None :
227+ dlg_id = dialogue_id or (self .active_dialogue_id or "" )
228+ store = self ._memory_store .setdefault (dlg_id , {})
229+ store .update ({k : str (v ) for k , v in (data or {}).items ()})
230+
174231 def current_node (self ) -> Optional [DialogueLine ]:
175232 dlg = self ._current_dialogue ()
176233 if not dlg :
@@ -179,9 +236,17 @@ def current_node(self) -> Optional[DialogueLine]:
179236 lines = dlg .lines
180237 while self .current_line_index < len (lines ):
181238 line = lines [self .current_line_index ]
182- if self .game_state .check_condition (line .condition ):
183- return line
184- self .current_line_index += 1
239+ if not self .game_state .check_condition (line .condition ):
240+ self .current_line_index += 1
241+ continue
242+ if line .requires_flag and not self .game_state .get_flag (line .requires_flag ):
243+ self .current_line_index += 1
244+ continue
245+ if not self ._check_memory (line .requires_memory ):
246+ self .current_line_index += 1
247+ continue
248+ return line
249+
185250 return None
186251
187252 # ------------------------------------------------------------------
@@ -217,13 +282,21 @@ def advance(self) -> Optional[DialogueLine]:
217282
218283 if node .options :
219284 self ._option_cache = [
220- opt for opt in node .options if self .game_state .check_condition (opt .condition )
285+ opt
286+ for opt in node .options
287+ if self .game_state .check_condition (opt .condition )
288+ and (not opt .requires_flag or self .game_state .get_flag (opt .requires_flag ))
289+ and self ._check_memory (opt .requires_memory )
221290 ]
222291 self .awaiting_choice = True
223292 return node
224293
225294 if node .set_flag :
226295 self .game_state .set_flag (node .set_flag , True )
296+ if node .clear_flag :
297+ self .game_state .set_flag (node .clear_flag , False )
298+ if node .set_memory :
299+ self ._set_memory (node .set_memory )
227300
228301 if node .next :
229302 return self ._goto (node .next )
@@ -240,7 +313,11 @@ def advance(self) -> Optional[DialogueLine]:
240313 node = self .current_node ()
241314 if node and node .options :
242315 self ._option_cache = [
243- opt for opt in node .options if self .game_state .check_condition (opt .condition )
316+ opt
317+ for opt in node .options
318+ if self .game_state .check_condition (opt .condition )
319+ and (not opt .requires_flag or self .game_state .get_flag (opt .requires_flag ))
320+ and self ._check_memory (opt .requires_memory )
244321 ]
245322 self .awaiting_choice = True
246323 return node
@@ -254,10 +331,14 @@ def choose(self, option_index: int) -> Optional[DialogueLine]:
254331 choice = self ._option_cache [option_index ]
255332 if choice .set_flag :
256333 self .game_state .set_flag (choice .set_flag , True )
334+ if choice .clear_flag :
335+ self .game_state .set_flag (choice .clear_flag , False )
336+ if choice .set_memory :
337+ self ._set_memory (choice .set_memory )
257338
258339 self .awaiting_choice = False
259- mem = self ._memory .setdefault (self .active_dialogue_id or "" , [])
260- mem .append (choice .text )
340+ hist = self ._memory_history .setdefault (self .active_dialogue_id or "" , [])
341+ hist .append (choice .text )
261342
262343 if choice .next :
263344 return self ._goto (choice .next )
@@ -296,12 +377,12 @@ def render(self, surface: "pygame.Surface") -> None: # pragma: no cover - UI on
296377 if not node :
297378 return
298379
299- text = node .text or ""
380+ text = self . resolve_localized_text ( node .text or "" )
300381 speaker = node .speaker
301382 self .ui_overlay .draw_dialogue_box (text , speaker )
302383
303384 if self .awaiting_choice :
304- options_text = [opt .text for opt in self ._option_cache ]
385+ options_text = [self . resolve_localized_text ( opt .text ) for opt in self ._option_cache ]
305386 self .ui_overlay .draw_options (options_text )
306387
307388 # Backwards compatibility -------------------------------------------------
@@ -317,3 +398,26 @@ def handle_input(self, event: "pygame.event.Event") -> None: # pragma: no cover
317398 elif event .key == pygame .K_SPACE :
318399 self .advance ()
319400
401+ # ------------------------------------------------------------------
402+ # Convenience helpers
403+ # ------------------------------------------------------------------
404+ def resolve_localized_text (self , key : str ) -> str :
405+ if self .locale_manager and self .locale_manager .has_translation (key ):
406+ return self .locale_manager .translate (key )
407+ return key
408+
409+ def get_current_line (self ) -> Optional [str ]:
410+ node = self .current_node ()
411+ if not node :
412+ return None
413+ return self .resolve_localized_text (node .text or "" )
414+
415+ def next_line (self , choice_index : Optional [int ] = None ) -> Optional [str ]:
416+ if choice_index is not None :
417+ node = self .choose (choice_index )
418+ else :
419+ node = self .advance ()
420+ if not node :
421+ return None
422+ return self .resolve_localized_text (node .text or "" )
423+
0 commit comments