diff --git a/scripts/fish.gd b/scripts/fish.gd index d43c1a0..2a8a4e8 100644 --- a/scripts/fish.gd +++ b/scripts/fish.gd @@ -191,6 +191,14 @@ mDifficulty, mMin_weight, mMax_weight, price_weight_multiplier, mType, weight_mu self.type = mType self.is_shiny = mIs_shiny + # In infinite mode, scale down fish prices significantly past 600m depth + if GameState.is_infinite() and abs(mStart_position.y) > 600: + var depth_factor = abs(mStart_position.y) - 600 + # Scale down price more aggressively the deeper you go + # At 700m: 0.5x, at 800m: 0.25x, at 1000m: 0.1x, etc. + var scale_factor = max(0.1, 1.0 - (depth_factor / 400.0)) + self.price = round(self.price * scale_factor) + scale = get_scale_for_weight(mMax_weight, mMin_weight, weight) if self.is_shiny: diff --git a/scripts/game_state.gd b/scripts/game_state.gd index 977ab33..8eeea81 100644 --- a/scripts/game_state.gd +++ b/scripts/game_state.gd @@ -4,7 +4,7 @@ signal inventory_updated #DEFINITIONS enum Stage {SURFACE, DEEP, DEEPER, SUPERDEEP, HOT, LAVA, VOID} -enum GameMode {NORMAL, INTRO_MISSION} +enum GameMode {NORMAL, INTRO_MISSION, INFINITE} var depthStageMap = { 0: Stage.SURFACE, 100: Stage.DEEP, @@ -15,7 +15,7 @@ var depthStageMap = { 600: Stage.VOID } -enum Upgrade {CARGO_SIZE, DEPTH_RESISTANCE, PICKAXE_UNLOCKED, VERT_SPEED, HOR_SPEED, LAMP_UNLOCKED, AK47, DUALAK47, HARPOON, HARPOON_ROTATION, INVENTORY_MANAGEMENT, SURFACE_BUOY, INVENTORY_SAVE, DRONE_SELLING} +enum Upgrade {CARGO_SIZE, DEPTH_RESISTANCE, PICKAXE_UNLOCKED, VERT_SPEED, HOR_SPEED, LAMP_UNLOCKED, AK47, DUALAK47, HARPOON, HARPOON_ROTATION, INVENTORY_MANAGEMENT, SURFACE_BUOY, INVENTORY_SAVE, DRONE_SELLING, SUBMARINE_COLOR, DRONE_COLOR} var upgradeCosts = { Upgrade.CARGO_SIZE: 25, Upgrade.DEPTH_RESISTANCE: 50, @@ -30,7 +30,9 @@ var upgradeCosts = { Upgrade.INVENTORY_MANAGEMENT: 400, Upgrade.SURFACE_BUOY: 1000, Upgrade.INVENTORY_SAVE: 250, - Upgrade.DRONE_SELLING: 300 + Upgrade.DRONE_SELLING: 300, + Upgrade.SUBMARINE_COLOR: 50000, + Upgrade.DRONE_COLOR: 30000 } var maxUpgrades = { @@ -47,7 +49,9 @@ var maxUpgrades = { Upgrade.INVENTORY_MANAGEMENT: 1, Upgrade.SURFACE_BUOY: 1, Upgrade.INVENTORY_SAVE: 1, - Upgrade.DRONE_SELLING: 1 + Upgrade.DRONE_SELLING: 1, + Upgrade.SUBMARINE_COLOR: 10, + Upgrade.DRONE_COLOR: 10 } #STATE @@ -65,7 +69,9 @@ var upgrades = { Upgrade.INVENTORY_MANAGEMENT: 0, Upgrade.SURFACE_BUOY: 0, Upgrade.INVENTORY_SAVE: 0, - Upgrade.DRONE_SELLING: 0 + Upgrade.DRONE_SELLING: 0, + Upgrade.SUBMARINE_COLOR: 0, + Upgrade.DRONE_COLOR: 0 } var depth = 0 var maxDepthReached = 0 @@ -91,17 +97,15 @@ var is_first_time_player = true var intro_mission_completed = false var friend_death_position: Vector3 = Vector3.ZERO var boss_encountered = false # Flag to track if player has seen the boss for the first time +var infinite_mode_enabled = false # Flag to track if infinite mode is active func _ready(): # Connect to inventory's methods to emit the inventory_updated signal inventory.connect_signals_to_gamestate(self) - # automatically start intro mission for first-time players (if enabled) - if enable_intro_mission and is_first_time_player and not intro_mission_completed: - start_intro_mission() - else: - # If intro mission is disabled, initialize normal mode - start_normal_mode() + # Show game mode selection UI + await get_tree().process_frame + show_game_mode_selection() func _process(_delta): # Ensure game is not paused during intro mission @@ -125,6 +129,11 @@ func setDepth(d: int): var snapped_depth = snapped(d, 100) var new_stage = Stage.SURFACE # Default to SURFACE for shallow depths + # In infinite mode, continue using VOID stage past 600m + if infinite_mode_enabled and d > 600: + playerInStage = Stage.VOID + return + # Sort depth thresholds and find the highest one that we meet or exceed var sorted_depths = depthStageMap.keys() sorted_depths.sort() @@ -267,6 +276,83 @@ func reset_upgrades(): func is_intro() -> bool: return current_game_mode == GameMode.INTRO_MISSION +func is_infinite() -> bool: + return infinite_mode_enabled + +func start_infinite_mode(): + print("Starting infinite mode") + infinite_mode_enabled = true + current_game_mode = GameMode.INFINITE + is_first_time_player = false + intro_mission_completed = true + boss_encountered = false + + # Reset game state for infinite mode + death_screen = false + paused = false + health = 100 + isDocked = false + + # Set player to surface level + playerInStage = Stage.SURFACE + depth = 0 + maxDepthReached = 0 + + # Initialize Boss system + Boss.boss_spawned = false + Boss.boss_defeated_permanently = false + Boss.boss_dialog_displayed = true + Boss.boss_dialog_section = Boss.BossDialogSections.TUTORIAL1 + Boss.boss_dialog_index = 0 + Boss.boss_health = Boss.boss_max_health + + # Wait for player node to be available, then set position to surface + await get_tree().process_frame + if player_node: + player_node.position = Vector3(-8, 0, 0.33) # Surface position + + # Reset tree pause state + get_tree().paused = false + + print("Infinite mode setup complete") + +func show_game_mode_selection(): + """Show the game mode selection UI""" + # Only show if we haven't started a game mode yet + if current_game_mode != GameMode.NORMAL and current_game_mode != GameState.GameMode.INFINITE: + var ui_node = get_node_or_null("/root/Node3D/UI") + if ui_node: + # Check if game mode selection already exists + var mode_selection = ui_node.find_child("GameModeSelection", true, false) + if not mode_selection: + # Create game mode selection UI + mode_selection = load("res://scripts/ui/game_mode_selection.gd").new() + mode_selection.name = "GameModeSelection" + mode_selection.mode_selected.connect(_on_game_mode_selected) + ui_node.add_child(mode_selection) + + mode_selection.show_menu() + else: + # Fallback: start intro mission if UI not found + if enable_intro_mission and is_first_time_player and not intro_mission_completed: + start_intro_mission() + else: + start_normal_mode() + else: + # Game mode already selected, don't show selection + pass + +func _on_game_mode_selected(mode: GameState.GameMode): + """Handle game mode selection""" + if mode == GameMode.NORMAL: + # For normal mode, check if we should start intro mission + if enable_intro_mission and is_first_time_player and not intro_mission_completed: + start_intro_mission() + else: + start_normal_mode() + elif mode == GameMode.INFINITE: + start_infinite_mode() + func start_normal_mode(): print("Starting normal mode") current_game_mode = GameMode.NORMAL diff --git a/scripts/level.gd b/scripts/level.gd index cd60d37..c96c9f8 100644 --- a/scripts/level.gd +++ b/scripts/level.gd @@ -24,7 +24,7 @@ func _process(_delta: float) -> void: if camera and camera.has_method("change_section_environment"): camera.change_section_environment(GameState.Stage.HOT) - if player.position.y < (lastSpawned) && GameState.current_game_mode == GameState.GameMode.NORMAL: + if player.position.y < (lastSpawned) && (GameState.current_game_mode == GameState.GameMode.NORMAL || GameState.current_game_mode == GameState.GameMode.INFINITE): spawnNewSection(lastSpawned - sectionHeight) if GameState.maxDepthReached > Boss.boss_spawn_height && Boss.boss_spawned == false && Boss.boss_defeated_permanently == false: var boss_spawn_loc = (GameState.maxDepthReached * -1) - 25 @@ -146,22 +146,57 @@ func spawn_intro_mission_sections(): func spawnNewSection(mPosition: float): var newSection = section.instantiate() newSection.position.y = mPosition - var i = snapped(-mPosition, 100) - i = min(i, GameState.depthStageMap.keys()[len(GameState.depthStageMap.keys())-1]) - newSection.sectionType = GameState.depthStageMap[i] - newSection.lastSectionType = last_section_type - - # Give section a meaningful name based on its type and depth var depth_meters = int(abs(mPosition)) - var stage_name = GameState.Stage.keys()[GameState.depthStageMap[i]] - newSection.name = "Section_%s_%dm" % [stage_name, depth_meters] + # Handle infinite mode - continue with VOID stage past 600m + if GameState.is_infinite() and depth_meters > 600: + newSection.sectionType = GameState.Stage.VOID + # Use previous section type for proper transitions + if last_section_type == GameState.Stage.VOID: + newSection.lastSectionType = GameState.Stage.VOID + else: + # Transition from LAVA to VOID + newSection.lastSectionType = GameState.Stage.LAVA + newSection.name = "Section_VOID_%dm" % depth_meters + else: + # Normal section generation - ensure no gaps + var i = snapped(-mPosition, 100) + # Ensure we don't exceed the maximum depth threshold + var max_depth_key = GameState.depthStageMap.keys()[len(GameState.depthStageMap.keys())-1] + i = min(i, max_depth_key) + + # Find the appropriate stage for this depth + var sorted_depths = GameState.depthStageMap.keys() + sorted_depths.sort() + var selected_stage = GameState.Stage.SURFACE + + # Find the highest threshold we meet or exceed + for depth_threshold in sorted_depths: + if i >= depth_threshold: + selected_stage = GameState.depthStageMap[depth_threshold] + + newSection.sectionType = selected_stage + # Set last section type for proper transitions + # If this is the first section or we're at a transition point, use previous + if last_section_type == GameState.Stage.SURFACE and selected_stage != GameState.Stage.SURFACE: + # First transition - use SURFACE as last + newSection.lastSectionType = GameState.Stage.SURFACE + else: + # Use the previous section type for continuity + newSection.lastSectionType = last_section_type + + # Give section a meaningful name based on its type and depth + var stage_name = GameState.Stage.keys()[selected_stage] + newSection.name = "Section_%s_%dm" % [stage_name, depth_meters] + + # Update lastSpawned BEFORE adding to scene to prevent gaps lastSpawned = mPosition GameState.fishes_lower_boarder = lastSpawned - sectionHeight/2 - 1 if mPosition <= -50: newSection.setDepth(mPosition * -1) add_child(newSection) - last_section_type = GameState.depthStageMap[i] + # Update last_section_type AFTER adding section + last_section_type = newSection.sectionType func spawnBoss(mPosition: float): print("Spawn boss", mPosition) diff --git a/scripts/player.gd b/scripts/player.gd index 13e41e1..090638f 100644 --- a/scripts/player.gd +++ b/scripts/player.gd @@ -669,9 +669,27 @@ func activate_selling_drone(): drone.scale = Vector3(0.5, 0.5, 0.5) # Make it a bit smaller than regular fish get_parent().add_child(drone) + # Apply drone color if upgrade is purchased + var drone_color_level = GameState.upgrades[GameState.Upgrade.DRONE_COLOR] + var drone_color = Color(0.8, 0.8, 0.2) # Default golden color + if drone_color_level > 0: + var colors = [ + Color(1.0, 0.2, 0.2), # Red + Color(0.2, 1.0, 0.2), # Green + Color(0.2, 0.2, 1.0), # Blue + Color(1.0, 1.0, 0.2), # Yellow + Color(1.0, 0.2, 1.0), # Magenta + Color(0.2, 1.0, 1.0), # Cyan + Color(1.0, 0.5, 0.2), # Orange + Color(0.5, 0.2, 1.0), # Purple + Color(1.0, 0.8, 0.2), # Gold + Color(0.8, 0.8, 0.8), # Silver + ] + drone_color = colors[min(drone_color_level - 1, colors.size() - 1)] + # Create visual effect for the drone var particles = $dronefart.duplicate() - particles.color = Color(0.8, 0.8, 0.2) # Give it a golden color + particles.color = drone_color drone.add_child(particles) particles.emitting = true @@ -870,4 +888,53 @@ func switch_to_normal_submarine(): # Reset upgrades to normal (will be handled by GameState.complete_intro_mission) # No need to reset here since that function handles the upgrade reset + # Apply cosmetic color if upgrade is purchased + apply_submarine_color() + print("Normal submarine appearance restored") + +func apply_submarine_color(): + """Apply cosmetic color to submarine based on upgrade level""" + # Don't apply color during intro mission (friend submarine) + if GameState.is_intro() or is_friend_submarine: + return + + var color_level = GameState.upgrades[GameState.Upgrade.SUBMARINE_COLOR] + if color_level == 0: + return # No color upgrade + + # Define color palette for different levels + var colors = [ + Color(1.0, 0.2, 0.2), # Red + Color(0.2, 1.0, 0.2), # Green + Color(0.2, 0.2, 1.0), # Blue + Color(1.0, 1.0, 0.2), # Yellow + Color(1.0, 0.2, 1.0), # Magenta + Color(0.2, 1.0, 1.0), # Cyan + Color(1.0, 0.5, 0.2), # Orange + Color(0.5, 0.2, 1.0), # Purple + Color(1.0, 0.8, 0.2), # Gold + Color(0.8, 0.8, 0.8), # Silver + ] + + var selected_color = colors[min(color_level - 1, colors.size() - 1)] + + # Apply color to submarine material + if has_node("Pivot/SmFishSubmarine"): + var submarine_mesh = $Pivot/SmFishSubmarine + var material = submarine_mesh.get_surface_override_material(0) + + if material == null: + # Create a new material if none exists + material = StandardMaterial3D.new() + # Try to load original textures + material.albedo_texture = load("res://textures/sub/SM_FishSubmarine_initialShadingGroup_BaseColor.png") + material.metallic_texture = load("res://textures/sub/SM_FishSubmarine_initialShadingGroup_Metallic.png") + material.roughness_texture = load("res://textures/sub/SM_FishSubmarine_initialShadingGroup_Roughness.png") + material.normal_texture = load("res://textures/sub/SM_FishSubmarine_initialShadingGroup_Normal.png") + material.height_texture = load("res://textures/sub/SM_FishSubmarine_initialShadingGroup_Height.png") + + # Apply color tint + material.albedo_color = selected_color + submarine_mesh.set_surface_override_material(0, material) + print("Applied submarine color level ", color_level, ": ", selected_color) diff --git a/scripts/strings.gd b/scripts/strings.gd index baca71c..7ca2965 100644 --- a/scripts/strings.gd +++ b/scripts/strings.gd @@ -25,6 +25,8 @@ var upgradeNames = { GameState.Upgrade.SURFACE_BUOY: "Emergency Buoy", GameState.Upgrade.INVENTORY_SAVE: "Inventory Insurance", GameState.Upgrade.DRONE_SELLING: "Remote Selling Drone", + GameState.Upgrade.SUBMARINE_COLOR: "Submarine Color", + GameState.Upgrade.DRONE_COLOR: "Drone Color", } var boss_dialog_lines = { @@ -58,5 +60,7 @@ var upgradeDescriptions = { GameState.Upgrade.INVENTORY_MANAGEMENT: "Automatically replaces less valuable fish with more expensive ones when your inventory is full.", GameState.Upgrade.SURFACE_BUOY: "Provides an emergency buoy that quickly returns you to the surface when you press B.", GameState.Upgrade.INVENTORY_SAVE: "Insures your inventory, preventing loss of collected fish when you die.", - GameState.Upgrade.DRONE_SELLING: "Deploys a drone that sells your inventory remotely when you press Q, without returning to dock." + GameState.Upgrade.DRONE_SELLING: "Deploys a drone that sells your inventory remotely when you press Q, without returning to dock.", + GameState.Upgrade.SUBMARINE_COLOR: "Changes the color of your submarine. Each level unlocks a new color variant. Only affordable by exploring the depths below the boss!", + GameState.Upgrade.DRONE_COLOR: "Changes the color of your selling drone. Each level unlocks a new color variant. Only affordable by exploring the depths below the boss!" } diff --git a/scripts/ui/game_mode_selection.gd b/scripts/ui/game_mode_selection.gd new file mode 100644 index 0000000..564f9af --- /dev/null +++ b/scripts/ui/game_mode_selection.gd @@ -0,0 +1,135 @@ +extends Control + +signal mode_selected(mode: GameState.GameMode) + +var story_button: Button +var infinite_button: Button +var title_label: Label +var description_label: Label + +func _ready(): + # Create main container + var main_container = VBoxContainer.new() + main_container.set_anchors_preset(Control.PRESET_FULL_RECT) + main_container.add_theme_constant_override("separation", 30) + add_child(main_container) + + # Create background panel + var background = PanelContainer.new() + background.set_anchors_preset(Control.PRESET_FULL_RECT) + var panel_style = StyleBoxFlat.new() + panel_style.bg_color = Color(0.05, 0.1, 0.2, 0.95) + panel_style.set_border_width_all(4) + panel_style.border_color = Color(0.3, 0.5, 0.8) + panel_style.set_corner_radius_all(15) + background.add_theme_stylebox_override("panel", panel_style) + main_container.add_child(background) + + var margin = MarginContainer.new() + margin.add_theme_constant_override("margin_left", 50) + margin.add_theme_constant_override("margin_right", 50) + margin.add_theme_constant_override("margin_top", 50) + margin.add_theme_constant_override("margin_bottom", 50) + background.add_child(margin) + + var content = VBoxContainer.new() + content.add_theme_constant_override("separation", 40) + margin.add_child(content) + + # Title + title_label = Label.new() + title_label.text = "SELECT GAME MODE" + title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + title_label.add_theme_font_size_override("font_size", 36) + title_label.add_theme_color_override("font_color", Color(1, 1, 1)) + content.add_child(title_label) + + # Description + description_label = Label.new() + description_label.text = "Choose how you want to play" + description_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + description_label.add_theme_font_size_override("font_size", 20) + description_label.add_theme_color_override("font_color", Color(0.8, 0.8, 0.9)) + content.add_child(description_label) + + # Button container + var button_container = HBoxContainer.new() + button_container.add_theme_constant_override("separation", 30) + button_container.alignment = BoxContainer.ALIGNMENT_CENTER + content.add_child(button_container) + + # Story Mode Button + story_button = create_mode_button("STORY MODE", "Complete the main story and rescue your friend from the blobfish boss.") + story_button.pressed.connect(func(): on_mode_selected(GameState.GameMode.NORMAL)) + button_container.add_child(story_button) + + # Infinite Mode Button + infinite_button = create_mode_button("INFINITE MODE", "Continue diving past the boss! Fish values scale down the deeper you go.") + infinite_button.pressed.connect(func(): on_mode_selected(GameState.GameMode.INFINITE)) + button_container.add_child(infinite_button) + +func create_mode_button(text: String, description: String) -> Button: + var button_container = VBoxContainer.new() + button_container.custom_minimum_size = Vector2(300, 200) + button_container.add_theme_constant_override("separation", 15) + + var button = Button.new() + button.text = text + button.custom_minimum_size = Vector2(300, 80) + button.add_theme_font_size_override("font_size", 24) + + # Style the button + var button_style = StyleBoxFlat.new() + button_style.bg_color = Color(0.2, 0.4, 0.7) + button_style.set_border_width_all(3) + button_style.border_color = Color(0.4, 0.6, 0.9) + button_style.set_corner_radius_all(10) + button.add_theme_stylebox_override("normal", button_style) + + var button_hover_style = StyleBoxFlat.new() + button_hover_style.bg_color = Color(0.3, 0.5, 0.8) + button_hover_style.set_border_width_all(3) + button_hover_style.border_color = Color(0.5, 0.7, 1.0) + button_hover_style.set_corner_radius_all(10) + button.add_theme_stylebox_override("hover", button_hover_style) + button.add_theme_stylebox_override("pressed", button_hover_style) + + button.add_theme_color_override("font_color", Color(1, 1, 1)) + button_container.add_child(button) + + var desc_label = Label.new() + desc_label.text = description + desc_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + desc_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + desc_label.add_theme_font_size_override("font_size", 16) + desc_label.add_theme_color_override("font_color", Color(0.9, 0.9, 0.9)) + button_container.add_child(desc_label) + + # Wrap in a container to return the button but keep the structure + var wrapper = Control.new() + wrapper.add_child(button_container) + button_container.set_anchors_preset(Control.PRESET_FULL_RECT) + + # Store reference to button for easy access + button_container.set_meta("button", button) + + return button + +func on_mode_selected(mode: GameState.GameMode): + emit_signal("mode_selected", mode) + visible = false + + if mode == GameState.GameMode.NORMAL: + GameState.start_normal_mode() + elif mode == GameState.GameMode.INFINITE: + GameState.start_infinite_mode() + +func show_menu(): + visible = true + GameState.paused = true + get_tree().paused = true + +func hide_menu(): + visible = false + GameState.paused = false + get_tree().paused = false diff --git a/scripts/upgrades.gd b/scripts/upgrades.gd index 2306f7b..5614c0b 100644 --- a/scripts/upgrades.gd +++ b/scripts/upgrades.gd @@ -25,6 +25,10 @@ var categories = { GameState.Upgrade.SURFACE_BUOY, GameState.Upgrade.INVENTORY_SAVE, GameState.Upgrade.DRONE_SELLING + ], + "COSMETIC": [ + GameState.Upgrade.SUBMARINE_COLOR, + GameState.Upgrade.DRONE_COLOR ] } @@ -284,6 +288,15 @@ func on_upgrade_pressed(key, info_label): # First update this button's state update_button_state(key) + # Handle cosmetic upgrades + if key == GameState.Upgrade.SUBMARINE_COLOR or key == GameState.Upgrade.DRONE_COLOR: + if GameState.player_node: + if key == GameState.Upgrade.SUBMARINE_COLOR: + GameState.player_node.apply_submarine_color() + elif key == GameState.Upgrade.DRONE_COLOR: + # Drone color will be applied when drone is spawned + pass + # Then update affordability of all other buttons since money has changed for upgrade_key in buttons: if upgrade_key != key: # Skip the one we just updated @@ -558,6 +571,12 @@ func _setup_upgrade_menu(): continue filtered_equipment.append(item) + # Filter COSMETIC category to only show if boss has been defeated + var filtered_cosmetic = [] + if GameState.is_boss_defeated(): + for item in categories["COSMETIC"]: + filtered_cosmetic.append(item) + # Create categories var equipment_column = create_category_column("EQUIPMENT", filtered_equipment) var performance_column = create_category_column("PERFORMANCE", categories["PERFORMANCE"]) @@ -567,6 +586,11 @@ func _setup_upgrade_menu(): content_container.add_child(performance_column) content_container.add_child(utility_column) + # Add cosmetic column if boss is defeated + if filtered_cosmetic.size() > 0: + var cosmetic_column = create_category_column("COSMETIC", filtered_cosmetic) + content_container.add_child(cosmetic_column) + # Initialize money display update_money_display()