File size: 10,711 Bytes
22a027e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 | 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 "<h3>Skills</h3>" 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)</small>' 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 "<summary>Developer Details</summary>" in rendered
assert "<code>str_scaling_dial</code>" in rendered
|