Mostly done?

This commit is contained in:
Dan 2026-01-29 13:26:42 +00:00
parent 22d7326565
commit a08c13b1a3
118 changed files with 2558 additions and 2519 deletions

79
scenes/scripts/animal.gd Normal file
View file

@ -0,0 +1,79 @@
@tool
extends Node2D
@export var animal_type : String:
set(value):
animal_type = value
if is_node_ready():
_update_sprite()
@export var flip_horizontal : bool = false:
set(value):
flip_horizontal = value
if is_node_ready():
_update_sprite()
@export var show_shavings : bool = false:
set(value):
show_shavings = value
if is_node_ready():
_update_shavings()
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
_update_sprite()
_update_shavings()
func _update_sprite() -> void:
# Hide all animal sprites first
%Fox.visible = false
%Porcupine.visible = false
%Wolf.visible = false
%Cat.visible = false
%Goose.visible = false
%Frog.visible = false
%Chick.visible = false
%Dog.visible = false
# Show the appropriate animal based on animal_type
var active_sprite: AnimatedSprite2D
match animal_type:
"Fox":
%Fox.visible = true
active_sprite = %Fox
"Porcupine":
%Porcupine.visible = true
active_sprite = %Porcupine
"Wolf":
%Wolf.visible = true
active_sprite = %Wolf
"Cat":
%Cat.visible = true
active_sprite = %Cat
"Goose":
%Goose.visible = true
active_sprite = %Goose
"Frog":
%Frog.visible = true
active_sprite = %Frog
"Chick":
%Chick.visible = true
active_sprite = %Chick
"Dog":
%Dog.visible = true
active_sprite = %Dog
# Apply horizontal flip if enabled
if active_sprite:
active_sprite.flip_h = flip_horizontal
func _update_shavings() -> void:
%Shavings.visible = show_shavings
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void:
pass

View file

@ -0,0 +1 @@
uid://uhlsvqaaemre

View file

@ -1,58 +1,136 @@
extends TextureButton
@onready var label: Label = $CenterContainer/Label # Adjust path to your Label node
@onready var name_label: Label = $CenterContainer/VBoxContainer/NameLabel
@onready var price_label: Label = $CenterContainer/VBoxContainer/PriceLabel
var unlock_id = "" # Store the unlock ID
var unlock_id = -1 # Store the unlock ID
var unlock_description_text = "" # Store the description for custom tooltip
func _ready():
label.visible = false # Hide label initially
name_label.visible = false # Hide label initially
price_label.visible = false
adjust_label_font_size()
# Connect the pressed signal
pressed.connect(_on_button_pressed)
# Connect to currency changes to update button state
Inventory.currency_changed.connect(_on_currency_changed)
# Connect to unlock events to update button state when items are unlocked
Unlocks.item_unlocked.connect(_on_item_unlocked)
func setup(unlock_data):
# Log.pr("Setting up button for unlock:", unlock_data.unlock_name)
unlock_id = unlock_data.unlock_id # Store the ID
if label:
label.visible = false
label.text = unlock_data.unlock_name + " " + str(unlock_data.get_next_rank())
label.text = label.text + " - " + Global.currency_symbol + str(unlock_data.get_next_cost())
label.text = label.text + "\n" + unlock_data.get_next_modifiers_string()
#self.disabled = unlock_data.is_unlocked
if name_label and price_label:
name_label.visible = false
price_label.visible = false
update_button_state(unlock_data)
adjust_label_font_size()
else:
Log.pr("Warning: Label node not found in button.")
Log.pr("Warning: Label nodes not found in button.")
func update_button_state(unlock_data):
# Store unlock description for custom tooltip
if unlock_data.unlock_description:
unlock_description_text = unlock_data.unlock_description
tooltip_text = unlock_data.unlock_description
# Check if at max rank
if not unlock_data.can_rank_up():
self.disabled = true
name_label.text = unlock_data.unlock_name + " (MAX)"
price_label.text = ""
return
# Build name text - only show rank if it's a scaling unlock
if unlock_data.is_scaling:
name_label.text = unlock_data.unlock_name + " " + str(unlock_data.get_next_rank())
else:
name_label.text = unlock_data.unlock_name
# Build price text
price_label.text = Global.currency_symbol + Global.format_number(unlock_data.get_next_cost())
# Check if player has enough currency
var cost = unlock_data.get_next_cost()
var current_currency = Inventory.get_currency()
self.disabled = current_currency < cost
func _on_currency_changed(new_amount: float):
# Update button state when currency changes
if unlock_id >= 0:
var unlock_data = Unlocks.get_unlock_by_id(unlock_id)
if unlock_data:
update_button_state(unlock_data)
func _on_item_unlocked():
# Update button state when any item is unlocked (in case this button reached max rank)
if unlock_id >= 0:
var unlock_data = Unlocks.get_unlock_by_id(unlock_id)
if unlock_data:
update_button_state(unlock_data)
adjust_label_font_size()
func _on_button_pressed():
# Log.pr("Button pressed, unlocking item:", unlock_id)
Unlocks.unlock_item(unlock_id)
func adjust_label_font_size():
if not label:
if not name_label or not price_label:
return
var available_width = size.x - 10
var available_height = size.y - 10
# Start with a reasonable font size
var font_size = 32
# Calculate font sizes for both labels
# Name label gets 60% of the height, price label gets 40%
var name_height = available_height * 0.6
var price_height = available_height * 0.4
# Start with reasonable font sizes
var name_font_size = 32
var price_font_size = 24
var min_font_size = 8
# Get or create a font
var font = label.get_theme_font("font")
# Binary search for the optimal font size
while font_size > min_font_size:
label.add_theme_font_size_override("font_size", font_size)
# Force update and get the actual text size
await get_tree().process_frame
var text_size = font.get_string_size(label.text, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size)
# Check if it fits
if text_size.x <= available_width and text_size.y <= available_height:
# Get fonts
var name_font = name_label.get_theme_font("font")
var price_font = price_label.get_theme_font("font")
# Calculate name label font size without applying it
while name_font_size > min_font_size:
var text_size = name_font.get_string_size(name_label.text, HORIZONTAL_ALIGNMENT_LEFT, -1, name_font_size)
if text_size.x <= available_width and text_size.y <= name_height:
break
# Reduce font size and try again
font_size -= 1
label.add_theme_font_size_override("font_size", font_size)
label.visible = true # Show label after resizing is complete
name_font_size -= 1
# Calculate price label font size without applying it
while price_font_size > min_font_size:
var text_size = price_font.get_string_size(price_label.text, HORIZONTAL_ALIGNMENT_LEFT, -1, price_font_size)
if text_size.x <= available_width and text_size.y <= price_height:
break
price_font_size -= 1
# Apply both font sizes at once
name_label.add_theme_font_size_override("font_size", name_font_size)
price_label.add_theme_font_size_override("font_size", price_font_size)
name_label.visible = true
price_label.visible = true
# Call this function whenever you change the label text
func set_label_text(new_text: String):
if label:
label.visible = false # Hide while resizing
label.text = new_text
if name_label:
name_label.visible = false # Hide while resizing
price_label.visible = false
name_label.text = new_text
price_label.text = ""
adjust_label_font_size()
# Override to create custom tooltip with larger font
func _make_custom_tooltip(for_text: String) -> Object:
var tooltip_label = Label.new()
tooltip_label.text = for_text
tooltip_label.add_theme_font_size_override("font_size", 26)
# Create a panel container for background
var panel = PanelContainer.new()
panel.add_child(tooltip_label)
return panel

