from pathlib import Path from tower_game.assets import AssetCatalog from tower_game.registry import SKILLS from tower_game.render import ( render_latest_log, render_sidebar, render_stage, render_stage_narration, skill_card_label, ) from tower_game.state import new_game_state from tower_game.ui import CSS, JS, _tooltip_data def test_stage_uses_reserved_huds_and_left_facing_enemy(): state = new_game_state(phase="combat") state["current_floor"] = 1 state["enemy_name"] = "Test Enemy" state["enemy_hp"] = 20 state["enemy_max_hp"] = 20 stage = render_stage(state, AssetCatalog.load()) assert 'class="stage-hud player-stage-hud"' in stage assert 'class="stage-hud enemy-stage-hud"' in stage assert stage.index('class="stage-hud player-stage-hud"') < stage.index('class="combatant-wrap player-wrap"') assert "enemy-sprite faces-left" in stage assert "scaleX(-1)" not in CSS def test_stage_renders_element_specific_attack_effects(): state = new_game_state(phase="combat") state["visual_event"] = { "event_id": 1, "player_animation": "skill", "player_element": "Lightning", "enemy_animation": "idle", "enemy_variant": "", "enemy_element": "Physical", } stage = render_stage(state, AssetCatalog.load()) assert "attack-fx effect-skill element-lightning" in stage for element in ( "physical", "magical", "fire", "water", "ice", "lightning", "earth", "holy", "demonic", ): assert f".element-{element}::before" in CSS def test_sidebar_removes_duplicate_skill_descriptions(): state = new_game_state(phase="combat") state["active_skills"] = [SKILLS["power_strike"].model_dump()] state["cooldowns"]["power_strike"] = 2 sidebar = render_sidebar( state, AssetCatalog.load(), {"power_strike": "18 damage"}, ) assert "sidebar-skill" not in sidebar assert "

Skills

