Files
seeding-planets/entities/player/scripts/player.gd

505 lines
16 KiB
GDScript

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 last_instruction_done := 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)
last_instruction_done = elapsed_time
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 and elapsed_time > last_instruction_done + 0.1:
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] = []
if item:
for a in item.get_usage_objects_affected(detected_areas, self):
if 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()
)