405 lines
13 KiB
GDScript
405 lines
13 KiB
GDScript
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
|