extends Control @onready var currency_label: Label = %CurrencyLabel @onready var wood_label: Label = %WoodLabel @onready var stock_label: Label = %StockLabel @onready var modifiers_label: Label = %ModifiersLabel @onready var timer_label : Label = %Timer @onready var game_complete_screen : Panel = %GameCompleted @onready var completion_time_label : Label = %CompletionTimeLabel @onready var continue_button : TextureButton = %ContinueButton @onready var config_button : Button = %ConfigButton @onready var config_panel : Panel = %ConfigPanel @onready var player_name_input : TextEdit = %TextEdit @onready var submit_score_button : TextureButton = %SubmitScoreButton @onready var submission_status_label : Label = %SubmissionStatusLabel var game_timer : Timer var elapsed_time := 0.0 var current_nonce : String = "" var bridge = null var score_submitted := false func _ready(): populate_modifiers_display() populate_unlock_buttons() update_currency_label() update_wood_label() update_stock_label() game_timer = Timer.new() game_timer.wait_time = 1.0 game_timer.one_shot = false game_timer.autostart = true add_child(game_timer) game_timer.timeout.connect(_on_timer_tick) currency_label.add_theme_color_override("font_color", Global.money_color) wood_label.add_theme_color_override("font_color", Global.wood_color) stock_label.add_theme_color_override("font_color", Global.stock_color) Inventory.currency_changed.connect(_on_currency_changed) Inventory.currency_added.connect(spawn_currency_increase) Inventory.currency_added.connect(_on_currency_added) Inventory.wood_changed.connect(_on_currency_changed) Inventory.wood_added.connect(spawn_wood_increase) Inventory.stock_added.connect(spawn_stock_increase) Inventory.stock_changed.connect(_on_currency_changed) Unlocks.item_unlocked.connect(populate_unlock_buttons) GameManager.currency_goal_met.connect(_on_currency_goal_met) if config_button: config_button.pressed.connect(_on_config_button_pressed) if submit_score_button: submit_score_button.pressed.connect(_on_submit_score_button_pressed) # Initialize JavaScript bridge for web builds if OS.has_feature("web"): bridge = JavaScriptBridge.get_interface("godotBridge") # get_tree().paused = true func update_currency_label(): currency_label.text = Global.currency_symbol + Global.format_number(Inventory.get_currency()) func update_wood_label(): wood_label.text = Global.format_number(Inventory.get_wood()) func update_stock_label(): stock_label.text = Global.format_number(Inventory.get_stock()) func spawn_currency_increase(value, _total): spawn_inventory_change_value(value, _total, "+", Global.currency_symbol, Global.money_color) func spawn_wood_increase(value, _total): spawn_inventory_change_value(value, _total, "+", "", Global.wood_color) func spawn_stock_increase(value, _total): spawn_inventory_change_value(value, _total, "+", "", Global.stock_color) func spawn_inventory_change_value(value, _total, display_sign: String = "+", symbol: String = "", label_color: Color = Color.WHITE): var float_label = Label.new() float_label.text = display_sign + symbol + Global.format_number(abs(value)) float_label.add_theme_font_size_override("font_size", 16) float_label.modulate = label_color # Add random offset around center var random_x = randf_range(-60, 30) var random_y = randf_range(-40, 20) float_label.position = Vector2(random_x, random_y) add_child(float_label) # Animate the label var tween = create_tween() tween.set_parallel(true) # Run both animations simultaneously # Move up tween.tween_property(float_label, "position:y", float_label.position.y - 50, 1.0) # Fade out tween.tween_property(float_label, "modulate:a", 0.0, 1.0) # Remove from scene when done tween.chain().tween_callback(float_label.queue_free) func populate_unlock_buttons(): var unlocks_container = $UnlockContainer for child in unlocks_container.get_children(): child.free() for unlock_data in Unlocks.unlocks.unlocks: var unlock_button_scene = load("res://scenes/button.tscn") var unlock_button = unlock_button_scene.instantiate() unlocks_container.add_child(unlock_button) unlock_button.setup(unlock_data) func populate_modifiers_display(): var modifiers_text = "" modifiers_text = modifiers_text + "Sale Price: " + Global.currency_symbol + Global.format_number(Unlocks.get_sale_price_per_item()) + "\n" modifiers_text = modifiers_text + "Items Produced Per Tick: " + Global.format_number(Unlocks.get_items_produced_per_tick()) + "\n" modifiers_text = modifiers_text + "Wood per Click: " + Global.format_number(Unlocks.get_wood_per_click()) + "\n\n" modifiers_text = modifiers_text + "Demand: " + Global.format_number(Unlocks.get_sale_demand()) + "\n\n" modifiers_text = modifiers_text + "Current Modifiers:\n" for key in Unlocks.current_modifiers.keys(): var display_name = key.replace("_modifier", "").replace("_", " ").capitalize() var percentage = int((Unlocks.current_modifiers[key] - 1.0) * 100) modifiers_text += "%s: %s%%\n" % [display_name, str(percentage)] modifiers_label.text = modifiers_text func _on_currency_changed(_value): populate_modifiers_display() update_currency_label() update_wood_label() update_stock_label() if Inventory.get_currency() >= Global.target_currency: game_timer.stop() func _on_timer_tick(): elapsed_time += 1 timer_label.text = format_time(elapsed_time) func _on_currency_goal_met(): Log.pr("Currency goal met!") get_tree().paused = true completion_time_label.text = format_time(elapsed_time) game_complete_screen.visible = true score_submitted = false # Request nonce from API when game is completed if bridge != null: # Disable submit button until nonce is received submit_score_button.disabled = true submission_status_label.visible = false _request_nonce() else: # No web bridge available, hide the submit button entirely submit_score_button.visible = false submission_status_label.visible = false func format_time(seconds: float) -> String: var total_seconds := int(seconds) var hours := total_seconds / 3600 var minutes := (total_seconds % 3600) / 60 var secs := total_seconds % 60 return "%02d:%02d:%02d" % [hours, minutes, secs] func _on_continue_button_pressed() -> void: Global.game_continue_pressed = true game_complete_screen.visible = false get_tree().paused = false func _on_config_button_pressed() -> void: if config_panel: config_panel.toggle_visibility() func _on_currency_added(_value, _total): var audio_manager = get_node("/root/Audio") if audio_manager: audio_manager.play_money_sound() # HIGH SCORE SUBMISSION FUNCTIONS func _request_nonce(): if bridge == null: return # Call JavaScript bridge to request nonce var result = bridge.requestNonce() # Set up polling to check if nonce was received # (since JS callbacks are async) var nonce_check_timer = Timer.new() nonce_check_timer.wait_time = 0.5 nonce_check_timer.one_shot = false nonce_check_timer.process_mode = Node.PROCESS_MODE_ALWAYS # Run even when paused add_child(nonce_check_timer) var attempts = 0 var max_attempts = 20 # 10 seconds total nonce_check_timer.timeout.connect(func(): attempts += 1 var nonce = _get_nonce_from_js() if nonce != null and nonce != "" and nonce != "null": current_nonce = nonce # Enable submit button when nonce is ready submit_score_button.disabled = false nonce_check_timer.stop() nonce_check_timer.queue_free() elif attempts >= max_attempts: # Failed to get nonce - show error and keep button disabled submission_status_label.text = "Failed to connect to server" submission_status_label.modulate = Color(1.0, 0.3, 0.3) # Red submission_status_label.visible = true nonce_check_timer.stop() nonce_check_timer.queue_free() ) nonce_check_timer.start() func _get_nonce_from_js() -> String: if bridge == null: return "" # Try to get the nonce that JavaScript stored var result = JavaScriptBridge.eval(""" (function() { if (window.godotNonce) { return window.godotNonce; } return ''; })(); """, true) return str(result) if result != null else "" func _on_submit_score_button_pressed(): if score_submitted: submission_status_label.text = "Score already submitted!" submission_status_label.modulate = Color(1.0, 0.8, 0.3) # Yellow submission_status_label.visible = true return if bridge == null or current_nonce == "": # This shouldn't happen as button should be disabled return # Disable button during submission and show status submit_score_button.disabled = true submission_status_label.text = "Submitting..." submission_status_label.modulate = Color(1.0, 1.0, 1.0) # White submission_status_label.visible = true # Get player name from input and sanitize var player_name = _sanitize_player_name(player_name_input.text) if player_name == "": player_name = "Anonymous" # Get completion time in seconds var completion_time_seconds = int(elapsed_time) # Create and encode payload on GDScript side var encoded_payload = _create_encoded_payload(current_nonce, player_name, completion_time_seconds) # Call JavaScript to submit score with pre-encoded payload and nonce bridge.submitScore(encoded_payload, current_nonce) # Poll for submission result var submit_check_timer = Timer.new() submit_check_timer.wait_time = 0.5 submit_check_timer.one_shot = false submit_check_timer.process_mode = Node.PROCESS_MODE_ALWAYS # Run even when paused add_child(submit_check_timer) var attempts = 0 var max_attempts = 30 # 15 seconds total submit_check_timer.timeout.connect(func(): attempts += 1 var result = _get_submission_result_from_js() if result.has("completed") and result["completed"]: submission_status_label.visible = true if result["success"]: submission_status_label.text = result.get("message", "Score submitted!") submission_status_label.modulate = Color(0.5, 1.0, 0.5) # Green score_submitted = true # Show rank if available if result.has("rank") and result["rank"] > 0: submission_status_label.text += " (Rank #%d)" % result["rank"] else: submission_status_label.text = result.get("message", "Failed to submit") submission_status_label.modulate = Color(1.0, 0.3, 0.3) # Red submit_score_button.disabled = false submit_check_timer.stop() submit_check_timer.queue_free() elif attempts >= max_attempts: submission_status_label.text = "Submission timeout" submission_status_label.modulate = Color(1.0, 0.3, 0.3) # Red submission_status_label.visible = true submit_score_button.disabled = false submit_check_timer.stop() submit_check_timer.queue_free() ) submit_check_timer.start() func _get_submission_result_from_js() -> Dictionary: if bridge == null: return {} var result = JavaScriptBridge.eval(""" (function() { if (window.godotSubmissionResult) { var result = window.godotSubmissionResult; window.godotSubmissionResult = null; // Clear after reading return JSON.stringify(result); } return '{}'; })(); """, true) if result != null and result != "": var json = JSON.new() var error = json.parse(str(result)) if error == OK: return json.data return {} func _create_encoded_payload(nonce: String, player_name: String, completion_time: int) -> String: # Create the payload as JSON var payload = { "nonce": nonce, "gameId": "whittling-clicker", # Unique identifier for this game "playerName": player_name, "completionTime": completion_time, "timestamp": Time.get_unix_time_from_system() * 1000 # Convert to milliseconds } var json_string = JSON.stringify(payload) # XOR encode with key derived from nonce var key = "WHITTLING_KEY_" + nonce.substr(0, 8) var encoded_bytes = _xor_encode(json_string, key) # Base64 encode (using raw bytes) var base64_encoded = Marshalls.raw_to_base64(encoded_bytes) return base64_encoded func _sanitize_player_name(name: String) -> String: # Strip leading/trailing whitespace name = name.strip_edges() # Remove control characters and most special characters, keep letters, numbers, spaces, and safe punctuation var safe_name = "" for i in range(name.length()): var c = name[i] var code = name.unicode_at(i) # Allow: letters, numbers, spaces, hyphens, underscores, periods # Block: control chars, path separators, quotes, angle brackets, etc if (code >= 48 and code <= 57) or \ (code >= 65 and code <= 90) or \ (code >= 97 and code <= 122) or \ c == " " or c == "-" or c == "_" or c == ".": safe_name += c # Limit length to 50 characters safe_name = safe_name.substr(0, 50) # Remove multiple consecutive spaces while safe_name.find(" ") != -1: safe_name = safe_name.replace(" ", " ") # Strip again after processing safe_name = safe_name.strip_edges() return safe_name func _xor_encode(text: String, key: String) -> PackedByteArray: var text_bytes = text.to_utf8_buffer() var key_bytes = key.to_utf8_buffer() var key_length = key_bytes.size() var result = PackedByteArray() for i in range(text_bytes.size()): var xor_byte = text_bytes[i] ^ key_bytes[i % key_length] result.append(xor_byte) return result