diff --git a/art/backgrounds/puyang_raid_camp.png b/art/backgrounds/puyang_raid_camp.png new file mode 100644 index 0000000..301791a Binary files /dev/null and b/art/backgrounds/puyang_raid_camp.png differ diff --git a/art/backgrounds/qingzhou_village_road.png b/art/backgrounds/qingzhou_village_road.png new file mode 100644 index 0000000..b55a331 Binary files /dev/null and b/art/backgrounds/qingzhou_village_road.png differ diff --git a/art/backgrounds/xingyang_forest_ambush.png b/art/backgrounds/xingyang_forest_ambush.png new file mode 100644 index 0000000..4c5b58c Binary files /dev/null and b/art/backgrounds/xingyang_forest_ambush.png differ diff --git a/art/items/imperial_seal.png b/art/items/imperial_seal.png new file mode 100644 index 0000000..13e3a68 Binary files /dev/null and b/art/items/imperial_seal.png differ diff --git a/art/items/war_drum.png b/art/items/war_drum.png new file mode 100644 index 0000000..95a5be9 Binary files /dev/null and b/art/items/war_drum.png differ diff --git a/data/defs/items.json b/data/defs/items.json index b867a3c..094bfbb 100644 --- a/data/defs/items.json +++ b/data/defs/items.json @@ -97,6 +97,7 @@ "war_drum": { "name": "War Drum", "kind": "accessory", + "icon": "res://art/items/war_drum.png", "accessory_type": "accessory", "bonuses": { "atk": 1, "agi": 1 }, "effects": [], @@ -105,6 +106,7 @@ "imperial_seal": { "name": "Imperial Seal", "kind": "accessory", + "icon": "res://art/items/imperial_seal.png", "accessory_type": "accessory", "bonuses": { "mp": 4, "int": 1 }, "effects": [], diff --git a/data/scenarios/003_xingyang_ambush.json b/data/scenarios/003_xingyang_ambush.json index d40dac1..7a25db7 100644 --- a/data/scenarios/003_xingyang_ambush.json +++ b/data/scenarios/003_xingyang_ambush.json @@ -142,6 +142,7 @@ "map": { "width": 12, "height": 9, + "background": "res://art/backgrounds/xingyang_forest_ambush.png", "terrain": [ "GGGFFRRGGGGG", "GGGFFRRGHHGG", diff --git a/data/scenarios/004_qingzhou_campaign.json b/data/scenarios/004_qingzhou_campaign.json index 0836c50..f0f6d17 100644 --- a/data/scenarios/004_qingzhou_campaign.json +++ b/data/scenarios/004_qingzhou_campaign.json @@ -122,6 +122,7 @@ "map": { "width": 14, "height": 10, + "background": "res://art/backgrounds/qingzhou_village_road.png", "terrain": [ "GGGGGGGGGGGGGG", "GGGFFFGGGHHGGG", diff --git a/data/scenarios/005_puyang_raid.json b/data/scenarios/005_puyang_raid.json index 676a74d..398abb1 100644 --- a/data/scenarios/005_puyang_raid.json +++ b/data/scenarios/005_puyang_raid.json @@ -117,6 +117,7 @@ "map": { "width": 14, "height": 10, + "background": "res://art/backgrounds/puyang_raid_camp.png", "terrain": [ "GGGGGHHGGGGGGG", "GGGFFHHGGGFFGG", diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index b877365..29bd9f8 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -2604,6 +2604,7 @@ func _draw_units() -> void: draw_arc(center, 28, 0.0, TAU, 64, Color(0.03, 0.035, 0.04), 3.0) draw_arc(center, 28, -PI * 0.65, PI * 0.35, 36, team_color.lightened(0.18), 3.0) _draw_unit_identity_marks(rect, center, unit, team_color) + _draw_unit_class_emblem(rect, center, unit, team_color) _draw_unit_class_badge(rect, unit, team_color) var label := _short_name(String(unit["name"])) @@ -2768,24 +2769,94 @@ func _is_generic_enemy_unit(unit: Dictionary) -> bool: func _draw_unit_class_badge(rect: Rect2, unit: Dictionary, team_color: Color) -> void: - var badge_rect := Rect2(rect.position + Vector2(5, 5), Vector2(28, 13)) - draw_rect(badge_rect, Color(0.02, 0.022, 0.026, 0.82)) - draw_rect(badge_rect, team_color.lightened(0.18), false, 1.2) + var badge_rect := Rect2(rect.position + Vector2(4, 4), Vector2(20, 20)) + var class_color := _unit_class_badge_color(unit, team_color) + draw_rect(badge_rect, Color(0.035, 0.018, 0.012, 0.88)) + draw_rect(badge_rect.grow(-2.0), Color(class_color.r, class_color.g, class_color.b, 0.78)) + draw_rect(badge_rect, UI_OLD_BRONZE, false, 1.3) var font := _draw_ui_font(true) - draw_string(font, badge_rect.position + Vector2(1, 11), _unit_class_abbrev(unit), HORIZONTAL_ALIGNMENT_CENTER, badge_rect.size.x - 2, 8, Color.WHITE) + draw_string(font, badge_rect.position + Vector2(0, 15), _unit_class_abbrev(unit), HORIZONTAL_ALIGNMENT_CENTER, badge_rect.size.x, 13, UI_PARCHMENT_TEXT) + + +func _draw_unit_class_emblem(rect: Rect2, center: Vector2, unit: Dictionary, team_color: Color) -> void: + var family := _unit_class_family(unit) + var class_color := _unit_class_badge_color(unit, team_color) + var emblem := class_color.lightened(0.36) + emblem.a = 0.72 + var shadow := Color(0.02, 0.016, 0.012, 0.72) + if family == "cavalry": + var pole_start := center + Vector2(16, -24) + var pole_end := center + Vector2(16, 16) + draw_line(pole_start + Vector2(1, 1), pole_end + Vector2(1, 1), shadow, 2.2) + draw_line(pole_start, pole_end, emblem, 2.0) + var pennant := PackedVector2Array([ + pole_start, + pole_start + Vector2(22, 7), + pole_start + Vector2(5, 16) + ]) + draw_colored_polygon(pennant, Color(class_color.r, class_color.g, class_color.b, 0.50)) + draw_polyline(PackedVector2Array([pennant[0], pennant[1], pennant[2], pennant[0]]), emblem, 1.2) + elif family == "ranged": + var bow_center := center + Vector2(-17, -5) + draw_arc(bow_center + Vector2(1, 1), 17.0, -PI * 0.55, PI * 0.55, 24, shadow, 2.4) + draw_arc(bow_center, 17.0, -PI * 0.55, PI * 0.55, 24, emblem, 2.2) + draw_line(bow_center + Vector2(6, -14), bow_center + Vector2(6, 14), Color(0.92, 0.82, 0.56, 0.58), 1.2) + draw_line(center + Vector2(-14, -5), center + Vector2(8, -5), emblem, 1.6) + elif family == "tactic": + draw_arc(center + Vector2(0, -3), 20.0, 0.0, TAU, 40, Color(emblem.r, emblem.g, emblem.b, 0.42), 1.8) + draw_arc(center + Vector2(0, -3), 12.0, -PI * 0.25, PI * 1.25, 30, emblem, 1.6) + draw_line(center + Vector2(-11, -3), center + Vector2(11, -3), Color(1.0, 0.88, 0.38, 0.42), 1.2) + draw_line(center + Vector2(0, -14), center + Vector2(0, 8), Color(1.0, 0.88, 0.38, 0.32), 1.2) + elif family == "heavy": + draw_line(center + Vector2(-19, -16), center + Vector2(17, 18), shadow, 4.4) + draw_line(center + Vector2(-19, -16), center + Vector2(17, 18), emblem, 3.0) + draw_line(center + Vector2(14, -17), center + Vector2(-16, 16), Color(1.0, 0.82, 0.32, 0.48), 2.4) + draw_circle(center + Vector2(15, 18), 4.5, Color(class_color.r, class_color.g, class_color.b, 0.42)) + else: + var shield := Rect2(center + Vector2(-13, -17), Vector2(26, 28)) + draw_rect(Rect2(shield.position + Vector2(1, 1), shield.size), shadow) + draw_rect(shield, Color(class_color.r, class_color.g, class_color.b, 0.38)) + draw_rect(shield.grow(-4.0), Color(0.82, 0.76, 0.58, 0.28), false, 1.4) + draw_line(shield.position + Vector2(shield.size.x * 0.5, 3), shield.position + Vector2(shield.size.x * 0.5, shield.size.y - 3), emblem, 1.6) func _unit_class_abbrev(unit: Dictionary) -> String: + var family := _unit_class_family(unit) + if family == "cavalry": + return "騎" + if family == "ranged": + return "弓" + if family == "tactic": + return "策" + if family == "heavy": + return "武" + return "步" + + +func _unit_class_family(unit: Dictionary) -> String: var class_id := str(unit.get("class_id", "")) if class_id.contains("cavalry"): - return "CAV" + return "cavalry" if class_id.contains("archer") or class_id.contains("marksman"): - return "ARC" + return "ranged" if class_id.contains("strategist") or class_id.contains("advisor") or class_id.contains("hero") or class_id.contains("commander"): - return "TAC" + return "tactic" if class_id.contains("warrior") or class_id.contains("champion") or class_id.contains("bandit"): - return "WAR" - return "INF" + return "heavy" + return "infantry" + + +func _unit_class_badge_color(unit: Dictionary, team_color: Color) -> Color: + var family := _unit_class_family(unit) + if family == "cavalry": + return Color(0.78, 0.34, 0.16, 1.0) + if family == "ranged": + return Color(0.16, 0.42, 0.26, 1.0) + if family == "tactic": + return Color(0.32, 0.26, 0.62, 1.0) + if family == "heavy": + return Color(0.62, 0.16, 0.12, 1.0) + return team_color.darkened(0.10) func _unit_hp_color(hp_ratio: float) -> Color: diff --git a/tools/smoke_visual_assets.gd b/tools/smoke_visual_assets.gd index 4bb1841..7296d05 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -15,6 +15,21 @@ const SCENARIO_BACKGROUNDS := [ "background_path": "res://art/backgrounds/sishui_gate_pass.png", "label": "002 Sishui Gate background" }, + { + "scenario_path": "res://data/scenarios/003_xingyang_ambush.json", + "background_path": "res://art/backgrounds/xingyang_forest_ambush.png", + "label": "003 Xingyang forest ambush background" + }, + { + "scenario_path": "res://data/scenarios/004_qingzhou_campaign.json", + "background_path": "res://art/backgrounds/qingzhou_village_road.png", + "label": "004 Qingzhou village road background" + }, + { + "scenario_path": "res://data/scenarios/005_puyang_raid.json", + "background_path": "res://art/backgrounds/puyang_raid_camp.png", + "label": "005 Puyang raid camp background" + }, { "scenario_path": "res://data/scenarios/008_wan_castle_escape.json", "background_path": "res://art/backgrounds/wan_castle_escape.png", @@ -2464,7 +2479,7 @@ func _check_battle_visual_data(failures: Array[String]) -> void: ).is_empty(): failures.append("035 old Counterstroke setup flags should not expose doubt rations") - for item_id in ["bronze_sword", "training_spear", "short_bow", "hand_axe", "iron_armor", "panacea"]: + for item_id in ["bronze_sword", "training_spear", "short_bow", "hand_axe", "iron_armor", "war_drum", "imperial_seal", "panacea"]: var item := state.get_item_def(item_id) if item.is_empty(): failures.append("missing expected item: %s" % item_id) @@ -2625,9 +2640,30 @@ func _check_hud_focus_text(failures: Array[String]) -> void: failures.append("Enemy archer HUD focus should describe enemy control: %s" % archer_text) if not scene._unit_current_terrain_text(archer).contains("평지"): failures.append("Archer terrain text should reflect its current plain tile") + _check_unit_class_mark(failures, scene, "cao_cao", "tactic", "策") + _check_unit_class_mark(failures, scene, "xiahou_dun", "cavalry", "騎") + _check_unit_class_mark(failures, scene, "yellow_turban_2", "infantry", "步") + _check_unit_class_mark(failures, scene, "yellow_turban_3", "ranged", "弓") + _check_unit_class_mark(failures, scene, "yellow_turban_1", "heavy", "武") scene.free() +func _check_unit_class_mark(failures: Array[String], scene, unit_id: String, expected_family: String, expected_mark: String) -> void: + var unit: Dictionary = scene.state.get_unit(unit_id) + if unit.is_empty(): + failures.append("missing unit for class mark check: %s" % unit_id) + return + var family := str(scene._unit_class_family(unit)) + var mark := str(scene._unit_class_abbrev(unit)) + if family != expected_family: + failures.append("%s class family should be %s, got %s" % [unit_id, expected_family, family]) + if mark != expected_mark: + failures.append("%s class mark should be %s, got %s" % [unit_id, expected_mark, mark]) + var color: Color = scene._unit_class_badge_color(unit, Color(0.20, 0.42, 0.82)) + if color.a <= 0.0: + failures.append("%s class badge color should be visible" % unit_id) + + func _check_ancient_ui_theme(failures: Array[String]) -> void: var scene = BattleSceneScript.new() scene._create_hud()