" not in sidebar assert "Equipment" in sidebar def test_skill_cards_include_effect_and_cooldown_without_long_description(): state = new_game_state(phase="combat") skill = SKILLS["power_strike"].model_dump() state["cooldowns"]["power_strike"] = 2 label = skill_card_label(skill, state, "18 damage") assert "Power Strike" in label assert "CD: 2T" in label assert "18 damage" in label assert skill["description"] not in label state["cooldowns"].clear() assert "CD: 1T" in skill_card_label(skill, state, "18 damage") shadow = SKILLS["shadow_step"].model_dump() assert "CD:" not in skill_card_label(shadow, state) assert "CD: 1T" in skill_card_label(skill, state, active=False) def test_sidebar_renders_effective_equipment_stats_and_shared_card_tokens(): state = new_game_state(phase="combat") state["equipment"]["weapon"] = { "id": "strength_blade", "name": "Strength Blade", "slot": "weapon", "rarity": "Rare", "asset_id": "weapon_sword_1", "stat": "strength", "value": 4, "element": "Physical", } sidebar = render_sidebar(state, AssetCatalog.load(), {}) assert "STR" in sidebar assert 'class="stat-bonus">(+4)' in sidebar assert "Max HP" in sidebar assert "Max AP" in sidebar assert "AP Regen" in sidebar assert "--card-bg:#12101a" in CSS assert "--deck-bg:#12101ae8" in CSS assert "--card-border:#30283f" in CSS assert ".action-deck button {" in CSS assert "background:var(--card-bg) !important" in CSS assert "background-image:none !important" in CSS assert "border:1px solid var(--card-border) !important" in CSS assert "border-radius:0 !important" in CSS def test_skill_tooltips_explain_cooldown_and_ap_disabling(): state = new_game_state(phase="combat") state["active_skills"] = [SKILLS["power_strike"].model_dump()] state["cooldowns"]["power_strike"] = 1 tooltip = _tooltip_data(state, {"power_strike": "18 damage"}) assert "Ready in 1 turn." in tooltip state["cooldowns"].clear() state["current_ap"] = 0 tooltip = _tooltip_data(state, {"power_strike": "18 damage"}) assert "Requires 2 AP." in tooltip def test_tooltip_observer_does_not_watch_its_own_attributes(): assert "childList:true" in JS assert "attributes:true" not in JS assert "button.dataset.tip" in JS assert "#button-tooltip-data [data-target-id]" in JS assert "tower-tooltip" in JS assert "requestAnimationFrame" in JS def test_integrated_command_panel_uses_explicit_compact_rows(): source = Path("tower_game/ui.py").read_text(encoding="utf-8") assert ".action-deck {" in CSS assert "position:absolute !important" in CSS assert "grid-template-columns:repeat(2" in CSS assert ".skills-deck > .styler" in CSS assert ".submenu-row" in CSS assert 'elem_classes=["action-deck", "basic-actions-deck"]' in source assert 'elem_classes=["action-deck", "skills-deck"]' in source assert "for row_index in range(3)" in source assert "for column_index in range(2)" in source assert "item_toggle.click" not in source assert "skills_toggle.click" not in source assert "button.click(item_handler" not in source assert "submenu-grid" not in source assert "padding-bottom:clamp" in CSS def test_player_hud_and_results_layer_above_integrated_commands(): assert ".player-stage-hud { left:3%; bottom:51%; }" in CSS assert ".stage-overlay" in CSS assert "z-index:20" in CSS assert "z-index:8" in CSS def test_stage_renders_status_icons_enemy_effects_and_accessible_tooltips(): state = new_game_state(phase="combat") state["player_statuses"] = {"poison": 2, "atkup": 1} state["enemy_statuses"] = {"defup": 1, "stun": 1} state["visual_event"] = { "event_id": 1, "player_animation": "idle", "player_element": "Physical", "enemy_animation": "grunt", "enemy_variant": "triple-claw", "enemy_element": "Physical", } stage = render_stage(state, AssetCatalog.load()) assert "player-status-rack" in stage assert "enemy-status-rack" in stage assert "Poison: Takes damage" in stage assert "Defense Up: Armor is increased" in stage assert "status-duration" in stage assert "enemy-effect-grunt" in stage assert "enemy-variant-triple-claw" in stage for variant in ( "silver-slash", "thrust", "triple-claw", "projectile", "hex-pulse", "heavy-cleave", "ground-shockwave", ): assert f".enemy-variant-{variant}::before" in CSS def test_stage_uses_cached_static_urls_and_small_payload(): state = new_game_state(phase="combat") state["current_floor"] = 1 stage = render_stage(state, AssetCatalog.load()) assert "/gradio_api/file=" in stage assert "data:image" not in stage assert len(stage.encode("utf-8")) < 20_000 def test_normal_turn_narration_timer_is_removed(): source = Path("tower_game/ui.py").read_text(encoding="utf-8") assert "gr.Timer" not in source assert "NARRATION_QUEUE" not in source assert "update_stage=False" in source assert "await asyncio.sleep(beat_delay)" in source assert "3.5" in source and "1.25" in source def test_stage_narration_is_a_separate_lightweight_overlay(): state = new_game_state(phase="combat") state["presentation_line"] = "Player: The blade finds its mark." narration = render_stage_narration(state) assert "stage-narration" in narration assert "The blade finds its mark" in narration assert "battle-frame" not in narration def test_latest_turn_feed_has_heading_and_three_labelled_events(): state = new_game_state(phase="combat") state["active_turn"] = 4 state["combat_events"] = [ {"turn": 3, "actor": "System", "text": "older"}, {"turn": 4, "actor": "Player", "text": "You strike."}, {"turn": 4, "actor": "Enemy", "text": "Enemy strikes."}, {"turn": 4, "actor": "Status", "text": "Poison deals damage."}, ] feed = render_latest_log(state) assert "Turn 4" in feed assert "older" not in feed assert "Player" in feed and "Enemy" in feed and "Status" in feed def test_difficulty_picker_and_percentage_equipment_labels_are_present(): source = Path("tower_game/ui.py").read_text(encoding="utf-8") assert 'label="Run Difficulty"' in source assert '("Easy - forgiving combat and lighter defenses", "easy")' in source assert '("Normal - stronger enemies and bosses", "normal")' in source state = new_game_state(phase="combat") state["equipment"]["armor"] = { "id": "ward", "name": "Ward", "slot": "armor", "rarity": "Rare", "asset_id": "armor_1", "bonuses": [ {"stat": "armor", "value": 5}, {"stat": "evasion", "value": 3}, ], } sidebar = render_sidebar(state, AssetCatalog.load(), {}) assert "+5% Armor" in sidebar assert "+3% Evasion" in sidebar def test_skill_scaling_tooltips_and_combat_options_are_present(): source = Path("tower_game/ui.py").read_text(encoding="utf-8") state = new_game_state(phase="combat") state["active_skills"] = [SKILLS["power_strike"].model_dump()] tooltip = _tooltip_data(state, {"power_strike": "18 damage"}) assert "Scales with STR" in tooltip assert "Cinematic - readable 3.5 second rounds" in source assert "Fast - current 1.25 second rounds" in source assert "Improves Strike and STR-scaling technique damage" in source def test_evolution_hides_raw_dials_until_developer_details(): from tower_game.render import render_evolution state = new_game_state(phase="evolution_reveal") state["pending_evolution"] = { "class_name": "Towerbreaker", "title": "Stone Answers", "narrative": "Repeated force becomes law.", "portrait_id": "player_strength", "signature_skill_id": "power_strike", "signature_skill": None, "dial_changes": [{"dial": "str_scaling_dial", "delta": 0.25}], } rendered = render_evolution(state, AssetCatalog.load()) assert "Strength scaling" in rendered assert "Developer Details" in rendered assert "str_scaling_dial" in rendered