View file

@ -0,0 +1,41 @@
extends Panel
@onready var music_toggle: CheckButton = %MusicToggle
@onready var chop_toggle: CheckButton = %ChopToggle
@onready var money_toggle: CheckButton = %MoneyToggle
@onready var close_button: Button = %CloseButton
func _ready():
# Initialize toggle states from Global settings
music_toggle.button_pressed = Global.play_background_music
chop_toggle.button_pressed = Global.play_chop_sound
money_toggle.button_pressed = Global.play_money_sound
# Connect signals
music_toggle.toggled.connect(_on_music_toggled)
chop_toggle.toggled.connect(_on_chop_toggled)
money_toggle.toggled.connect(_on_money_toggled)
close_button.pressed.connect(_on_close_pressed)
# Start hidden
visible = false
func _on_music_toggled(enabled: bool):
Global.play_background_music = enabled
var audio_manager = get_node("/root/Audio")
if enabled:
audio_manager.play_background_music()
else:
audio_manager.stop_background_music()
func _on_chop_toggled(enabled: bool):
Global.play_chop_sound = enabled
func _on_money_toggled(enabled: bool):
Global.play_money_sound = enabled
func _on_close_pressed():
visible = false
func toggle_visibility():
visible = !visible

View file

@ -0,0 +1 @@
uid://2jm25u1ehlac

View file

@ -8,10 +8,19 @@ extends Control
@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()
@ -34,6 +43,7 @@ func _ready():
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)
@ -41,17 +51,27 @@ func _ready():
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 + " " + str(int(Inventory.get_currency()))
currency_label.text = Global.currency_symbol + Global.format_number(Inventory.get_currency())
func update_wood_label():
wood_label.text = "W: " + str(int(Inventory.get_wood()))
wood_label.text = Global.format_number(Inventory.get_wood())
func update_stock_label():
stock_label.text = "S: " + str(int(Inventory.get_stock()))
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)
@ -64,7 +84,7 @@ func spawn_stock_increase(value, _total):
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 + str(int(abs(value)))
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
@ -99,10 +119,10 @@ func populate_unlock_buttons():
func populate_modifiers_display():
var modifiers_text = ""
modifiers_text = modifiers_text + "Sale Price: " + Global.currency_symbol + str(Unlocks.get_sale_price_per_item()) + "\n"
modifiers_text = modifiers_text + "Items Produced Per Tick: " + str(Unlocks.get_items_produced_per_tick()) + "\n"
modifiers_text = modifiers_text + "Wood per Click: " + str(Unlocks.get_wood_per_click()) + "\n\n"
modifiers_text = modifiers_text + "Demand: " + str(int(Unlocks.get_sale_demand())) + "\n\n"
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():
@ -129,7 +149,20 @@ func _on_timer_tick():
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)
@ -145,3 +178,228 @@ 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