diff --git a/difficulty.json b/difficulty.json new file mode 100644 index 00000000..20a1729d --- /dev/null +++ b/difficulty.json @@ -0,0 +1,92 @@ +{ + "knob_schema": { + "production_mult": "Scales city build-queue output per turn (food, hammers). Easy=0.70 produces ~34% less total output than Normal baseline; Hard=1.30 produces ~30% more. Primary lever that separates tier progression speed. Empirically validated: Easy median prod_total=26.1 vs Normal 39.5.", + "research_mult": "Scales per-turn tech research progress. Lower values delay tier advancement and unlock timing. Composes multiplicatively with personality axis-driven research priority (p0-37 architecture).", + "gold_mult": "Reserved for future gold income scaling. Currently 1.0 for all tiers — gold differentiation handled via starting_gold_bonus instead.", + "combat_bonus": "Reserved for future combat power modifier (+N to attack/defense rolls). Currently 0 for all tiers — difficulty shapes resource efficiency, not raw combat stats.", + "extra_starting_units": "Hard-handicap: AI spawns N extra units at game start. Only Insane tier uses this (1 warrior). Gives an aggressive opener advantage.", + "extra_unit_id": "Unit type id for the extra_starting_units spawn (e.g. 'warrior'). Ignored when extra_starting_units=0.", + "starting_gold_bonus": "Hard-handicap: AI receives bonus gold at T0 before any production. Hard=75, Insane=150. Allows faster early expansion and unit purchases.", + "difficulty_threshold_mult": "Scales the tactical posture thresholds in mc-ai::tactical::movement (DOMINANCE_FACTOR, retreat_hp_fraction). Easy=0.85 → AI overcommits (lower dominance required to attack); Hard=1.15 → AI waits for real superiority before engaging. Composes onto personality axis-derived thresholds (p0-37) as a multiplicative layer." + }, + "difficulty": [ + { + "level": 1, + "id": "easy", + "name": "Easy", + "description": "AI receives penalties. Ideal for learning the game.", + "ai_modifiers": { + "production_mult": 0.70, + "research_mult": 0.80, + "gold_mult": 1.0, + "combat_bonus": 0, + "extra_starting_units": 0, + "starting_gold_bonus": 0, + "difficulty_threshold_mult": 0.85 + }, + "player_modifiers": { + "production_mult": 1.0, + "research_mult": 1.0 + } + }, + { + "level": 2, + "id": "normal", + "name": "Normal", + "description": "Baseline challenge. No bonuses for either side.", + "ai_modifiers": { + "production_mult": 1.0, + "research_mult": 1.0, + "gold_mult": 1.0, + "combat_bonus": 0, + "extra_starting_units": 0, + "starting_gold_bonus": 0, + "difficulty_threshold_mult": 1.00 + }, + "player_modifiers": { + "production_mult": 1.0, + "research_mult": 1.0 + } + }, + { + "level": 3, + "id": "hard", + "name": "Hard", + "description": "AI receives significant bonuses. A fair challenge for experienced players.", + "ai_modifiers": { + "production_mult": 1.30, + "research_mult": 2.00, + "gold_mult": 1.0, + "combat_bonus": 0, + "extra_starting_units": 0, + "starting_gold_bonus": 75, + "difficulty_threshold_mult": 1.15 + }, + "player_modifiers": { + "production_mult": 1.0, + "research_mult": 1.0 + } + }, + { + "level": 4, + "id": "insane", + "name": "Insane", + "description": "AI receives major bonuses and a free starting warrior. A serious challenge.", + "ai_modifiers": { + "production_mult": 1.50, + "research_mult": 3.00, + "gold_mult": 1.0, + "combat_bonus": 0, + "extra_starting_units": 1, + "extra_unit_id": "warrior", + "starting_gold_bonus": 150, + "difficulty_threshold_mult": 1.25 + }, + "player_modifiers": { + "production_mult": 1.0, + "research_mult": 1.0 + } + } + ], + "default": "normal" +} diff --git a/public/games/age-of-dwarves/data/difficulty.json b/public/games/age-of-dwarves/data/difficulty.json index c54b0409..20a1729d 100644 --- a/public/games/age-of-dwarves/data/difficulty.json +++ b/public/games/age-of-dwarves/data/difficulty.json @@ -55,7 +55,7 @@ "description": "AI receives significant bonuses. A fair challenge for experienced players.", "ai_modifiers": { "production_mult": 1.30, - "research_mult": 1.20, + "research_mult": 2.00, "gold_mult": 1.0, "combat_bonus": 0, "extra_starting_units": 0, @@ -74,7 +74,7 @@ "description": "AI receives major bonuses and a free starting warrior. A serious challenge.", "ai_modifiers": { "production_mult": 1.50, - "research_mult": 1.40, + "research_mult": 3.00, "gold_mult": 1.0, "combat_bonus": 0, "extra_starting_units": 1, diff --git a/src/game/engine/scenes/world_map/world_map_city_actions.gd b/src/game/engine/scenes/world_map/world_map_city_actions.gd index ac3aa5ab..d2cc47de 100644 --- a/src/game/engine/scenes/world_map/world_map_city_actions.gd +++ b/src/game/engine/scenes/world_map/world_map_city_actions.gd @@ -39,15 +39,12 @@ func on_found_city_pressed(selected_unit: RefCounted) -> void: var is_capital: bool = player.cities.is_empty() var city: RefCounted = CityScript.new() city.owner = player.index - ( - city - . found( - city_name, - selected_unit.position.x, - selected_unit.position.y, - is_capital, - GameState.turn_number, - ) + city.found( + city_name, + selected_unit.position.x, + selected_unit.position.y, + is_capital, + GameState.turn_number, ) player.cities.append(city) @@ -80,13 +77,10 @@ func on_build_improvement_pressed(selected_unit: RefCounted) -> void: _improvement_popup.clear() for i: int in range(_pending_improvements_list.size()): var entry: Dictionary = _pending_improvements_list[i] - var label: String = ( - "%s (%d turns)" - % [ - entry.get("name", ""), - entry.get("build_turns", 0), - ] - ) + var label: String = "%s (%d turns)" % [ + entry.get("name", ""), + entry.get("build_turns", 0), + ] _improvement_popup.add_item(label, i) var mouse_pos: Vector2i = DisplayServer.mouse_get_position() @@ -103,23 +97,9 @@ func _on_popup_selected(id: int) -> void: _pending_unit = null return ## p1-26d: delegate to world_map to show the yield-delta preview overlay - ## before the player commits. If no listener is connected (e.g. AI_ARENA), - ## fall back to immediate commit via _improvement_manager. - ## (merged from local 6cd221add + origin 164467b58 — origin's version is a - ## superset adding the AI_ARENA fallback path) - if improvement_preview_requested.get_connections().size() > 0: - improvement_preview_requested.emit(_pending_unit, improvement_id) - _pending_unit = null - return - var player: RefCounted = GameState.get_current_player() - if player == null: - _pending_unit = null - return - var success: bool = _improvement_manager.start_improvement( - _pending_unit, improvement_id, player - ) - if success: - improvement_started.emit(_pending_unit) + ## before the player commits. world_map connects this signal on init; + ## commit_improvement() is called when the player confirms. + improvement_preview_requested.emit(_pending_unit, improvement_id) _pending_unit = null