extends CharacterBody2D class_name Player const ACTION_AREA_UPDATE_TIME=0.05 # When creating an action_zone, we make sure that the area setup correctly by waiting a little const MAX_REACH = 100 const HOLDING_ITEM_SPRITE_SIZE = 20. const TURN_ANIMATION_MINIMUM_THRESHOLD = 0.2 const SPEED = 350 const JUST_DROPPED_ITEM_UPDATE_INTERVAL = 1. signal player_updated(player: Player) signal upgraded var terrain : Terrain @export var region : Region var data : PlayerData var last_action_area_movement_timer : float = 100. var controlling_player : bool = false : set(v): controlling_player = v velocity = Vector2.ZERO var instruction : Instruction = null : set(i): if instruction and is_node_ready(): instruction.abort(self) instruction = i if instruction and is_node_ready(): instruction.spawn_indicator(self) var just_dropped_item_objects : Array = [] var last_just_dropped_item_objects_updated := 0. var elapsed_time := 0. @onready var preview_zone : ActionZone = await setup_action_zone(Vector2.ZERO, null) @onready var action_zone : ActionZone = await setup_action_zone(Vector2.ZERO, null) func _ready(): data = GameInfo.game_data.player_data data.updated.connect(_on_data_changed) data.inventory.updated.connect(_on_inventory_updated) player_updated.emit(self) Pointer.player = self setup_preview_zone(data.inventory.get_item()) %NoEnergyLeftIcon.visible = data.energy == 0 func appear(with_falling_animation = true): if with_falling_animation: %AnimationPlayer.play("fall") await %AnimationPlayer.animation_finished controlling_player = true %AnimationPlayer.play("float") func _input(_event) -> void: if not Pointer.dragging_inspected: if Input.is_action_pressed("change_item_left"): data.inventory.change_current_item(1) if Input.is_action_pressed("change_item_right"): data.inventory.change_current_item(-1) for i in range(1, 10): if Input.is_action_pressed("item_" + str(i)): data.inventory.set_current_item(i - 1) # Méthode déclenchée par la classe region func _start_pass_day(): controlling_player = false instruction = null # Méthode déclenchée par la classe region func _pass_day(): full_recharge() # Méthode déclenchée par la classe region func _end_pass_day(): controlling_player = true func _process(delta): elapsed_time += delta last_action_area_movement_timer += delta if controlling_player: var input_direction : Vector2 = calculate_direction_input_direction() if ( last_action_area_movement_timer >= ACTION_AREA_UPDATE_TIME and instruction and instruction.can_be_done(self) ): instruction.do(self) instruction = null if instruction and instruction.need_movement: if input_direction.length() != 0: instruction = null input_direction = calculate_direction_instruction_direction() if instruction == null and action_zone: action_zone.destroy() action_zone = null velocity = input_direction * SPEED turn_animate(input_direction) move_preview_zone(get_global_mouse_position()) else: velocity = Vector2.ZERO if velocity == Vector2.ZERO and %MovementAudioStreamPlayer.playing == true: %MovementAudioStreamPlayer.stop() elif velocity != Vector2.ZERO and %MovementAudioStreamPlayer.playing == false: %MovementAudioStreamPlayer.play() # print("-----") # print(elapsed_time) # print(last_just_dropped_item_objects_updated) if elapsed_time > last_just_dropped_item_objects_updated + JUST_DROPPED_ITEM_UPDATE_INTERVAL: update_just_dropped_item_objects() if GameInfo.settings_data.auto_pickup: take_surrounding_seeds() move_and_slide() func _on_data_changed(pd : PlayerData): %NoEnergyLeftIcon.visible = pd.energy == 0 func _on_inventory_updated(_inventory: Inventory): setup_preview_zone(data.inventory.get_item()) emit_signal("player_updated", self) func update_just_dropped_item_objects(): last_just_dropped_item_objects_updated = elapsed_time var overlapping_areas = (%InteractArea2D as Area2D).get_overlapping_areas() just_dropped_item_objects = just_dropped_item_objects.filter( func (i : ItemObject): return i in overlapping_areas ) func take_surrounding_seeds(): var overlapping_areas = (%InteractArea2D as Area2D).get_overlapping_areas() if not data.inventory.is_full(): for area in overlapping_areas: if ( area is ItemObject and not area in just_dropped_item_objects and not Pointer.dragging_inspected ): area.interact(self) return func calculate_direction_instruction_direction() -> Vector2: if ( instruction and ( instruction.position.distance_to(global_position) > (MAX_REACH - 1.) or instruction is MoveInstruction ) ): if %NavigationAgent2D.target_position != instruction.position: %NavigationAgent2D.target_position = instruction.position return to_local(%NavigationAgent2D.get_next_path_position()).normalized() # return self.global_position.direction_to(instruction.position) return Vector2.ZERO func calculate_direction_input_direction() -> Vector2: return Input.get_vector("move_left", "move_right", "move_up", "move_down") func turn_animate(input_direction): if input_direction.x > TURN_ANIMATION_MINIMUM_THRESHOLD: if input_direction.y > TURN_ANIMATION_MINIMUM_THRESHOLD: %PlayerSprite.wanted_orientation = PlayerSprite.ROrient.FRONT_RIGHT elif input_direction.y < -TURN_ANIMATION_MINIMUM_THRESHOLD: %PlayerSprite.wanted_orientation = PlayerSprite.ROrient.BACK_RIGHT else: %PlayerSprite.wanted_orientation = PlayerSprite.ROrient.RIGHT elif input_direction.x < -TURN_ANIMATION_MINIMUM_THRESHOLD: if input_direction.y > TURN_ANIMATION_MINIMUM_THRESHOLD: %PlayerSprite.wanted_orientation = PlayerSprite.ROrient.FRONT_LEFT elif input_direction.y < -TURN_ANIMATION_MINIMUM_THRESHOLD: %PlayerSprite.wanted_orientation = PlayerSprite.ROrient.BACK_LEFT else: %PlayerSprite.wanted_orientation = PlayerSprite.ROrient.LEFT else: if input_direction.y > TURN_ANIMATION_MINIMUM_THRESHOLD: %PlayerSprite.wanted_orientation = PlayerSprite.ROrient.FRONT elif input_direction.y < -TURN_ANIMATION_MINIMUM_THRESHOLD: %PlayerSprite.wanted_orientation = PlayerSprite.ROrient.BACK func can_interact(interactable : Interactable): return interactable.can_interact(self) func try_interact(interactable : Interactable): if interactable: instruction = InteractableInstruction.new( interactable ) func try_move(move_to : Vector2): instruction = MoveInstruction.new(move_to) func can_pick_item(_item: Item): return true func pick_item(item : Item): if item.type != Item.ItemType.TOOL_ITEM && data.inventory.is_full(): await drop_item() AudioManager.play_sfx("PickUp") data.inventory.add_item(item) # Save after a timer to let the time to the item to disappear get_tree().create_timer(0.1).timeout.connect(region.save) func drop_item(): var ind_to_drop := data.inventory.current_item_ind if ( data.inventory.get_item(ind_to_drop) == null or ind_to_drop < len(data.inventory.tools) ): var possible_ind : Array = range( len(data.inventory.tools), len(data.inventory.tools) + data.inventory.seeds_size ).filter( func (i): return data.inventory.get_item(i) != null ) if len(possible_ind): ind_to_drop = possible_ind.pop_back() else: return var item_to_drop : Item = data.inventory.pop_item(ind_to_drop) if item_to_drop and item_to_drop.type != Item.ItemType.TOOL_ITEM: var dropped_item_object = terrain.drop_item(item_to_drop, global_position) just_dropped_item_objects.append(dropped_item_object) AudioManager.play_sfx("Drop") region.save() func try_use_item(item : Item, use_position : Vector2): await setup_action_zone(use_position, item) instruction = ItemActionInstruction.new( use_position, item ) func preview_could_use_item(item : Item) -> bool: return could_use_item_on_zone(item, preview_zone) func could_use_item_on_zone(item : Item, zone: ActionZone) -> bool: return ( data.inventory.has_item(item) and item.can_use(self, zone) ) func can_use_item_on_zone(item : Item, zone: ActionZone) -> bool: return ( could_use_item_on_zone(item, zone) and has_energy_to_use_item(item) ) func has_energy_to_use_item(item : Item): return (data.energy - item.energy_usage) >= 0 func use_item(item : Item): if can_use_item_on_zone(item, action_zone): var is_item_used = await item.use(self, action_zone) if is_item_used: data.energy -= item.energy_usage if item.is_one_time_use(): data.inventory.remove_item(item) get_tree().create_timer(0.1).timeout.connect(region.save) func upgrade_max_energy(amount = 1): data.max_energy += amount upgraded.emit() player_updated.emit(self) func upgrade_inventory_size(amount = 1): data.inventory.change_size(amount) upgraded.emit() player_updated.emit(self) func recharge(amount : int = data.max_energy): data.energy += + amount upgraded.emit() func full_recharge(): data.energy = max(data.energy, data.max_energy) func generate_action_zone(item : Item, preview = true) -> ActionZone: var zone = ActionZone.new(item, region.plant_grid, preview) if not get_parent().is_node_ready(): await get_parent().ready get_parent().add_child(zone.area) return zone func setup_preview_zone(item : Item): if preview_zone and preview_zone.item == item: return preview_zone elif preview_zone: preview_zone.destroy() preview_zone = null if item: preview_zone = await generate_action_zone(item, true) func setup_action_zone(zone_position : Vector2, item: Item) -> ActionZone: if action_zone: action_zone.destroy() action_zone = await generate_action_zone(item, false) action_zone.move_to_position(zone_position) last_action_area_movement_timer = 0. return action_zone func move_preview_zone(zone_position : Vector2): if preview_zone: preview_zone.move_to_position(zone_position) preview_zone.update_preview(self) class Instruction: const INDICATOR_COLOR := Color("#96B3DB") var position : Vector2 var need_movement : bool = true var indicator := Sprite2D.new() func _init(_pos : Vector2): position = _pos func can_be_done(player : Player): return player.global_position.distance_to(position) < player.MAX_REACH func do(_player : Player): pass func indicator_texture(): return preload("res://common/icons/map-pin.svg") func indicator_size(): return 40 func indicator_shift(): return Vector2.ZERO func spawn_indicator(player : Player): indicator.texture = indicator_texture() indicator.texture = indicator_texture() indicator.scale = Vector2( 1./(float(indicator_texture().get_width())/indicator_size()), 1./(float(indicator_texture().get_height())/indicator_size()) ) indicator.modulate = Color( INDICATOR_COLOR.r, INDICATOR_COLOR.g, INDICATOR_COLOR.b, 0. ) player.get_parent().add_child(indicator) indicator.global_position = position + indicator_shift() player.get_tree().create_tween().tween_property(indicator, "modulate:a", 0.8, 0.2) func abort(player : Player): indicator.queue_free() class MoveInstruction extends Instruction: func indicator_shift(): return Vector2.UP * 50 func can_be_done(player : Player): return player.global_position.distance_to(position) < 10. class ItemActionInstruction extends Instruction: var item : Item func _init(_pos : Vector2, _item : Item): position = _pos item = _item need_movement = item.is_usage_need_proximity() func can_be_done(player : Player): return ( not item.is_usage_need_proximity() or player.global_position.distance_to(position) < player.MAX_REACH ) func indicator_texture(): return item.icon func do(player : Player): player.use_item(item) class InteractableInstruction extends Instruction: var interactable : Interactable func _init(_interactable : Interactable): interactable = _interactable position = interactable.global_position func can_be_done(player : Player): return player.global_position.distance_to(position) < player.MAX_REACH func indicator_texture(): return preload("res://common/icons/hand-grab.svg") func do(player : Player): interactable.interact(player) class ActionZone: const ZONE_ACTIVATED_COLOR = Color("#96B3DB") const ZONE_DEACTIVATED_COLOR = Color("#FF006E") const ZONE_OPACITY = 0.6 var item : Item = null var area : Area2D = Area2D.new() var affected_areas : Array[Area2D]= [] var plant_grid : PlantGrid var circle : Circle var preview: bool func _init(_i : Item, _grid : PlantGrid, _preview = false): item = _i plant_grid = _grid preview = _preview if item and item.get_usage_zone_radius() > 0: area = Area2D.new() var collision_shape = CollisionShape2D.new() var circle_shape = CircleShape2D.new() circle_shape.radius = item.get_usage_zone_radius() collision_shape.shape = circle_shape area.add_child(collision_shape) circle = Circle.new( item.get_usage_zone_radius(), ) circle.fill = preview circle.modulate.a = ZONE_OPACITY area.add_child(circle) circle.z_index = 100 func clear_preview_on_affected_area(): for a in affected_areas: if a: a.affect_preview(false) func update_preview(player : Player): update_preview_on_affected_area() if circle: circle.color = ZONE_ACTIVATED_COLOR if item.can_use(player, self) else ZONE_DEACTIVATED_COLOR func update_preview_on_affected_area(): var detected_areas = get_affected_areas() clear_preview_on_affected_area() var new_affected_areas : Array[Area2D] = [] for a in detected_areas: if a is Area2D and item.get_usage_object_affected(a) and a.has_method("affect_preview"): a.affect_preview(true) new_affected_areas.append(a) affected_areas = new_affected_areas func get_affected_areas() -> Array[Area2D]: var empty_array : Array[Area2D] = [] return empty_array if area == null else area.get_overlapping_areas() func destroy(): clear_preview_on_affected_area() if area: area.queue_free() func get_global_position() -> Vector2: return Vector2.ZERO if area == null else area.global_position func move_to_position(pos : Vector2): if area and plant_grid: if item and item.snap_usage_to_grid(): area.global_position = plant_grid.get_point_for_tile( Math.get_tile_from_pos(pos) ) else: area.global_position = pos func get_tiles() -> Array[Vector2i]: return Math.get_tiles_in_circle( get_global_position(), item.get_usage_zone_radius() )