from pathlib import Path

ROOT = Path.cwd()

def replace(path, old, new, count=1):
    p = ROOT / path
    raw = p.read_bytes()
    text = raw.decode('utf-8')
    eol = '\r\n' if '\r\n' in text else '\n'
    normalized = text.replace('\r\n', '\n')
    if normalized.count(old) != count:
        raise ValueError(f'{path}: expected {count} anchors, got {normalized.count(old)}')
    changed = normalized.replace(old, new).replace('\n', eol)
    if p.read_bytes() != raw: raise ValueError('Concurrent edit: ' + path)
    p.write_bytes(changed.encode('utf-8'))

helper = '''extends RefCounted

# The original trigger still owns quest effects. Combat scenes now belong to
# the real opponent in its room; old story checks must verify that link too.
const ALIASES := {"mld_entry_d1": "mld_entry", "mld_entry_d2": "mld_entry",
    "ti_gras_d4_lair": "ti_gras_d3_lair", "ti_gras_d4_fuses": "ti_gras_d3_fuses"}

static func scene_for_trigger(id: String) -> String:
    var trigger: Dictionary = DataCatalog.STORY_TRIGGERS.get(id, {})
    if trigger.is_empty():
        return ""
    var scene := str(trigger.get("scene", ""))
    if not scene.is_empty():
        return scene if DataCatalog.STORY_SCENES.has(scene) else ""
    scene = str(ALIASES.get(id, id))
    if not DataCatalog.STORY_SCENES.has(scene):
        return ""
    var room_filter := ""
    for condition: Dictionary in trigger.get("conditions", []):
        var parts := str(condition.get("path", "")).split(".")
        if parts.size() == 3 and parts[0] == "rooms" and parts[2] == "current" and condition.get("operator", "") == "truthy":
            room_filter = parts[1]
    for room_id: String in DataCatalog.ROOMS:
        if not room_filter.is_empty() and room_filter != room_id:
            continue
        for presentation: Dictionary in DataCatalog.ROOMS[room_id].get("combat_scenes", []):
            for field: String in ["intro_scene", "defeat_scene", "flee_scene"]:
                if presentation.get(field, "") == scene:
                    return scene
    return ""
'''
(ROOT / 'scripts/core/story_check_scenes.gd').write_text(helper.replace('    ', '\t'), encoding='utf-8')
for filename in ['d1_story_check.gd', 'd2_story_check.gd', 'd3_story_check.gd', 'd4_story_check.gd']:
    path = 'scripts/core/' + filename
    replace(path, 'extends Node\n', 'extends Node\n\nconst PRESENTED_SCENES := preload("res://scripts/core/story_check_scenes.gd")\n')

replace('scripts/core/d1_story_check.gd',
    'not trigger.is_empty() and DataCatalog.STORY_SCENES.has(str(trigger.get("scene", "")))',
    'not trigger.is_empty() and not PRESENTED_SCENES.scene_for_trigger(id).is_empty()')
replace('scripts/core/d2_story_check.gd',
    'authored.has(id) and DataCatalog.STORY_SCENES.has(str(authored.get(id, {}).get("scene", "")))',
    'authored.has(id) and not PRESENTED_SCENES.scene_for_trigger(id).is_empty()', 2)
replace('scripts/core/d2_story_check.gd',
    'DataCatalog.STORY_SCENES.has(str(DataCatalog.STORY_TRIGGERS.get(id, {}).get("scene", "")))',
    'not PRESENTED_SCENES.scene_for_trigger(id).is_empty()')
replace('scripts/core/d3_story_check.gd',
    'authored.has(id) and DataCatalog.STORY_SCENES.has(str(authored.get(id, {}).get("scene", "")))',
    'authored.has(id) and not PRESENTED_SCENES.scene_for_trigger(id).is_empty()')
replace('scripts/core/d4_story_check.gd',
    'not trigger.is_empty() and DataCatalog.STORY_SCENES.has(str(trigger.get("scene", "")))',
    'not trigger.is_empty() and not PRESENTED_SCENES.scene_for_trigger(id).is_empty()')
replace('scripts/core/d4_story_check.gd',
    '# Une nouvelle montée : tout repart à zéro, et la scène courte se joue.',
    '# Une nouvelle montée : tout repart à zéro, et le festin complet revient.')
replace('scripts/core/d4_story_check.gd',
    'and GameState.conditions_met(short.get("conditions", [])) and _has_text("roof_feast_short"),\n\t\t"a new climb does not start over with the short scene")',
    'and GameState.conditions_met(short.get("conditions", [])) and str(short.get("scene", "")) == "roof_feast_full"\n\t\tand _has_text("roof_feast_full"),\n\t\t"a new climb does not replay the complete feast")')

# The Bob fixture freezes the dungeon and simulates room hooks. Explicitly
# deliver the real spawn/defeat notifications to its combat director, while
# preserving every quest, movement, gift and dialogue assertion.
path = 'scripts/core/bob_quest_check.gd'
replace(path, '\t_enter_room("d1_l04_abandoned_greenhouse")\n\tawait _play("bob_eltuna_wall")',
    '\t_enter_room("d1_l04_abandoned_greenhouse")\n\t_combat_spawn("eltuna")\n\tawait _play("bob_eltuna_wall")')
