Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions scripts/main.gd
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const BUILD_UNLOCKS := Constants.BUILD_UNLOCKS
const RotatingGoal := preload("res://scripts/rotating_goal.gd")
const GoalProgression := preload("res://scripts/goal_progression.gd")
const GoalReward := preload("res://scripts/goal_reward.gd")
const MilestoneManager := preload("res://scripts/milestone_manager.gd")
const RESOURCE_TRENDS := Constants.RESOURCE_TRENDS
const ColonyStance := preload("res://scripts/colony_stance.gd")
const TileRender := preload("res://scripts/tile_render.gd")
Expand Down Expand Up @@ -100,6 +101,8 @@ var game_active := false
var active_goal: Dictionary = {}
var completed_goal_ids: Array = []
var active_rewards: Array = []
var current_milestone_id: String = ""
var completed_milestone_ids: Array = []
var event_drawer_visible := false

func make_panel_style(bg: Color, border: Color, corner_radius: int = 12) -> StyleBoxFlat:
Expand Down Expand Up @@ -764,6 +767,10 @@ func bootstrap_state() -> void:
apply_priority_order()
# Initialize active goal
active_goal = GoalProgression.init_goals(completed_goal_ids)
# Initialize milestone state
var milestone_seed: Dictionary = MilestoneManager.make_goal_state()
current_milestone_id = String(milestone_seed.get("milestone_id", ""))
completed_milestone_ids = Array(milestone_seed.get("completed_ids", []))
completed_goal_ids = []
active_rewards = []
_mark_dirty()
Expand Down Expand Up @@ -914,6 +921,10 @@ func load_saved_game() -> void:
completed_goal_ids = state["completed_goal_ids"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Major (bug): Fallback logic in load_saved_game() uses MilestoneManager.make_goal_state() to derive defaults, which is non-deterministic for corrupted/unrecognized milestone IDs in legacy saves.

Automated finding from AI PR review.

if state.has("active_rewards"):
active_rewards = state["active_rewards"]
# Restore milestone state (seed defaults for legacy saves without milestone keys)
var milestone_seed: Dictionary = MilestoneManager.make_goal_state()
current_milestone_id = String(state.get("current_milestone_id", milestone_seed.get("milestone_id", "")))
completed_milestone_ids = Array(state.get("completed_milestone_ids", milestone_seed.get("completed_ids", [])))
colony_stance = String(state.get("colony_stance", ColonyStance.STANCE_BALANCED))
apply_priority_order()
apply_orientation_lock_ui()
Expand Down Expand Up @@ -1398,6 +1409,13 @@ func _on_tick() -> void:
push_event(evt)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 Blocker (bug): Milestone evaluation block is incorrectly nested inside the reward-expired loop, causing it to execute 0–N times per tick instead of exactly once. Dedent by one level to match the surrounding loop's nesting.

Automated finding from AI PR review.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still open after this push; carried forward. (as of 7422627)

for expired_label in reward_result["expired"]:
push_event("Reward ended: %s" % expired_label)
# Milestone evaluation (issue #237)
var active_milestone: Dictionary = MilestoneManager.get_current_milestone(MilestoneManager.MILESTONE_CATALOG, current_milestone_id)
if not active_milestone.is_empty() and MilestoneManager.is_milestone_complete(active_milestone, state):
var completed_milestone_id: String = current_milestone_id
completed_milestone_ids.append(completed_milestone_id)
push_event("Milestone reached: %s" % MilestoneManager.milestone_description(active_milestone))
current_milestone_id = MilestoneManager.advance_to_next(completed_milestone_ids, completed_milestone_id)
persist()
state.workers = state.workers
render_all()
Expand Down Expand Up @@ -2440,6 +2458,9 @@ func persist() -> void:
state["completed_goal_ids"] = completed_goal_ids.duplicate()
# Persist active rewards for goal reward system
state["active_rewards"] = active_rewards.duplicate(true)
# Persist milestone state (issue #237)
state["current_milestone_id"] = current_milestone_id
state["completed_milestone_ids"] = completed_milestone_ids.duplicate()
GameState.save_game(state)

func get_tile(pos: Vector2i) -> Dictionary:
Expand Down
30 changes: 15 additions & 15 deletions scripts/milestone_manager.gd
Original file line number Diff line number Diff line change
Expand Up @@ -80,29 +80,29 @@ static func get_current_milestone(catalog: Array, milestone_id: String) -> Dicti
# Evaluate whether the current milestone is complete given game state.
# Returns {progress: int, total: int} for UI progress display.
static func evaluate_milestone(milestone: Dictionary, game_state: Dictionary) -> Dictionary:
var mtype := String(milestone.get("type", ""))
var target := milestone.get("target", {})
var mtype: String = String(milestone.get("type", ""))
var target: Dictionary = milestone.get("target", {})

match mtype:
MILESTONE_TYPE_BUILD:
var build_kind := String(target.get("build_kind", ""))
var builds := game_state.get("builds", [])
var build_kind: String = String(target.get("build_kind", ""))
var builds: Array = game_state.get("builds", [])
for build in builds:
if bool(build.get("complete")) and String(build.get("kind", "")) == build_kind:
return {"progress": 1, "total": 1}
return {"progress": 0, "total": 1}

MILESTONE_TYPE_STOCKPILE:
var resource := String(target.get("resource", ""))
var amount := int(target.get("amount", 0))
var harvested := game_state.get("harvested", {})
var current := int(harvested.get(resource, 0))
var resource: String = String(target.get("resource", ""))
var amount: int = int(target.get("amount", 0))
var harvested: Dictionary = game_state.get("harvested", {})
var current: int = int(harvested.get(resource, 0))
return {"progress": mini(current, amount), "total": amount}

MILESTONE_TYPE_WORKER:
var count := int(target.get("worker_count", 0))
var workers := game_state.get("workers", [])
var active := 0
var count: int = int(target.get("worker_count", 0))
var workers: Array = game_state.get("workers", [])
var active: int = 0
for worker in workers:
if int(worker.get("break_ticks", 0)) <= 0:
active += 1
Expand All @@ -113,13 +113,13 @@ static func evaluate_milestone(milestone: Dictionary, game_state: Dictionary) ->

# ── Completion check ────────────────────────────────────────────────────────
static func is_milestone_complete(milestone: Dictionary, game_state: Dictionary) -> bool:
var eval := evaluate_milestone(milestone, game_state)
return eval.get("progress", 0) >= eval.get("total", 1)
var eval: Dictionary = evaluate_milestone(milestone, game_state)
return int(eval.get("progress", 0)) >= int(eval.get("total", 1))

# ── Advance to next milestone ───────────────────────────────────────────────
static func advance_to_next(completed_ids: Array, current_id: String) -> String:
# Find the index of the current milestone in the catalog
var current_index := -1
var current_index: int = -1
for i in range(MILESTONE_CATALOG.size()):
if MILESTONE_CATALOG[i]["id"] == current_id:
current_index = i
Expand All @@ -128,7 +128,7 @@ static func advance_to_next(completed_ids: Array, current_id: String) -> String:
if current_index < 0 or current_index >= MILESTONE_CATALOG.size() - 1:
return current_id # No next milestone

var next_index := current_index + 1
var next_index: int = current_index + 1
return MILESTONE_CATALOG[next_index]["id"]

# ── Milestone description for event log ─────────────────────────────────────
Expand Down
Loading