Mostly done?
This commit is contained in:
parent
22d7326565
commit
a08c13b1a3
118 changed files with 2558 additions and 2519 deletions
101
scripts/animal_friends_manager.gd
Normal file
101
scripts/animal_friends_manager.gd
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
extends Node
|
||||
|
||||
## Manages the visibility of animal friends in the animal_friends group
|
||||
## Animals start invisible and are revealed one at a time randomly as ranks are unlocked
|
||||
|
||||
const ANIMAL_FRIENDS_UNLOCK_ID: int = 6 # Forest Friends unlock
|
||||
|
||||
var all_animals: Array[Node] = []
|
||||
var hidden_animals: Array[Node] = []
|
||||
var revealed_animals: Array[Node] = []
|
||||
var last_known_rank: int = 0
|
||||
|
||||
func _ready() -> void:
|
||||
# Wait a frame to ensure all nodes are ready
|
||||
call_deferred("_initialize")
|
||||
|
||||
func _initialize() -> void:
|
||||
# Get all nodes in the animal_friends group
|
||||
all_animals = get_tree().get_nodes_in_group("animal_friends")
|
||||
|
||||
if all_animals.is_empty():
|
||||
Log.pr("Warning: No animal friends found in the animal_friends group")
|
||||
return
|
||||
|
||||
# Shuffle the animals to randomize the order they'll be revealed
|
||||
all_animals.shuffle()
|
||||
|
||||
# Start with all animals hidden
|
||||
for animal in all_animals:
|
||||
animal.visible = false
|
||||
hidden_animals.append(animal)
|
||||
|
||||
Log.pr("Animal Friends Manager initialized with %d animals" % all_animals.size())
|
||||
|
||||
# Get the Forest Friends unlock to track its rank
|
||||
var forest_friends = Unlocks.get_unlock_by_id(ANIMAL_FRIENDS_UNLOCK_ID)
|
||||
if forest_friends:
|
||||
last_known_rank = forest_friends.current_rank
|
||||
# Reveal animals based on current rank (for game loads)
|
||||
_reveal_animals_for_current_rank()
|
||||
else:
|
||||
Log.pr("Warning: Forest Friends unlock (ID %d) not found" % ANIMAL_FRIENDS_UNLOCK_ID)
|
||||
|
||||
# Connect to unlock signal to detect when new ranks are purchased
|
||||
Unlocks.item_unlocked.connect(_on_item_unlocked)
|
||||
|
||||
func _reveal_animals_for_current_rank() -> void:
|
||||
# Reveal one animal per rank in the Forest Friends unlock
|
||||
var forest_friends = Unlocks.get_unlock_by_id(ANIMAL_FRIENDS_UNLOCK_ID)
|
||||
if not forest_friends:
|
||||
return
|
||||
|
||||
var ranks_to_reveal = forest_friends.current_rank
|
||||
for i in range(ranks_to_reveal):
|
||||
if hidden_animals.is_empty():
|
||||
break
|
||||
_reveal_next_animal()
|
||||
|
||||
func _on_item_unlocked() -> void:
|
||||
# Check if the Forest Friends unlock was ranked up
|
||||
var forest_friends = Unlocks.get_unlock_by_id(ANIMAL_FRIENDS_UNLOCK_ID)
|
||||
if not forest_friends:
|
||||
return
|
||||
|
||||
var current_rank = forest_friends.current_rank
|
||||
var new_ranks = current_rank - last_known_rank
|
||||
|
||||
if new_ranks > 0:
|
||||
Log.pr("Forest Friends ranked up! Revealing %d animal friend(s)" % new_ranks)
|
||||
|
||||
# Reveal one animal for each new rank
|
||||
for i in range(new_ranks):
|
||||
if hidden_animals.is_empty():
|
||||
Log.pr("All animal friends have been revealed!")
|
||||
break
|
||||
_reveal_next_animal()
|
||||
|
||||
last_known_rank = current_rank
|
||||
|
||||
func _reveal_next_animal() -> void:
|
||||
if hidden_animals.is_empty():
|
||||
return
|
||||
|
||||
# Take the first animal from the shuffled hidden list (random order)
|
||||
var animal = hidden_animals.pop_front()
|
||||
animal.visible = true
|
||||
revealed_animals.append(animal)
|
||||
|
||||
Log.pr("Revealed animal friend: %s (%d/%d visible)" % [animal.name, revealed_animals.size(), all_animals.size()])
|
||||
|
||||
## Debug function to manually reveal the next animal
|
||||
func debug_reveal_next() -> void:
|
||||
_reveal_next_animal()
|
||||
|
||||
## Debug function to hide all animals
|
||||
func debug_reset_all() -> void:
|
||||
for animal in all_animals:
|
||||
animal.visible = false
|
||||
hidden_animals = all_animals.duplicate()
|
||||
revealed_animals.clear()
|
||||
Log.pr("All animals hidden")
|
||||
1
scripts/animal_friends_manager.gd.uid
Normal file
1
scripts/animal_friends_manager.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://ewmlbda2adc7
|
||||
|
|
@ -7,16 +7,24 @@ func _ready():
|
|||
play_background_music()
|
||||
|
||||
func play_chop_sound():
|
||||
if not Global.play_chop_sound:
|
||||
return
|
||||
## Pick one of the chopping sounds randomly
|
||||
var chop_sounds = [
|
||||
"res://assets/audio/ogg/SFX/Chopping and Mining/chop 1.ogg",
|
||||
"res://assets/audio/ogg/SFX/Chopping and Mining/chop 2.ogg",
|
||||
"res://assets/audio/ogg/SFX/Chopping and Mining/chop 3.ogg",
|
||||
"res://assets/audio/ogg/SFX/Chopping and Mining/chop 4.ogg"
|
||||
"res://assets/audio/OGG/SFX/chopping/chop1.ogg",
|
||||
"res://assets/audio/OGG/SFX/chopping/chop2.ogg",
|
||||
"res://assets/audio/OGG/SFX/chopping/chop3.ogg",
|
||||
"res://assets/audio/OGG/SFX/chopping/chop4.ogg"
|
||||
]
|
||||
var random_index = randi() % chop_sounds.size()
|
||||
play_sound_effect(chop_sounds[random_index])
|
||||
|
||||
func play_money_sound():
|
||||
if not Global.play_money_sound:
|
||||
return
|
||||
# Using a simple coin sound - you can replace with your own sound file
|
||||
play_sound_effect("res://assets/audio/coin.mp3")
|
||||
|
||||
|
||||
func play_sound_effect(sound_path: String):
|
||||
var sfx_player = AudioStreamPlayer.new()
|
||||
|
|
@ -26,10 +34,22 @@ func play_sound_effect(sound_path: String):
|
|||
sfx_player.play()
|
||||
sfx_player.connect("finished", sfx_player.queue_free)
|
||||
|
||||
var music_player: AudioStreamPlayer = null
|
||||
|
||||
func play_background_music():
|
||||
var music_player = AudioStreamPlayer.new()
|
||||
if not Global.play_background_music:
|
||||
return
|
||||
if music_player:
|
||||
return # Already playing
|
||||
music_player = AudioStreamPlayer.new()
|
||||
music_player.stream = load("res://assets/audio/background_music.ogg")
|
||||
music_player.volume_db = -10 # Set volume to a comfortable level
|
||||
music_player.autoplay = true
|
||||
add_child(music_player)
|
||||
music_player.play()
|
||||
|
||||
func stop_background_music():
|
||||
if music_player:
|
||||
music_player.stop()
|
||||
music_player.queue_free()
|
||||
music_player = null
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ extends Node
|
|||
|
||||
# SETTINGS
|
||||
var play_background_music: bool = true
|
||||
var play_chop_sound: bool = true
|
||||
var play_money_sound: bool = false
|
||||
var game_continue_pressed : bool = false
|
||||
|
||||
# STRINGS
|
||||
|
|
@ -13,7 +15,7 @@ var wood_color: Color = Color(0.95, 0.6, 0.35) # Light pumpkin orange (autumn le
|
|||
var stock_color: Color = Color(0.6, 0.75, 0.95) # Light periwinkle blue (clear autumn sky)
|
||||
|
||||
# GAMEPLAY VALUES
|
||||
var target_currency: float = 1000000
|
||||
var target_currency: float = 100
|
||||
var base_sale_price: float = 30
|
||||
var base_wood_respawn: float = 5 # seconds
|
||||
var wood_per_click: float = 5
|
||||
|
|
@ -30,3 +32,17 @@ var autowood_unlock_id: int = 7
|
|||
|
||||
var premium_crafts_unlock_id: int = 8
|
||||
var reputation_unlock_id: int = 9
|
||||
|
||||
# FORMAT NUMBERS WITH COMMAS FOR DISPLAY
|
||||
func format_number(value: float) -> String:
|
||||
var num_str = str(int(value))
|
||||
var result = ""
|
||||
var length = num_str.length()
|
||||
|
||||
for i in range(length):
|
||||
result += num_str[i]
|
||||
var remaining = length - i - 1
|
||||
if remaining > 0 and remaining % 3 == 0:
|
||||
result += ","
|
||||
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -1,931 +0,0 @@
|
|||
class_name UnlockSimulatorCached
|
||||
extends Control
|
||||
|
||||
# CACHED VERSION - Uses aggressive chunk-based caching for 10-100x speedup
|
||||
|
||||
# Load the actual game resources
|
||||
var unlock_collection: UnlockDataCollection = load("res://resources/UnlockData.tres")
|
||||
var inventory_resource: InventoryResource = load("res://resources/InventoryData.tres")
|
||||
|
||||
# Results tracking
|
||||
var all_results: Array[Dictionary] = []
|
||||
var results_mutex: Mutex = Mutex.new()
|
||||
|
||||
# Chunk-based caching for intermediate simulation states
|
||||
# Key: "unlock_id:rank,..." sorted string
|
||||
# Value: {cache_key, ticks, currency, wood, stock, current_ranks, modifiers}
|
||||
var simulation_cache: Dictionary = {}
|
||||
var cache_mutex: Mutex = Mutex.new()
|
||||
|
||||
# Global unlock struct cache (build once, clone per simulation)
|
||||
var global_unlock_structs: Array[Dictionary] = []
|
||||
var global_unlock_structs_mutex: Mutex = Mutex.new()
|
||||
var unlock_structs_initialized: bool = false
|
||||
|
||||
# Manual thread pool
|
||||
var num_threads: int = 14 # Increase this for more CPU usage
|
||||
var threads: Array[Thread] = []
|
||||
var task_queue: Array[Dictionary] = []
|
||||
var queue_mutex: Mutex = Mutex.new()
|
||||
var completed_count: int = 0
|
||||
var completed_mutex: Mutex = Mutex.new()
|
||||
var active_threads: int = 0
|
||||
var threads_done: bool = false
|
||||
|
||||
# Pre-calculated cost arrays for faster lookup - OPTIMIZATION
|
||||
var cost_cache: Dictionary = {}
|
||||
|
||||
var start_time: int = 0
|
||||
var total_combinations: int = 0
|
||||
var last_progress_time: int = 0
|
||||
var monitoring_active: bool = false
|
||||
var cache_hits: int = 0
|
||||
var cache_misses: int = 0
|
||||
|
||||
# UI References
|
||||
@onready var status_label = $MarginContainer/VBoxContainer/StatusPanel/VBox/StatusLabel
|
||||
@onready var progress_label = $MarginContainer/VBoxContainer/StatusPanel/VBox/ProgressLabel
|
||||
@onready var progress_bar = $MarginContainer/VBoxContainer/StatusPanel/VBox/ProgressBar
|
||||
@onready var rate_label = $MarginContainer/VBoxContainer/StatusPanel/VBox/RateLabel
|
||||
@onready var eta_label = $MarginContainer/VBoxContainer/StatusPanel/VBox/ETALabel
|
||||
@onready var cache_hits_label = $MarginContainer/VBoxContainer/CachePanel/VBox/CacheHitsLabel
|
||||
@onready var cache_misses_label = $MarginContainer/VBoxContainer/CachePanel/VBox/CacheMissesLabel
|
||||
@onready var cache_rate_label = $MarginContainer/VBoxContainer/CachePanel/VBox/CacheRateLabel
|
||||
@onready var cache_size_label = $MarginContainer/VBoxContainer/CachePanel/VBox/CacheSizeLabel
|
||||
@onready var results_label = $MarginContainer/VBoxContainer/ResultsPanel/VBox/ScrollContainer/ResultsLabel
|
||||
|
||||
func _ready():
|
||||
GameManager.tick.stop()
|
||||
print("=== CACHED Unlock Simulator Started ===")
|
||||
print("Using aggressive chunk-based caching for 10-100x speedup")
|
||||
var cpu_count = OS.get_processor_count()
|
||||
print("CPU cores detected: %d" % cpu_count)
|
||||
print("Creating %d worker threads (adjust num_threads variable for more/less)" % num_threads)
|
||||
|
||||
# Update UI
|
||||
status_label.text = "Status: Starting cached simulation..."
|
||||
results_label.text = "[b]CACHED Unlock Simulator Started[/b]\n\nCPU cores: %d\nWorker threads: %d\n\nGenerating combinations..." % [cpu_count, num_threads]
|
||||
|
||||
run_comprehensive_test()
|
||||
|
||||
func _process(_delta):
|
||||
if monitoring_active:
|
||||
# Only update progress once per second
|
||||
var current_time = Time.get_ticks_msec()
|
||||
if current_time - last_progress_time >= 1000:
|
||||
last_progress_time = current_time
|
||||
update_progress()
|
||||
|
||||
func update_progress():
|
||||
"""Update progress display"""
|
||||
var current_count = 0
|
||||
completed_mutex.lock()
|
||||
current_count = completed_count
|
||||
completed_mutex.unlock()
|
||||
|
||||
# Check if all work is complete
|
||||
if current_count >= total_combinations:
|
||||
monitoring_active = false
|
||||
finish_processing()
|
||||
return
|
||||
|
||||
var percent = float(current_count) / total_combinations * 100.0
|
||||
var elapsed = (Time.get_ticks_msec() - start_time) / 1000.0
|
||||
var rate = current_count / elapsed if elapsed > 0 else 0
|
||||
var eta_seconds = (total_combinations - current_count) / rate if rate > 0 else 0
|
||||
|
||||
# Calculate cache hit rate
|
||||
var total_cache_checks = cache_hits + cache_misses
|
||||
var cache_hit_rate = (float(cache_hits) / total_cache_checks * 100.0) if total_cache_checks > 0 else 0.0
|
||||
|
||||
# Format ETA
|
||||
var eta_str = ""
|
||||
if eta_seconds > 0:
|
||||
var eta_minutes = int(eta_seconds) / 60
|
||||
var eta_secs = int(eta_seconds) % 60
|
||||
if eta_minutes > 0:
|
||||
eta_str = "%dm %ds" % [eta_minutes, eta_secs]
|
||||
else:
|
||||
eta_str = "%ds" % eta_secs
|
||||
else:
|
||||
eta_str = "calculating..."
|
||||
|
||||
print("Progress: %.1f%% (%d/%d) - %.1f combos/sec - Cache: %.1f%% hits - ETA: %s" % [
|
||||
percent, current_count, total_combinations, rate, cache_hit_rate, eta_str
|
||||
])
|
||||
|
||||
# Update UI
|
||||
status_label.text = "Status: Running simulation..."
|
||||
progress_label.text = "Progress: %.1f%% (%d/%d)" % [percent, current_count, total_combinations]
|
||||
progress_bar.value = percent / 100.0
|
||||
rate_label.text = "Speed: %.1f combos/sec" % rate
|
||||
eta_label.text = "ETA: %s" % eta_str
|
||||
|
||||
# Update cache stats
|
||||
cache_hits_label.text = "Cache Hits: %d" % cache_hits
|
||||
cache_misses_label.text = "Cache Misses: %d" % cache_misses
|
||||
cache_rate_label.text = "Hit Rate: %.1f%%" % cache_hit_rate
|
||||
cache_mutex.lock()
|
||||
cache_size_label.text = "Cache Entries: %d" % simulation_cache.size()
|
||||
cache_mutex.unlock()
|
||||
|
||||
func worker_thread(thread_id: int):
|
||||
"""Worker thread function that pulls tasks from the queue"""
|
||||
# Local batch storage to reduce mutex contention - OPTIMIZATION
|
||||
var local_results: Array[Dictionary] = []
|
||||
var local_count: int = 0
|
||||
var batch_size: int = 10 # Process 10 before syncing
|
||||
|
||||
while true:
|
||||
# Get next task from queue
|
||||
var task_data = null
|
||||
queue_mutex.lock()
|
||||
if task_queue.size() > 0:
|
||||
task_data = task_queue.pop_front()
|
||||
queue_mutex.unlock()
|
||||
|
||||
# If no more tasks, flush results and exit
|
||||
if task_data == null:
|
||||
if local_results.size() > 0:
|
||||
results_mutex.lock()
|
||||
all_results.append_array(local_results)
|
||||
results_mutex.unlock()
|
||||
|
||||
# CRITICAL FIX: Update completed count when flushing final batch
|
||||
completed_mutex.lock()
|
||||
completed_count += local_results.size()
|
||||
completed_mutex.unlock()
|
||||
break
|
||||
|
||||
# Process the task
|
||||
var result = simulate_rank_combination_pure(task_data.combo, task_data.unlock_data, 1000000)
|
||||
|
||||
# Store in local batch
|
||||
local_results.append(result)
|
||||
local_count += 1
|
||||
|
||||
# Flush batch periodically to reduce mutex contention
|
||||
if local_results.size() >= batch_size:
|
||||
results_mutex.lock()
|
||||
all_results.append_array(local_results)
|
||||
results_mutex.unlock()
|
||||
|
||||
# CRITICAL FIX: Update progress counter with mutex protection
|
||||
completed_mutex.lock()
|
||||
completed_count += local_results.size()
|
||||
completed_mutex.unlock()
|
||||
|
||||
local_results.clear()
|
||||
local_count = 0
|
||||
|
||||
func get_cache_key(current_ranks: Dictionary) -> String:
|
||||
"""Generate a cache key from current unlock ranks"""
|
||||
var sorted_keys = current_ranks.keys()
|
||||
sorted_keys.sort()
|
||||
var key_parts = []
|
||||
for k in sorted_keys:
|
||||
key_parts.append(str(k) + ":" + str(current_ranks[k]))
|
||||
return ",".join(key_parts)
|
||||
|
||||
func try_load_best_prefix_from_cache(rank_targets: Dictionary) -> Variant:
|
||||
"""Balanced cache lookup - fast with good coverage (~10-15 lookups)"""
|
||||
|
||||
cache_mutex.lock()
|
||||
|
||||
# Try exact match first
|
||||
var full_key = get_cache_key(rank_targets)
|
||||
if simulation_cache.has(full_key):
|
||||
cache_hits += 1
|
||||
var result = simulation_cache[full_key]
|
||||
cache_mutex.unlock()
|
||||
return result
|
||||
|
||||
# Sort unlock IDs for consistent ordering
|
||||
var unlock_ids = rank_targets.keys()
|
||||
unlock_ids.sort()
|
||||
var num_unlocks = unlock_ids.size()
|
||||
|
||||
var best_match = null
|
||||
var best_rank_sum = 0
|
||||
|
||||
# STRATEGY: Try progressively shorter prefixes by dropping unlocks from the END
|
||||
# This is the most common pattern: {1,2,3,4,5} → {1,2,3,4} → {1,2,3} → {1,2} → {1}
|
||||
# Covers 80%+ of cache reuse because combinations are generated in sorted order
|
||||
|
||||
for prefix_len in range(num_unlocks - 1, 0, -1):
|
||||
var subset = {}
|
||||
for i in range(prefix_len):
|
||||
subset[unlock_ids[i]] = rank_targets[unlock_ids[i]]
|
||||
|
||||
var key = get_cache_key(subset)
|
||||
if simulation_cache.has(key):
|
||||
var cached_entry = simulation_cache[key]
|
||||
var rank_sum = 0
|
||||
for r in cached_entry.current_ranks.values():
|
||||
rank_sum += r
|
||||
|
||||
# Keep best match (longest prefix)
|
||||
if rank_sum > best_rank_sum:
|
||||
best_match = cached_entry
|
||||
best_rank_sum = rank_sum
|
||||
# Early exit if we found a substantial match
|
||||
if prefix_len >= num_unlocks - 2:
|
||||
break
|
||||
|
||||
if best_match != null:
|
||||
cache_hits += 1
|
||||
else:
|
||||
cache_misses += 1
|
||||
cache_mutex.unlock()
|
||||
|
||||
return best_match
|
||||
|
||||
func should_cache_state(current_ranks: Dictionary, targets_remaining: int) -> bool:
|
||||
"""Decide if this state is worth caching"""
|
||||
# Don't cache if all targets reached
|
||||
if targets_remaining == 0:
|
||||
return false
|
||||
|
||||
# Cache aggressively for early states
|
||||
var total_ranks = 0
|
||||
var active_unlocks = 0
|
||||
|
||||
for rank in current_ranks.values():
|
||||
if rank > 0:
|
||||
total_ranks += rank
|
||||
active_unlocks += 1
|
||||
|
||||
# Cache if: multiple unlocks active OR significant progress on one
|
||||
return (active_unlocks >= 2) or (total_ranks >= 2)
|
||||
|
||||
func simulate_rank_combination_pure(rank_targets: Dictionary, unlock_data_array: Array, max_ticks: int, track_purchases: bool = false) -> Dictionary:
|
||||
"""Optimized pure simulation function with struct-based unlocks"""
|
||||
var currency: float = 0.0
|
||||
var stock: float = 0.0
|
||||
var wood: float = 0.0
|
||||
|
||||
# Purchase tracking (only enabled for top results)
|
||||
var purchases: Array[Dictionary] = []
|
||||
|
||||
# GLOBAL STRUCT CACHE - Build once, clone per simulation
|
||||
# This avoids rebuilding cost/effect tables for every simulation
|
||||
var unlocks: Array[Dictionary] = []
|
||||
var unlock_by_id: Dictionary = {}
|
||||
|
||||
if not unlock_structs_initialized:
|
||||
global_unlock_structs_mutex.lock()
|
||||
if not unlock_structs_initialized: # Double-check pattern
|
||||
for unlock_data in unlock_data_array:
|
||||
var base_mods = unlock_data.base_modifiers
|
||||
|
||||
# Pre-calculate cost table for first 20 ranks (avoid pow() in hot loop)
|
||||
var cost_table: Array[float] = []
|
||||
if unlock_data.is_scaling:
|
||||
# Use cost_ladder if defined, otherwise use exponential scaling
|
||||
if unlock_data.has("cost_ladder") and unlock_data.cost_ladder.size() > 0:
|
||||
# Use fixed cost ladder
|
||||
for cost in unlock_data.cost_ladder:
|
||||
cost_table.append(float(cost))
|
||||
# Fill remaining slots with last cost for safety
|
||||
while cost_table.size() < 21:
|
||||
cost_table.append(cost_table[cost_table.size() - 1])
|
||||
else:
|
||||
# Fallback to exponential scaling
|
||||
var base_cost_float = float(unlock_data.base_cost)
|
||||
var mult = unlock_data.cost_scaling_multiplier
|
||||
for r in range(21): # Pre-calc ranks 0-20
|
||||
cost_table.append(base_cost_float * pow(mult, r))
|
||||
else:
|
||||
cost_table.append(float(unlock_data.base_cost))
|
||||
|
||||
# Pre-calculate effect scale factors for first 20 ranks
|
||||
var effect_scale_table: Array[float] = []
|
||||
var effect_mult = unlock_data.effect_scaling_multiplier
|
||||
for r in range(21):
|
||||
if r == 0:
|
||||
effect_scale_table.append(0.0) # No effect at rank 0
|
||||
elif r == 1:
|
||||
effect_scale_table.append(1.0) # Base effect at rank 1
|
||||
else:
|
||||
effect_scale_table.append(pow(effect_mult, r - 1))
|
||||
|
||||
var unlock_struct = {
|
||||
"id": unlock_data.unlock_id,
|
||||
"name": unlock_data.unlock_name,
|
||||
"base_cost": unlock_data.base_cost,
|
||||
"is_scaling": unlock_data.is_scaling,
|
||||
"max_rank": unlock_data.max_rank,
|
||||
"cost_multiplier": unlock_data.cost_scaling_multiplier,
|
||||
"effect_multiplier": unlock_data.effect_scaling_multiplier,
|
||||
"base_mods": base_mods,
|
||||
"cost_table": cost_table, # Pre-calculated costs
|
||||
"effect_scale_table": effect_scale_table, # Pre-calculated effect scales
|
||||
# Pre-calculate whether this unlock affects each modifier (avoids string lookups)
|
||||
"affects_sale_price": base_mods.has("sale_price_modifier"),
|
||||
"affects_efficiency": base_mods.has("efficiency_modifier"),
|
||||
"affects_wood_per_click": base_mods.has("wood_per_click_modifier"),
|
||||
"affects_purchase_rate": base_mods.has("purchase_rate_modifier"),
|
||||
"affects_autowood": base_mods.has("autowood_modifier"),
|
||||
"is_multicraft": base_mods.has("multicraft_increase_modifier"),
|
||||
# Cache base modifier values to avoid dictionary lookups
|
||||
"sale_price_value": base_mods.get("sale_price_modifier", 1.0),
|
||||
"efficiency_value": base_mods.get("efficiency_modifier", 1.0),
|
||||
"wood_per_click_value": base_mods.get("wood_per_click_modifier", 1.0),
|
||||
"purchase_rate_value": base_mods.get("purchase_rate_modifier", 1.0),
|
||||
"autowood_value": base_mods.get("autowood_modifier", 0.0)
|
||||
}
|
||||
global_unlock_structs.append(unlock_struct)
|
||||
|
||||
unlock_structs_initialized = true
|
||||
global_unlock_structs_mutex.unlock()
|
||||
|
||||
# Clone structs for this simulation (fast shallow copy)
|
||||
for template in global_unlock_structs:
|
||||
var unlock = template.duplicate(false) # Shallow copy
|
||||
unlock.current_rank = 0 # Reset rank for this simulation
|
||||
unlocks.append(unlock)
|
||||
unlock_by_id[unlock.id] = unlock
|
||||
|
||||
var ticks: int = 0
|
||||
# Removed purchases array - it's only needed for debug output and slows down simulation
|
||||
|
||||
# Track how many targets still need to be reached - OPTIMIZATION
|
||||
var targets_remaining: int = 0
|
||||
var current_ranks: Dictionary = {}
|
||||
var active_unlock_ids: Array = [] # Only check unlocks that haven't reached target yet
|
||||
for unlock_id in rank_targets.keys():
|
||||
current_ranks[unlock_id] = 0
|
||||
targets_remaining += rank_targets[unlock_id]
|
||||
active_unlock_ids.append(unlock_id)
|
||||
|
||||
# Modifiers as individual variables for faster access - MAJOR OPTIMIZATION
|
||||
var sale_price_mod: float = 1.0
|
||||
var efficiency_mod: float = 1.0
|
||||
var wood_per_click_mod: float = 1.0
|
||||
var purchase_rate_mod: float = 1.0
|
||||
var autowood_mod: float = 0.0
|
||||
var multicraft_rank: int = 0
|
||||
|
||||
var wholesale_unlocked: bool = false
|
||||
|
||||
# Pre-calculate constants
|
||||
var wood_per_click_base: float = Global.wood_per_click
|
||||
var cost_per_whittle: float = Global.cost_per_whittle
|
||||
var base_sale_price: float = Global.base_sale_price
|
||||
var base_purchase_rate: float = Global.base_purchase_rate
|
||||
var wholesale_id: int = Global.wholesale_unlock_id
|
||||
var wholesale_size: float = Global.wholesale_bundle_size
|
||||
var wholesale_mult: float = Global.wholesale_discount_multiplier
|
||||
|
||||
# CACHE LOOKUP: Try to load from cached intermediate state
|
||||
# NOTE: Disable cache when tracking purchases to ensure all purchases are recorded
|
||||
var cached_state = null
|
||||
if not track_purchases:
|
||||
cached_state = try_load_best_prefix_from_cache(rank_targets)
|
||||
|
||||
if cached_state != null:
|
||||
# Restore full state from cache
|
||||
ticks = cached_state.ticks
|
||||
currency = cached_state.currency
|
||||
stock = cached_state.stock
|
||||
wood = cached_state.wood
|
||||
|
||||
# Restore modifiers
|
||||
sale_price_mod = cached_state.modifiers.sale_price_mod
|
||||
efficiency_mod = cached_state.modifiers.efficiency_mod
|
||||
wood_per_click_mod = cached_state.modifiers.wood_per_click_mod
|
||||
purchase_rate_mod = cached_state.modifiers.purchase_rate_mod
|
||||
autowood_mod = cached_state.modifiers.autowood_mod
|
||||
multicraft_rank = cached_state.modifiers.multicraft_rank
|
||||
wholesale_unlocked = cached_state.modifiers.wholesale_unlocked
|
||||
|
||||
# Restore unlock ranks
|
||||
for unlock_id in cached_state.current_ranks.keys():
|
||||
if unlock_by_id.has(unlock_id):
|
||||
unlock_by_id[unlock_id].current_rank = cached_state.current_ranks[unlock_id]
|
||||
current_ranks[unlock_id] = cached_state.current_ranks[unlock_id]
|
||||
|
||||
# Recalculate targets_remaining and active_unlock_ids
|
||||
targets_remaining = 0
|
||||
active_unlock_ids.clear()
|
||||
for unlock_id in rank_targets.keys():
|
||||
if not current_ranks.has(unlock_id):
|
||||
current_ranks[unlock_id] = 0
|
||||
var remaining = rank_targets[unlock_id] - current_ranks[unlock_id]
|
||||
if remaining > 0:
|
||||
targets_remaining += remaining
|
||||
active_unlock_ids.append(unlock_id)
|
||||
|
||||
# PRE-CALCULATE all next costs to avoid repeated lookups in main loop
|
||||
var next_costs: Array[float] = []
|
||||
next_costs.resize(active_unlock_ids.size())
|
||||
|
||||
for i in range(active_unlock_ids.size()):
|
||||
var unlock = unlock_by_id[active_unlock_ids[i]]
|
||||
var current_rank: int = unlock.current_rank
|
||||
if current_rank < unlock.cost_table.size():
|
||||
next_costs[i] = unlock.cost_table[current_rank]
|
||||
else:
|
||||
next_costs[i] = unlock.base_cost * pow(unlock.cost_multiplier, current_rank)
|
||||
|
||||
while ticks < max_ticks:
|
||||
# Find cheapest affordable unlock using pre-calculated costs
|
||||
var cheapest_unlock_id: int = -1
|
||||
var cheapest_cost: float = INF
|
||||
var cheapest_unlock = null
|
||||
var cheapest_index: int = -1
|
||||
|
||||
if targets_remaining > 0:
|
||||
for i in range(active_unlock_ids.size()):
|
||||
if next_costs[i] < cheapest_cost and currency >= next_costs[i]:
|
||||
cheapest_cost = next_costs[i]
|
||||
cheapest_unlock_id = active_unlock_ids[i]
|
||||
cheapest_unlock = unlock_by_id[cheapest_unlock_id]
|
||||
cheapest_index = i
|
||||
|
||||
# If we can't afford anything and all targets are met, skip to earning 1M
|
||||
if cheapest_unlock == null and targets_remaining == 0:
|
||||
if currency >= 1000000.0:
|
||||
break
|
||||
# Skip ahead: calculate ticks needed to reach 1M currency
|
||||
# Use current production rate to estimate
|
||||
var currency_needed = 1000000.0 - currency
|
||||
var price_per_item = base_sale_price * sale_price_mod
|
||||
var items_per_tick = max(1.0, floor(base_purchase_rate * purchase_rate_mod))
|
||||
var revenue_per_tick = items_per_tick * price_per_item
|
||||
|
||||
if revenue_per_tick > 0:
|
||||
var ticks_needed = int(ceil(currency_needed / revenue_per_tick))
|
||||
ticks += ticks_needed
|
||||
currency += revenue_per_tick * ticks_needed
|
||||
break
|
||||
|
||||
# Purchase the cheapest unlock if found
|
||||
if cheapest_unlock != null:
|
||||
currency -= cheapest_cost
|
||||
cheapest_unlock.current_rank += 1
|
||||
current_ranks[cheapest_unlock_id] += 1
|
||||
targets_remaining -= 1
|
||||
|
||||
# Update wholesale cache
|
||||
if cheapest_unlock_id == wholesale_id:
|
||||
wholesale_unlocked = true
|
||||
|
||||
# OPTIMIZED modifier update - use ratio instead of recalculating from scratch
|
||||
var rank: int = cheapest_unlock.current_rank
|
||||
var prev_rank: int = rank - 1
|
||||
|
||||
if cheapest_unlock.is_multicraft:
|
||||
multicraft_rank = rank
|
||||
|
||||
# Get scale factors from pre-calculated tables
|
||||
var old_scale: float = cheapest_unlock.effect_scale_table[prev_rank] if prev_rank < cheapest_unlock.effect_scale_table.size() else 0.0
|
||||
var new_scale: float = cheapest_unlock.effect_scale_table[rank] if rank < cheapest_unlock.effect_scale_table.size() else pow(cheapest_unlock.effect_multiplier, rank - 1)
|
||||
|
||||
# Apply incremental changes using ratio
|
||||
if cheapest_unlock.affects_sale_price:
|
||||
var base_bonus: float = cheapest_unlock.sale_price_value - 1.0
|
||||
var old_mult: float = 1.0 + base_bonus * old_scale
|
||||
var new_mult: float = 1.0 + base_bonus * new_scale
|
||||
sale_price_mod = sale_price_mod * (new_mult / old_mult)
|
||||
|
||||
if cheapest_unlock.affects_efficiency:
|
||||
var base_bonus: float = cheapest_unlock.efficiency_value - 1.0
|
||||
var old_mult: float = 1.0 + base_bonus * old_scale
|
||||
var new_mult: float = 1.0 + base_bonus * new_scale
|
||||
efficiency_mod = efficiency_mod * (new_mult / old_mult)
|
||||
|
||||
if cheapest_unlock.affects_wood_per_click:
|
||||
var base_bonus: float = cheapest_unlock.wood_per_click_value - 1.0
|
||||
var old_mult: float = 1.0 + base_bonus * old_scale
|
||||
var new_mult: float = 1.0 + base_bonus * new_scale
|
||||
wood_per_click_mod = wood_per_click_mod * (new_mult / old_mult)
|
||||
|
||||
if cheapest_unlock.affects_purchase_rate:
|
||||
var base_bonus: float = cheapest_unlock.purchase_rate_value - 1.0
|
||||
var old_mult: float = 1.0 + base_bonus * old_scale
|
||||
var new_mult: float = 1.0 + base_bonus * new_scale
|
||||
purchase_rate_mod = purchase_rate_mod * (new_mult / old_mult)
|
||||
|
||||
if cheapest_unlock.affects_autowood:
|
||||
autowood_mod = autowood_mod - cheapest_unlock.autowood_value * prev_rank + cheapest_unlock.autowood_value * rank
|
||||
|
||||
# Track purchase if enabled
|
||||
if track_purchases:
|
||||
if purchases.size() == 0:
|
||||
print("DEBUG: First purchase being tracked!")
|
||||
purchases.append({
|
||||
"unlock_id": cheapest_unlock_id,
|
||||
"unlock_name": cheapest_unlock.name,
|
||||
"rank": rank,
|
||||
"cost": cheapest_cost,
|
||||
"tick": ticks,
|
||||
"currency_after": currency
|
||||
})
|
||||
|
||||
# Update next cost for this unlock or remove from active list
|
||||
if current_ranks[cheapest_unlock_id] >= rank_targets[cheapest_unlock_id]:
|
||||
# Target reached - swap with last element and shrink array
|
||||
var last_idx = active_unlock_ids.size() - 1
|
||||
if cheapest_index != last_idx:
|
||||
active_unlock_ids[cheapest_index] = active_unlock_ids[last_idx]
|
||||
next_costs[cheapest_index] = next_costs[last_idx]
|
||||
active_unlock_ids.resize(last_idx)
|
||||
next_costs.resize(last_idx)
|
||||
else:
|
||||
# Update cost for next rank
|
||||
var new_rank = cheapest_unlock.current_rank
|
||||
if new_rank < cheapest_unlock.cost_table.size():
|
||||
next_costs[cheapest_index] = cheapest_unlock.cost_table[new_rank]
|
||||
else:
|
||||
next_costs[cheapest_index] = cheapest_unlock.base_cost * pow(cheapest_unlock.cost_multiplier, new_rank)
|
||||
|
||||
# Removed purchase tracking for performance - only track in debug mode if needed
|
||||
# Don't append to purchases array on every purchase
|
||||
|
||||
# CACHE INSERTION: Cache this state if valuable
|
||||
if should_cache_state(current_ranks, targets_remaining):
|
||||
var cache_key = get_cache_key(current_ranks)
|
||||
|
||||
cache_mutex.lock()
|
||||
if not simulation_cache.has(cache_key):
|
||||
simulation_cache[cache_key] = {
|
||||
"cache_key": cache_key,
|
||||
"ticks": ticks,
|
||||
"currency": currency,
|
||||
"stock": stock,
|
||||
"wood": wood,
|
||||
"current_ranks": current_ranks.duplicate(),
|
||||
"modifiers": {
|
||||
"sale_price_mod": sale_price_mod,
|
||||
"efficiency_mod": efficiency_mod,
|
||||
"wood_per_click_mod": wood_per_click_mod,
|
||||
"purchase_rate_mod": purchase_rate_mod,
|
||||
"autowood_mod": autowood_mod,
|
||||
"multicraft_rank": multicraft_rank,
|
||||
"wholesale_unlocked": wholesale_unlocked
|
||||
}
|
||||
}
|
||||
cache_mutex.unlock()
|
||||
|
||||
# Simulate one tick - HEAVILY OPTIMIZED
|
||||
|
||||
# 1. Generate wood
|
||||
var wood_per_click_modified = wood_per_click_base * wood_per_click_mod
|
||||
|
||||
# Manual clicks based on tick range (pre-calculate to avoid repeated conditions)
|
||||
var manual_clicks: float = 1.0 if ticks < 120 else (0.5 if ticks < 300 else (0.25 if (ticks < 600 and autowood_mod < 0.2) else 0.0))
|
||||
|
||||
# Total wood generation
|
||||
var wood_gen: float = manual_clicks * wood_per_click_modified
|
||||
if autowood_mod > 0.0:
|
||||
wood_gen += max(wood_per_click_modified * autowood_mod, 1.0)
|
||||
wood += wood_gen
|
||||
|
||||
# 2. Whittle wood into stock - MATCHES tick_process.gd:19-32
|
||||
# Base whittling action (always happens once)
|
||||
var multicraft_actions = 1 + multicraft_rank # 1 base + multicraft ranks
|
||||
|
||||
# Each whittle action: items_produced_per_tick = cost_per_whittle * efficiency_modifier
|
||||
var items_per_whittle = cost_per_whittle * efficiency_mod
|
||||
|
||||
for action in range(multicraft_actions):
|
||||
if wood >= 1: # Need at least 1 wood to whittle
|
||||
# How much wood needed for this whittle (matches tick_process.gd:63-65)
|
||||
var wood_needed = ceil(items_per_whittle)
|
||||
var wood_to_use = min(wood, wood_needed)
|
||||
var items_produced = wood_to_use # 1 wood = 1 item always
|
||||
|
||||
wood -= wood_to_use
|
||||
stock += items_produced
|
||||
else:
|
||||
break # Not enough wood for more whittle actions
|
||||
|
||||
# 3. Sell stock for currency - MATCHES tick_process.gd:34-58
|
||||
var price_per_item = base_sale_price * sale_price_mod
|
||||
|
||||
# 3a. Wholesale selling (if unlocked) - matches tick_process.gd:36-42
|
||||
# Sell ALL possible 100-item bundles at 1.2x price
|
||||
if wholesale_unlocked:
|
||||
while stock >= wholesale_size:
|
||||
stock -= wholesale_size
|
||||
currency += wholesale_size * price_per_item * wholesale_mult
|
||||
|
||||
# 3b. Regular selling - matches tick_process.gd:45-58
|
||||
if stock > 0:
|
||||
var purchase_rate = base_purchase_rate * purchase_rate_mod
|
||||
var max_stock_to_sell = floor(purchase_rate)
|
||||
# Always sell at least 1, up to the max
|
||||
var actual_stock_to_sell = min(stock, max(1.0, max_stock_to_sell))
|
||||
stock -= actual_stock_to_sell
|
||||
currency += actual_stock_to_sell * price_per_item
|
||||
|
||||
ticks += 1
|
||||
|
||||
var success = currency >= 1000000.0
|
||||
|
||||
var result = {
|
||||
"rank_targets": rank_targets,
|
||||
"success": success,
|
||||
"ticks": ticks if success else -1,
|
||||
"final_currency": currency,
|
||||
"time_formatted": format_time(ticks) if success else "Failed"
|
||||
}
|
||||
|
||||
# Include purchase timeline if tracking was enabled
|
||||
if track_purchases:
|
||||
result["purchases"] = purchases
|
||||
print("DEBUG: track_purchases=true, purchases.size()=%d" % purchases.size())
|
||||
|
||||
return result
|
||||
|
||||
|
||||
func format_time(ticks: int) -> String:
|
||||
var seconds = ticks
|
||||
var minutes = seconds / 60
|
||||
var hours = minutes / 60
|
||||
|
||||
if hours > 0:
|
||||
return "%dh %dm %ds" % [hours, minutes % 60, seconds % 60]
|
||||
elif minutes > 0:
|
||||
return "%dm %ds" % [minutes, seconds % 60]
|
||||
else:
|
||||
return "%ds" % seconds
|
||||
|
||||
func generate_all_combinations(unlimited_scaling_cap: int = 5) -> Array[Dictionary]:
|
||||
"""Generate combinations for ALL unlocks dynamically, respecting max_ranks from resource file
|
||||
|
||||
Args:
|
||||
unlimited_scaling_cap: Maximum rank to test for unlocks with unlimited scaling (default: 5)
|
||||
Lower values = faster testing, higher = more comprehensive
|
||||
Cap=3: ~13K combos (~28 sec) | Cap=5: ~47K combos (~93 sec)
|
||||
Cap=7: ~111K combos (~3.7min) | Cap=10: ~287K combos (~9.6min)
|
||||
"""
|
||||
var combinations: Array[Dictionary] = []
|
||||
|
||||
# Build constraint list from resource file
|
||||
var unlock_constraints = []
|
||||
for unlock in unlock_collection.unlocks:
|
||||
var max_rank: int
|
||||
if unlock.max_rank > 0:
|
||||
max_rank = unlock.max_rank
|
||||
elif not unlock.is_scaling:
|
||||
max_rank = 1 # One-shot unlocks
|
||||
else:
|
||||
max_rank = unlimited_scaling_cap # Configurable cap for unlimited scaling
|
||||
|
||||
unlock_constraints.append({
|
||||
"id": unlock.unlock_id,
|
||||
"name": unlock.unlock_name,
|
||||
"max_rank": max_rank
|
||||
})
|
||||
|
||||
print("\n=== Generating Combinations ===")
|
||||
print("Reading from resource file: %d unlocks" % unlock_constraints.size())
|
||||
for c in unlock_constraints:
|
||||
print(" - %s (ID %d): 0-%d ranks" % [c.name, c.id, c.max_rank])
|
||||
|
||||
# Recursive generation
|
||||
_generate_combinations_recursive(unlock_constraints, 0, {}, combinations)
|
||||
|
||||
print("Generated %d total combinations" % combinations.size())
|
||||
return combinations
|
||||
|
||||
func _generate_combinations_recursive(constraints: Array, index: int, current: Dictionary, output: Array):
|
||||
"""Recursively generate all valid combinations"""
|
||||
if index >= constraints.size():
|
||||
# Skip all-zeros combination
|
||||
if current.size() > 0:
|
||||
output.append(current.duplicate())
|
||||
return
|
||||
|
||||
var constraint = constraints[index]
|
||||
for rank in range(constraint.max_rank + 1):
|
||||
if rank > 0:
|
||||
current[constraint.id] = rank
|
||||
|
||||
_generate_combinations_recursive(constraints, index + 1, current, output)
|
||||
|
||||
if rank > 0:
|
||||
current.erase(constraint.id)
|
||||
|
||||
func serialize_unlock_data() -> Array:
|
||||
"""Convert unlock collection to serializable data for threads"""
|
||||
var unlock_data = []
|
||||
for unlock in unlock_collection.unlocks:
|
||||
unlock_data.append({
|
||||
"unlock_id": unlock.unlock_id,
|
||||
"unlock_name": unlock.unlock_name,
|
||||
"base_cost": unlock.base_cost,
|
||||
"is_scaling": unlock.is_scaling,
|
||||
"max_rank": unlock.max_rank,
|
||||
"cost_scaling_multiplier": unlock.cost_scaling_multiplier,
|
||||
"effect_scaling_multiplier": unlock.effect_scaling_multiplier,
|
||||
"cost_ladder": unlock.cost_ladder.duplicate() if unlock.cost_ladder.size() > 0 else [],
|
||||
"base_modifiers": unlock.base_modifiers.duplicate()
|
||||
})
|
||||
return unlock_data
|
||||
|
||||
func run_comprehensive_test():
|
||||
"""Test all combinations dynamically generated from resource file"""
|
||||
print("\n=== Available Unlocks ===")
|
||||
for unlock in unlock_collection.unlocks:
|
||||
var max_rank_str = str(unlock.max_rank) if unlock.max_rank > 0 else "unlimited"
|
||||
print("ID: %d | %s | Base Cost: %d | Scaling: %s | Max Rank: %s" % [
|
||||
unlock.unlock_id,
|
||||
unlock.unlock_name,
|
||||
unlock.base_cost,
|
||||
"Yes" if unlock.is_scaling else "No",
|
||||
max_rank_str
|
||||
])
|
||||
print(" Modifiers: ", unlock.base_modifiers)
|
||||
|
||||
print("\n=== Global Constants ===")
|
||||
print("Base Sale Price: %s" % Global.base_sale_price)
|
||||
print("Base Purchase Rate: %s" % Global.base_purchase_rate)
|
||||
print("Cost Per Whittle: %s" % Global.cost_per_whittle)
|
||||
|
||||
# Serialize unlock data for threads
|
||||
var unlock_data = serialize_unlock_data()
|
||||
|
||||
# CACHE WARMUP: Pre-populate cache with common single-unlock states
|
||||
print("\n=== Cache Warmup ===")
|
||||
print("Pre-populating cache with common prefixes...")
|
||||
for unlock in unlock_collection.unlocks:
|
||||
var max_warmup_rank = 3
|
||||
if unlock.max_rank > 0:
|
||||
max_warmup_rank = min(unlock.max_rank, 3)
|
||||
|
||||
for rank in range(1, max_warmup_rank + 1):
|
||||
var warmup_target = {unlock.unlock_id: rank}
|
||||
simulate_rank_combination_pure(warmup_target, unlock_data, 1000000)
|
||||
|
||||
print("Cache warmup complete. Cache size: %d entries" % simulation_cache.size())
|
||||
|
||||
# Generate all combinations (configurable cap for unlimited scaling)
|
||||
var unlimited_cap = 5 # Adjust this to test more/fewer ranks: 3=fast, 5=balanced, 7+=comprehensive
|
||||
print("\n=== Generation Settings ===")
|
||||
print("Unlimited scaling cap: %d ranks" % unlimited_cap)
|
||||
var combinations = generate_all_combinations(unlimited_cap)
|
||||
total_combinations = combinations.size()
|
||||
print("\n=== Testing %d Combinations ===" % total_combinations)
|
||||
|
||||
# Fill task queue
|
||||
task_queue.clear()
|
||||
for combo in combinations:
|
||||
task_queue.append({
|
||||
"combo": combo,
|
||||
"unlock_data": unlock_data
|
||||
})
|
||||
|
||||
# Reset counters
|
||||
completed_count = 0
|
||||
all_results.clear()
|
||||
threads_done = false
|
||||
start_time = Time.get_ticks_msec()
|
||||
last_progress_time = start_time
|
||||
monitoring_active = true
|
||||
|
||||
# Create and start threads
|
||||
print("Starting %d worker threads..." % num_threads)
|
||||
for i in range(num_threads):
|
||||
var thread = Thread.new()
|
||||
thread.start(worker_thread.bind(i))
|
||||
threads.append(thread)
|
||||
|
||||
print("All threads started, processing...")
|
||||
|
||||
func finish_processing():
|
||||
"""Called when all processing is complete"""
|
||||
print("\nAll combinations complete! Waiting for threads to finish...")
|
||||
|
||||
# Wait for all threads to finish
|
||||
for thread in threads:
|
||||
thread.wait_to_finish()
|
||||
threads.clear()
|
||||
threads_done = true
|
||||
|
||||
print("All threads finished. Processing results...")
|
||||
|
||||
var total_time = (Time.get_ticks_msec() - start_time) / 1000.0
|
||||
|
||||
# SAFETY CHECK: Verify result count matches
|
||||
results_mutex.lock()
|
||||
var actual_results = all_results.size()
|
||||
results_mutex.unlock()
|
||||
|
||||
if actual_results != total_combinations:
|
||||
print("WARNING: Result count mismatch! Expected %d, got %d" % [total_combinations, actual_results])
|
||||
print("This indicates a threading issue where some results weren't flushed")
|
||||
|
||||
# Print results
|
||||
print("\n=== RESULTS ===")
|
||||
print("Total time: %.1f seconds" % total_time)
|
||||
print("Total combinations tested: %d (expected %d)" % [actual_results, total_combinations])
|
||||
|
||||
# Cache statistics
|
||||
var total_cache_checks = cache_hits + cache_misses
|
||||
var cache_hit_rate = (float(cache_hits) / total_cache_checks * 100.0) if total_cache_checks > 0 else 0.0
|
||||
cache_mutex.lock()
|
||||
var cache_size = simulation_cache.size()
|
||||
cache_mutex.unlock()
|
||||
print("\n=== CACHE STATISTICS ===")
|
||||
print("Cache hits: %d" % cache_hits)
|
||||
print("Cache misses: %d" % cache_misses)
|
||||
print("Hit rate: %.1f%%" % cache_hit_rate)
|
||||
print("Cache entries stored: %d" % cache_size)
|
||||
|
||||
var successful = all_results.filter(func(r): return r.success)
|
||||
print("Successful strategies: %d" % successful.size())
|
||||
|
||||
# Update UI status
|
||||
status_label.text = "Status: Complete!"
|
||||
progress_label.text = "Progress: 100%% (%d/%d)" % [all_results.size(), total_combinations]
|
||||
progress_bar.value = 1.0
|
||||
eta_label.text = "Total Time: %.1f seconds" % total_time
|
||||
|
||||
# Build results text for UI
|
||||
var results_text = "[b]SIMULATION COMPLETE[/b]\n\n"
|
||||
results_text += "[color=green]Total time: %.1f seconds[/color]\n" % total_time
|
||||
results_text += "Combinations tested: %d\n" % all_results.size()
|
||||
results_text += "Successful strategies: %d\n\n" % successful.size()
|
||||
|
||||
results_text += "[b]Cache Performance:[/b]\n"
|
||||
results_text += " Hits: %d\n" % cache_hits
|
||||
results_text += " Misses: %d\n" % cache_misses
|
||||
results_text += " [color=cyan]Hit Rate: %.1f%%[/color]\n" % cache_hit_rate
|
||||
results_text += " Entries: %d\n\n" % cache_size
|
||||
|
||||
if successful.size() > 0:
|
||||
# Sort by ticks (fastest first)
|
||||
successful.sort_custom(func(a, b): return a.ticks < b.ticks)
|
||||
|
||||
# Re-simulate top 10 with detailed purchase tracking
|
||||
print("\n=== RE-SIMULATING TOP 10 WITH PURCHASE TRACKING ===")
|
||||
var unlock_data = serialize_unlock_data()
|
||||
var top_10_detailed: Array = []
|
||||
|
||||
for i in range(min(10, successful.size())):
|
||||
var result = successful[i]
|
||||
print("Re-simulating #%d with track_purchases=true..." % (i + 1))
|
||||
var detailed_result = simulate_rank_combination_pure(result.rank_targets, unlock_data, 1000000, true)
|
||||
print(" Result has purchases key: %s" % detailed_result.has("purchases"))
|
||||
if detailed_result.has("purchases"):
|
||||
print(" Purchases array size: %d" % detailed_result.purchases.size())
|
||||
top_10_detailed.append(detailed_result)
|
||||
|
||||
print("\n=== TOP 10 FASTEST STRATEGIES (WITH PURCHASE TIMELINE) ===")
|
||||
results_text += "[b]TOP 10 FASTEST STRATEGIES:[/b]\n\n"
|
||||
|
||||
for i in range(top_10_detailed.size()):
|
||||
var result = top_10_detailed[i]
|
||||
print("\n#%d: %s (%d ticks)" % [i + 1, result.time_formatted, result.ticks])
|
||||
|
||||
# Format ranks with unlock names
|
||||
var rank_display = []
|
||||
for unlock_id in result.rank_targets.keys():
|
||||
var unlock_name = get_unlock_name_by_id(unlock_id)
|
||||
var ranks = result.rank_targets[unlock_id]
|
||||
rank_display.append("%s: %d" % [unlock_name, ranks])
|
||||
print("Target Ranks: %s" % ", ".join(rank_display))
|
||||
|
||||
# Add to UI
|
||||
results_text += "[color=yellow]#%d: %s (%d ticks)[/color]\n" % [i + 1, result.time_formatted, result.ticks]
|
||||
results_text += " Ranks: %s\n" % ", ".join(rank_display)
|
||||
results_text += " Currency: %.0f\n" % result.final_currency
|
||||
|
||||
# Add purchase timeline
|
||||
if result.has("purchases") and result.purchases.size() > 0:
|
||||
print("\nPurchase Timeline:")
|
||||
results_text += " [b]Purchase Timeline:[/b]\n"
|
||||
for purchase in result.purchases:
|
||||
var time_str = format_time(purchase.tick)
|
||||
print(" %s: %s Rank %d - Cost: %d¥ @ %s" % [
|
||||
time_str, purchase.unlock_name, purchase.rank,
|
||||
purchase.cost, time_str
|
||||
])
|
||||
results_text += " • %s [color=cyan]%s Rank %d[/color] - %d¥ @ %s\n" % [
|
||||
format_time(purchase.tick), purchase.unlock_name, purchase.rank,
|
||||
purchase.cost, time_str
|
||||
]
|
||||
results_text += "\n"
|
||||
else:
|
||||
print("\nNo successful strategies found!")
|
||||
results_text += "[color=red]No successful strategies found![/color]\n"
|
||||
|
||||
# Update results UI
|
||||
results_label.text = results_text
|
||||
|
||||
func get_unlock_name_by_id(unlock_id: int) -> String:
|
||||
"""Helper function to get unlock name by ID"""
|
||||
for unlock in unlock_collection.unlocks:
|
||||
if unlock.unlock_id == unlock_id:
|
||||
return unlock.unlock_name
|
||||
return "Unknown"
|
||||
|
||||
func _exit_tree():
|
||||
# Clean up threads
|
||||
monitoring_active = false
|
||||
for thread in threads:
|
||||
if thread.is_alive():
|
||||
thread.wait_to_finish()
|
||||
|
|
@ -1 +0,0 @@
|
|||
uid://bup76ad02kuse
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1 +0,0 @@
|
|||
uid://citjokiv6skqi
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
class_name UnlockDataLightweight
|
||||
## Lightweight unlock data structure for simulations
|
||||
## Contains the same calculation logic as UnlockDataResource but without Resource overhead
|
||||
|
||||
var unlock_id: int = 0
|
||||
var unlock_name: String = ""
|
||||
var base_cost: int = 0
|
||||
var is_unlocked: bool = false
|
||||
|
||||
# Scaling settings
|
||||
var is_scaling: bool = false
|
||||
var current_rank: int = 0
|
||||
var max_rank: int = -1
|
||||
|
||||
# Cost scaling
|
||||
var cost_scaling_type: int = 1 # 0=Linear, 1=Exponential
|
||||
var cost_scaling_multiplier: float = 1.5
|
||||
var cost_linear_increase: int = 100
|
||||
var cost_ladder: Array[int] = []
|
||||
|
||||
# Effect ladder
|
||||
var effect_ladder: Array[float] = []
|
||||
|
||||
# Base modifiers
|
||||
var base_modifiers: Dictionary = {}
|
||||
|
||||
## Static factory method to create from UnlockDataResource (one-time conversion)
|
||||
static func from_resource(resource: UnlockDataResource) -> UnlockDataLightweight:
|
||||
var data = UnlockDataLightweight.new()
|
||||
data.unlock_id = resource.unlock_id
|
||||
data.unlock_name = resource.unlock_name
|
||||
data.base_cost = resource.base_cost
|
||||
data.is_scaling = resource.is_scaling
|
||||
data.max_rank = resource.max_rank
|
||||
data.cost_scaling_type = resource.cost_scaling_type
|
||||
data.cost_scaling_multiplier = resource.cost_scaling_multiplier
|
||||
data.cost_linear_increase = resource.cost_linear_increase
|
||||
data.cost_ladder = resource.cost_ladder.duplicate()
|
||||
data.effect_ladder = resource.effect_ladder.duplicate()
|
||||
data.base_modifiers = resource.base_modifiers.duplicate(true)
|
||||
# Start fresh
|
||||
data.is_unlocked = false
|
||||
data.current_rank = 0
|
||||
return data
|
||||
|
||||
## Clone for thread safety (fast - no Resource creation)
|
||||
func clone() -> UnlockDataLightweight:
|
||||
var copy = UnlockDataLightweight.new()
|
||||
copy.unlock_id = unlock_id
|
||||
copy.unlock_name = unlock_name
|
||||
copy.base_cost = base_cost
|
||||
copy.is_scaling = is_scaling
|
||||
copy.max_rank = max_rank
|
||||
copy.cost_scaling_type = cost_scaling_type
|
||||
copy.cost_scaling_multiplier = cost_scaling_multiplier
|
||||
copy.cost_linear_increase = cost_linear_increase
|
||||
copy.cost_ladder = cost_ladder # Shared - read-only
|
||||
copy.effect_ladder = effect_ladder # Shared - read-only
|
||||
copy.base_modifiers = base_modifiers # Shared - read-only
|
||||
# Mutable state
|
||||
copy.is_unlocked = false
|
||||
copy.current_rank = 0
|
||||
return copy
|
||||
|
||||
## Same logic as UnlockDataResource.get_next_cost()
|
||||
func get_next_cost() -> int:
|
||||
if not is_scaling:
|
||||
return base_cost
|
||||
|
||||
if cost_ladder.size() > 0 and current_rank < cost_ladder.size():
|
||||
return cost_ladder[current_rank]
|
||||
|
||||
if cost_scaling_type == 0: # Linear
|
||||
return base_cost + (cost_linear_increase * current_rank)
|
||||
else: # Exponential
|
||||
return int(base_cost * pow(cost_scaling_multiplier, current_rank))
|
||||
|
||||
## Same logic as UnlockDataResource.get_current_modifiers()
|
||||
func get_current_modifiers() -> Dictionary:
|
||||
if not is_unlocked or current_rank == 0:
|
||||
return {}
|
||||
return get_modifiers_at_rank(current_rank)
|
||||
|
||||
## Same logic as UnlockDataResource.get_modifiers_at_rank()
|
||||
func get_modifiers_at_rank(rank: int) -> Dictionary:
|
||||
if rank == 0:
|
||||
return {}
|
||||
|
||||
# For one-shot unlocks or empty ladder, return base_modifiers
|
||||
if effect_ladder.size() == 0:
|
||||
return base_modifiers.duplicate()
|
||||
|
||||
var ladder_index = rank - 1
|
||||
if ladder_index >= effect_ladder.size():
|
||||
ladder_index = effect_ladder.size() - 1 # Use last value if rank exceeds ladder
|
||||
|
||||
var result = {}
|
||||
for key in base_modifiers.keys():
|
||||
result[key] = effect_ladder[ladder_index]
|
||||
return result
|
||||
|
||||
## Same logic as UnlockDataResource.can_rank_up()
|
||||
func can_rank_up() -> bool:
|
||||
if not is_scaling:
|
||||
return not is_unlocked
|
||||
|
||||
if max_rank > 0 and current_rank >= max_rank:
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
## Same logic as UnlockDataResource.unlock()
|
||||
func unlock() -> bool:
|
||||
if not can_rank_up():
|
||||
return false
|
||||
|
||||
if not is_scaling:
|
||||
is_unlocked = true
|
||||
current_rank = 1
|
||||
else:
|
||||
current_rank += 1
|
||||
is_unlocked = true
|
||||
|
||||
return true
|
||||
|
|
@ -1 +0,0 @@
|
|||
uid://yx6cnoob2can
|
||||
Loading…
Add table
Add a link
Reference in a new issue