replace(path, '\tvar sorry_body := _fallen_bob()\n\tawait _play("bob_sorry")',
    '\tvar sorry_body := _fallen_bob()\n\t_combat_defeat(sorry_body)\n\tawait _play("bob_sorry")')
for room, scene in [('bob_duel_challenge', 'bob_duel_intro'), ('bob_duel_break', 'bob_break_intro')]:
    replace(path, f'\t_enter_room("{room}")\n\tawait _play("{scene}")',
        f'\t_enter_room("{room}")\n\t_combat_spawn("bob_the_horse")\n\tawait _play("{scene}")')
replace(path, '\tvar laughing := _fallen_bob()\n\tawait _play("bob_joins")',
    '\tvar laughing := _fallen_bob()\n\t_combat_defeat(laughing)\n\tawait _play("bob_joins")')
replace(path, '\tvar galloping := [_fallen_bob()]\n\tawait _play("bob_break_over")',
    '\tvar galloping := [_fallen_bob()]\n\t_combat_defeat(galloping[0])\n\tawait _play("bob_break_over")')
replace(path, 'func _finish() -> void:', '''func _combat_spawn(enemy_id: String) -> void:
\tvar dungeon := get_tree().current_scene
\tvar director: Node = dungeon.get("combat_presentation")
\tdirector.reset()
\tdirector.enabled = true
\tvar enemy: Enemy = dungeon._spawn_one(enemy_id, Vector2(800, 400))
\tcheck(enemy != null, "combat fixture did not spawn " + enemy_id)
\tif enemy != null:
\t\tenemy.set_physics_process(false)

func _combat_defeat(body: Enemy) -> void:
\tvar director: Node = get_tree().current_scene.get("combat_presentation")
\tif director._room_key != DiveMap.current_room_id():
\t\tdirector.reset()
\tdirector.enabled = true
\tdirector.enemy_defeated(body)

func _finish() -> void:''')

# Add a durable D4 phase regression before fixing the director itself.
path = 'scripts/core/combat_presentation_check.gd'
replace(path, '\t\tawait _two_real_fights()\n\t\tawait _final_phase()',
    '\t\tawait _two_real_fights()\n\t\tawait _fourth_phase()\n\t\tawait _final_phase()')
replace(path, '\tInput.mouse_mode = Input.MOUSE_MODE_VISIBLE',
    '\tInput.mouse_mode = Input.MOUSE_MODE_VISIBLE\n\tInput.set_custom_mouse_cursor(null)')
replace(path, 'func _final_phase() -> void:', '''func _fourth_phase() -> void:
\tGameState.story_triggers.enabled = false
\tGameState.story_triggers.reset()
\tGameState._reset_progress_state()
\tGameState.clear_announcements()
\tGameState.set_scoped_value("meta", "dimension_index", 3)
\tGameState.set_scoped_value("run", "debug_arrival_room", "d4_l30_wec_lobby")
\tget_tree().change_scene_to_file("res://scenes/dungeon/dungeon.tscn")
\tawait _frames(8)
\tvar dungeon := get_tree().current_scene
\tvar boss: Enemy = null
\tfor enemy: Node in GameState.scoped_nodes(dungeon, "enemy"):
\t\tif enemy is Enemy and enemy.enemy_id == "big_egg_four":
\t\t\tboss = enemy
\tcheck(boss != null, "D4's hall has no Big Egg 4")
\tif boss == null:
\t\treturn
\tdungeon.combat_presentation.enabled = true
\tboss.set_physics_process(false)
\tif DIALOGUE.current != null:
\t\tawait _read_all(dungeon.player)
\t\tawait _frames(3)
\t# Run the actual ordered health-threshold abilities: summon, then flee.
\tboss.health = boss.max_health * 0.4
\tboss._run_ability_triggers("health_below", boss.max_health)
\tvar scenes: Array[String] = []
\tvar stamp := false
\tfor frame: int in 1000:
\t\tif DIALOGUE.current != null:
\t\t\tvar id := str(DIALOGUE.current.scene_id)
\t\t\tif scenes.is_empty() or scenes.back() != id:
\t\t\t\tscenes.append(id)
\t\t\tvar reader: Node = DIALOGUE.current.story
\t\t\tif reader._victory != null and reader._victory.visible:
\t\t\t\tstamp = reader._victory.enemy_id == "big_egg_four" and reader._victory.result == "REPOUSSÉ !" if Loc.language == "fr" else reader._victory.enemy_id == "big_egg_four"
\t\t\treader.continue_scene()
\t\tawait _frames(1)
\t\tif DIALOGUE.current == null and dungeon.combat_presentation.is_idle():
\t\t\tbreak
\tcheck(scenes == ["big_egg_four_slop_machine", "slop_machine_combat_intro"],
\t\t"the machine speaks before Big Egg activates it: %s" % [scenes])
\tcheck(stamp, "Big Egg 4's flight has no repelled announcement")
\tget_tree().change_scene_to_file("res://scenes/main/main_menu.tscn")
\tawait _frames(8)

func _final_phase() -> void:''')
print('Updated story checks and phase regression; director fix still pending.')

replace("scripts/core/room_stamina_check.gd", "\troom.player = hero", "\troom.combat_presentation = room.COMBAT_PRESENTATION.new()\n\troom.combat_presentation.enabled = false\n\troom.add_child(room.combat_presentation)\n\troom.player = hero")
