Deleted ESCHotspot (use ESCItem instead)

Moved duplicated movement code from ESCPlayer and ESCItem/ESCHotspot to its own script.
Added talking animations management.
Fixed bug: character was turning to last_direction after talking, if coming from another direction than speaking direction.
Continued removing unfree stuff.
This commit is contained in:
Julian Murgia
2021-01-12 23:05:23 +01:00
parent 933122f085
commit ff56816205
73 changed files with 1211 additions and 1652 deletions

View File

@@ -13,9 +13,7 @@ func _enter_tree():
load("res://addons/escoria-core/game/core-scripts/escbackground.gd"), null)
add_custom_type("ESCCharacter", "KinematicBody2D",
load("res://addons/escoria-core/game/core-scripts/esccharacter.gd"), null)
add_custom_type("ESCHotspot", "Area2D",
load("res://addons/escoria-core/game/core-scripts/eschotspot.gd"), null)
add_custom_type("ESCItem", "Sprite",
add_custom_type("ESCItem", "Area2D",
load("res://addons/escoria-core/game/core-scripts/escitem.gd"), null)
add_custom_type("ESCItemsInventory", "GridContainer",
load("res://addons/escoria-core/game/core-scripts/items_inventory.gd"), null)
@@ -148,7 +146,6 @@ func remove_autoloads():
func _exit_tree():
remove_custom_type("ESCBackground")
remove_custom_type("ESCCharacter")
remove_custom_type("ESCHotspot")
remove_custom_type("ESCItem")
remove_custom_type("ESCInventoryItem")
remove_custom_type("ESCItemsInventory")

View File

@@ -0,0 +1,4 @@
extends Node
func _ready():
pass

View File

@@ -0,0 +1,273 @@
tool
extends Node
class_name Movable
"""
This class performs the moving (walk, teleport, terrain scaling...) actions on
the parent node.
"""
onready var parent = get_parent()
# If character misses an animation, bypass it and proceed.
onready var bypass_missing_animation = false
func _ready():
parent.add_user_signal("arrived")
func _process(time):
if Engine.is_editor_hint():
return
if parent.task == parent.PLAYER_TASKS.WALK or parent.task == parent.PLAYER_TASKS.SLIDE:
var pos = parent.get_position()
var old_pos = pos
var next
if parent.walk_path.size() > 1:
next = parent.walk_path[parent.path_ofs + 1]
else:
next = parent.walk_path[parent.path_ofs]
var dist = parent.speed * time * pow(parent.last_scale.x, 2) * \
parent.terrain.player_speed_multiplier
if parent.walk_context and "fast" in parent.walk_context and parent.walk_context.fast:
dist *= parent.terrain.player_doubleclick_speed_multiplier
var dir = (next - pos).normalized()
# assume that x^2 + y^2 == 1, apply v_speed_damp the y axis
#printt("dir before", dir)
dir = dir * (dir.x * dir.x + dir.y * dir.y * parent.v_speed_damp)
#printt("dir after", dir, dist)
var new_pos
if pos.distance_to(next) < dist:
new_pos = next
parent.path_ofs += 1
else:
new_pos = pos + dir * dist
if parent.path_ofs >= parent.walk_path.size() - 1:
walk_stop(parent.walk_destination)
return
pos = new_pos
var angle = (old_pos.angle_to_point(pos))
parent.set_position(pos)
if parent.task == parent.PLAYER_TASKS.WALK:
parent.last_deg = escoria.utils._get_deg_from_rad(angle)
parent.last_dir = _get_dir_deg(parent.last_deg, parent.animations)
var current_animation = ""
if parent.animation_sprite != null:
current_animation = parent.animation_sprite.animation
# elif animation != null:
# current_animation = animation.current_animation
bypass_missing_animation = false
if !bypass_missing_animation:
var animation_to_play = parent.animations.directions[parent.last_dir][0]
if current_animation != animation_to_play:
if parent.animation_sprite.frames.has_animation(animation_to_play):
parent.animation_sprite.play(animation_to_play)
else:
bypass_missing_animation = true
current_animation = animation_to_play
escoria.report_warnings("movable.gd:_process()",
["Character " + parent.global_id + " has no animation " + animation_to_play,
"Bypassing missing animation and proceed movement."])
parent.pose_scale = parent.animations.directions[parent.last_dir][1]
update_terrain()
else:
parent.moved = false
set_process(false)
func teleport(target, angle : Object = null) -> void:
"""
Teleports the item on target position.
target can be Vector2 or Object
"""
if typeof(target) == TYPE_VECTOR2:
printt("Item teleported at position", target, "with angle", angle)
parent.position = target
elif typeof(target) == TYPE_OBJECT:
if target.get("interact_positions") != null:
parent.position = target.interact_positions.default #.global_position
else:
parent.position = target.position
printt("Item teleported at", target.name, "position", parent.position, "with angle", angle)
else:
escoria.report_errors("escitem.gd:teleport()", ["Target to teleport to is null or unusable (" + target + ")"])
# PUBLIC FUNCTION
func walk_to(pos : Vector2, p_walk_context = null):
if not parent.terrain:
return walk_stop(parent.get_position())
if parent.interact_status == parent.INTERACT_STATES.INTERACT_WALKING:
return
if parent.interact_status == parent.INTERACT_STATES.INTERACT_STARTED:
parent.interact_status = parent.INTERACT_STATES.INTERACT_WALKING
parent.walk_path = parent.terrain.get_terrain_path(parent.get_position(), pos)
parent.walk_context = p_walk_context
if parent.walk_path.size() == 0:
parent.task = parent.PLAYER_TASKS.NONE
walk_stop(parent.get_position())
set_process(false)
return
parent.moved = true
parent.walk_destination = parent.walk_path[parent.walk_path.size()-1]
if parent.terrain.is_solid(pos):
parent.walk_destination = parent.walk_path[parent.walk_path.size()-1]
parent.path_ofs = 0.0
parent.task = parent.PLAYER_TASKS.WALK
set_process(true)
# PRIVATE FUNCTION
func walk(target_pos, p_speed, context = null):
if p_speed:
parent.orig_speed = parent.speed
parent.speed = p_speed
walk_to(target_pos, context)
# PRIVATE FUNCTION
func walk_stop(pos):
parent.position = pos
parent.interact_status = parent.INTERACT_STATES.INTERACT_NONE
parent.walk_path = []
if parent.orig_speed:
parent.speed = parent.orig_speed
parent.orig_speed = 0.0
parent.task = parent.PLAYER_TASKS.NONE
parent.moved = false
set_process(false)
if parent.params_queue != null && !parent.params_queue.empty():
if parent.animations.dir_angles.size() > 0:
if parent.arams_queue[0].interact_angle == -1:
escoria.tools.resolve_angle_to(parent.params_queue[0])
else:
parent.last_dir = _get_dir_deg(parent.params_queue[0].interact_angle, parent.animations)
parent.animation_sprite.play(parent.animations.idles[parent.last_dir][0])
parent.pose_scale = parent.animations.idles[parent.last_dir][1]
update_terrain()
else:
parent.animation_sprite.play(parent.animations.idles[parent.last_dir][0])
parent.pose_scale = parent.animations.idles[parent.last_dir][1]
get_tree().call_group_flags(SceneTree.GROUP_CALL_DEFAULT, "game", "interact", parent.params_queue)
# Clear params queue to prevent the same action from being triggered again
parent.params_queue = []
else:
# If we're heading to an object and reached its interaction position,
# orient towards the defined interaction direction set on the object (if any)
if parent.walk_context.has("target_object") and parent.walk_context.target_object.player_orients_on_arrival \
and escoria.esc_runner.get_interactive(parent.walk_context.target_object.global_id):
var orientation = parent.walk_context["target_object"].interaction_direction
parent.last_dir = orientation
parent.animation_sprite.play(parent.animations.idles[orientation][0])
parent.pose_scale = parent.animations.idles[orientation][1]
else:
parent.animation_sprite.play(parent.animations.idles[parent.last_dir][0])
parent.pose_scale = parent.animations.idles[parent.last_dir][1]
update_terrain()
if parent.walk_context != null:
# escoria.esc_level_runner.finished(walk_context)
escoria.esc_level_runner.finished()
parent.walk_context = null
parent.emit_signal("arrived")
func update_terrain(on_event_finished_name = null):
if !parent.terrain or parent.terrain == null or !is_instance_valid(parent.terrain):
return
if on_event_finished_name != null and on_event_finished_name != "setup":
return
if parent.get("is_exit"):
return
var pos = parent.position
parent.z_index = pos.y if pos.y <= VisualServer.CANVAS_ITEM_Z_MAX else VisualServer.CANVAS_ITEM_Z_MAX
var color
if parent.terrain_is_scalenodes:
parent.last_scale = parent.terrain.get_terrain(pos)
parent.scale = parent.last_scale
elif parent.check_maps:
color = parent.terrain.get_terrain(pos)
var scal = parent.terrain.get_scale_range(color.b)
if scal != parent.get_scale():
parent.last_scale = scal
parent.scale = parent.last_scale
# Do not flip the entire player character, because that would conflict
# with shadows that expect to be siblings of $texture
if parent.pose_scale == -1 and parent.get_node("sprite").scale.x > 0:
parent.get_node("sprite").scale.x *= parent.pose_scale
parent.collision.scale.x *= parent.pose_scale
elif parent.pose_scale == 1 and parent.get_node("sprite").scale.x < 0:
parent.get_node("sprite").scale.x *= -1
parent.collision.scale.x *= -1
# if parent.check_maps:
# color = parent.terrain.get_light(pos)
#
# if color:
# for s in sprites:
# s.set_modulate(color)
func _get_dir(angle : float, animations) -> int:
var deg = escoria.utils._get_deg_from_rad(angle)
return _get_dir_deg(deg, animations)
func _get_dir_deg(deg : int, animations) -> int:
# We turn the angle by -90° because angle_to_point gives the angle against X axis, not Y
deg = wrapi(deg - 90, 0, 360)
var dir = -1
var i = 0
for arr_angle_zone in animations.dir_angles:
if is_angle_in_interval(deg, arr_angle_zone):
dir = i
break
else:
i += 1
continue
# It's an error to have the animations misconfigured
if dir == -1:
escoria.report_errors("escitem.gd:_get_dir_deg()", ["No direction found for " + str(deg)])
return dir
"""
Returns true if given angle is inside the interval given by a starting_angle and the size.
@param angle : Angle to test
@param: interval : Array of size 2, containing the starting angle, and the size of interval
 eg: [90, 40] corresponds to angle between 90° and 130°
"""
func is_angle_in_interval(angle: float, interval : Array) -> bool:
angle = wrapi(angle, 0, 360)
if angle == 0:
angle = 360
var start_angle = wrapi(interval[0], 0, 360)
var angle_area = interval[1]
var end_angle = wrapi(interval[0] + angle_area, 0, 360)
if (angle >= 270 and angle <= 360) or (angle >= 0 and angle <= 90):
if wrapi(angle+180, 0, 360) > wrapi(interval[0]+ 180, 0, 360) \
&& wrapi(angle+180, 0, 360) <= wrapi(interval[0] + angle_area + 180, 0, 360):
return true
else:
if wrapi(angle, 0, 360) > start_angle && wrapi(angle, 0, 360) <= end_angle:
return true
return false

View File

@@ -449,7 +449,10 @@ func set_angle(command_params : Array):
if !escoria.esc_runner.check_obj(command_params[0], "set_angle"):
return esctypes.EVENT_LEVEL_STATE.RETURN
var obj = escoria.esc_runner.get_object(command_params[0])
obj.set_angle(int(command_params[1]))
# HACK Countering the fact that angle_to_point() function gives
# angle against X axis not Y, we need to check direction using (angle-90°).
# Since the ESC command already gives the right angle, we add 90.
obj.set_angle(int(command_params[1] + 90))
return esctypes.EVENT_LEVEL_STATE.RETURN

View File

@@ -1,148 +0,0 @@
tool
extends Area2D
class_name ESCHotspot
func get_class():
return "ESCHotspot"
"""
ESCHotspot is an Area2D (hotspot).
A hotspot is a simple area that can be defined by the user and is thus invisible
Usually, hotspots are used to define areas of the background that the player can
look at.
"""
signal mouse_entered_hotspot(global_id)
signal mouse_exited_hotspot
signal mouse_left_clicked_hotspot(global_id, click_position)
signal mouse_double_left_clicked_hotspot(global_id, click_position)
signal mouse_right_clicked_hotspot(global_id, click_position)
export(String) var global_id
export(bool) var is_exit
export(String, FILE, "*.esc") var esc_script
export(bool) var is_interactive = true
export(bool) var player_orients_on_arrival = true
export(ESCPlayer.Directions) var interaction_direction
export(String) var tooltip_name
export(String) var default_action
# If action used by player is in the list, game will wait for a second click on another item
# to combine objects together (typical USE <X> WITH <Y>, GIVE <X> TO <Y>)
export(PoolStringArray) var combine_if_action_used_among = []
export(Color) var dialog_color = ColorN("white")
# Detected interact position set by a Position2D node OUTSIDE OF THE HOTSPOT SCENE.
# You have to add a child to the INSTANCED HOTSPOT SCENE, IN THE ROOM SCENE.
export(Dictionary) var interact_positions : Dictionary = { "default": null}
var collision
var terrain : ESCTerrain
# If the terrain node type is scalenodes
var terrain_is_scalenodes : bool
var check_maps = true
var pose_scale : int
var last_scale : Vector2
func _ready():
if !Engine.is_editor_hint():
escoria.register_object(self)
connect("mouse_entered_hotspot", escoria.inputs_manager, "_on_mouse_entered_hotspot")
connect("mouse_exited_hotspot", escoria.inputs_manager, "_on_mouse_exited_hotspot")
connect("mouse_left_clicked_hotspot", escoria.inputs_manager, "_on_mouse_left_clicked_hotspot")
connect("mouse_right_clicked_hotspot", escoria.inputs_manager, "_on_mouse_right_clicked_hotspot")
connect("mouse_entered", self, "_on_mouse_entered")
connect("mouse_exited", self, "_on_mouse_exited")
connect("input_event", self, "manage_input")
init_interact_position_with_node()
terrain = escoria.room_terrain
update_terrain()
func init_interact_position_with_node():
"""
Initialize the interact_position attribute by searching for a Position2D
node in children nodes.
If any is found, the first one is used as interaction position with this hotspot.
If none is found, we use the CollisionShape2D or CollisionPolygon2D child node's
position instead.
"""
for c in get_children():
if c is Position2D:
# If the position2D node is part of the hotspot, it means it is not an interact position
# but a dialog position for example. Interact position node must be set in the room scene.
if c.get_owner() == self:
continue
interact_positions.default = c.global_position
break
if c is CollisionShape2D or c is CollisionPolygon2D:
interact_positions.default = c.global_position
func manage_input(viewport : Viewport, event : InputEvent, shape_idx : int):
if event is InputEventMouseButton:
# var p = get_global_mouse_position()
if event.doubleclick:
if event.button_index == BUTTON_LEFT:
emit_signal("mouse_double_left_clicked_hotspot", global_id, event)
else:
if event.is_pressed():
if event.button_index == BUTTON_LEFT:
emit_signal("mouse_left_clicked_hotspot", global_id, event)
if event.button_index == BUTTON_RIGHT:
emit_signal("mouse_right_clicked_hotspot", global_id, event)
func _on_mouse_entered():
emit_signal("mouse_entered_hotspot", global_id)
func _on_mouse_exited():
emit_signal("mouse_exited_hotspot")
func get_item_child_if_any():
for c in get_children():
if c is ESCItem:
return c
func update_terrain(on_event_finished_name = null):
if !terrain:
return
if on_event_finished_name != null and on_event_finished_name != "setup":
return
if is_exit:
return
var pos = position
z_index = pos.y if pos.y <= VisualServer.CANVAS_ITEM_Z_MAX else VisualServer.CANVAS_ITEM_Z_MAX
var color
if terrain_is_scalenodes:
last_scale = terrain.get_terrain(pos)
self.scale = last_scale
elif check_maps:
color = terrain.get_terrain(pos)
var scal = terrain.get_scale_range(color.b)
if scal != get_scale():
last_scale = scal
self.scale = last_scale
# Do not flip the entire player character, because that would conflict
# with shadows that expect to be siblings of $"sprite"
if pose_scale == -1 and $"sprite".scale.x > 0:
$"sprite".scale.x *= pose_scale
collision.scale.x *= pose_scale
elif pose_scale == 1 and $"sprite".scale.x < 0:
$"sprite".scale.x *= -1
collision.scale.x *= -1
# if check_maps:
# color = terrain.get_light(pos)
#
# if color:
# for s in sprites:
# s.set_modulate(color)

View File

@@ -1,5 +1,5 @@
tool
extends Sprite
extends Area2D
class_name ESCItem
func get_class():
@@ -15,6 +15,9 @@ signal mouse_left_clicked_item(global_id)
signal mouse_double_left_clicked_item(global_id)
signal mouse_right_clicked_item(global_id)
var Movable
var MovableScript = load("res://addons/escoria-core/game/core-scripts/behaviors/movable.gd")
export(String) var global_id
export(String, FILE, "*.esc") var esc_script
# If true, the ESC script may have an ":exit_scene" event to manage scene changes
@@ -37,12 +40,15 @@ export(bool) var use_from_inventory_only = false
# Scene used in inventory for the object if it is picked up
export(PackedScene) var inventory_item_scene_file : PackedScene
export(Color) var dialog_color = ColorN("white")
# Detected interact position set by a Position2D node OUTSIDE OF THE SCENE.
# You have to add a child to the INSTANCED SCENE, IN THE ROOM SCENE.
export(Dictionary) var interact_positions : Dictionary = { "default": null}
# Animation node (null if none was found)
var animation_sprite
onready var interact_positions : Dictionary = { "default": null}
# Animations script (for walking, idling...)
export(Script) var animations
@@ -84,9 +90,7 @@ var task # type PLAYER_TASKS
var params_queue : Array
# PRIVATE VARS
var area : Area2D
# Size of the item
var size : Vector2
var last_deg : int
@@ -94,6 +98,11 @@ var last_dir : int
func _ready():
# Adds movable behavior
Movable = Node.new()
Movable.set_script(MovableScript)
add_child(Movable)
for n in get_children():
if n is AnimatedSprite:
animation_sprite = n
@@ -101,14 +110,10 @@ func _ready():
if n is AnimationPlayer:
animation_sprite = n
continue
if n is Area2D:
area = n
continue
if area:
area.connect("mouse_entered", self, "_on_mouse_entered")
area.connect("mouse_exited", self, "_on_mouse_exited")
area.connect("input_event", self, "manage_input")
connect("mouse_entered", self, "_on_mouse_entered")
connect("mouse_exited", self, "_on_mouse_exited")
connect("input_event", self, "manage_input")
init_interact_position_with_node()
terrain = escoria.room_terrain
@@ -121,71 +126,9 @@ func _ready():
connect("mouse_double_left_clicked_item", escoria.inputs_manager, "_on_mouse_left_double_clicked_item")
connect("mouse_right_clicked_item", escoria.inputs_manager, "_on_mouse_right_clicked_item")
update_terrain()
func _process(time):
if Engine.is_editor_hint():
return
if task == PLAYER_TASKS.WALK or task == PLAYER_TASKS.SLIDE:
var pos = get_position()
var old_pos = pos
var next
if walk_path.size() > 1:
next = walk_path[path_ofs + 1]
else:
next = walk_path[path_ofs]
var dist = speed * time * pow(last_scale.x, 2) * terrain.player_speed_multiplier
if walk_context and "fast" in walk_context and walk_context.fast:
dist *= terrain.player_doubleclick_speed_multiplier
var dir = (next - pos).normalized()
# assume that x^2 + y^2 == 1, apply v_speed_damp the y axis
#printt("dir before", dir)
dir = dir * (dir.x * dir.x + dir.y * dir.y * v_speed_damp)
#printt("dir after", dir, dist)
var new_pos
if pos.distance_to(next) < dist:
new_pos = next
path_ofs += 1
else:
new_pos = pos + dir * dist
if path_ofs >= walk_path.size() - 1:
walk_stop(walk_destination)
return
pos = new_pos
var angle = (old_pos.angle_to_point(pos))
set_position(pos)
if task == PLAYER_TASKS.WALK:
last_deg = escoria.utils._get_deg_from_rad(angle)
last_dir = _get_dir_deg(last_deg, animations)
var current_animation = ""
if animation_sprite != null:
current_animation = animation_sprite.animation
# elif animation != null:
# current_animation = animation.current_animation
if current_animation != animations.directions[last_dir][0]:
animation_sprite.play(animations.directions[last_dir][0])
pose_scale = animations.directions[last_dir][1]
update_terrain()
else:
moved = false
set_process(false)
if !is_exit:
last_scale = scale
Movable.update_terrain()
func get_animation_player():
@@ -204,6 +147,7 @@ If none is found, we use the CollisionShape2D or CollisionPolygon2D child node's
position instead.
"""
func init_interact_position_with_node():
interact_positions.default = null
for c in get_children():
if c is Position2D:
# If the position2D node is part of the hotspot, it means it is not an interact position
@@ -211,9 +155,11 @@ func init_interact_position_with_node():
if c.get_owner() == self:
continue
interact_positions.default = c.global_position
break
if c is CollisionShape2D or c is CollisionPolygon2D:
interact_positions.default = c.global_position
collision = c
if interact_positions.default == null:
interact_positions.default = c.global_position
func manage_input(viewport : Viewport, event : InputEvent, shape_idx : int):
@@ -236,193 +182,22 @@ func _on_mouse_entered():
func _on_mouse_exited():
emit_signal("mouse_exited_item")
func update_terrain(on_event_finished_name = null):
if !terrain or terrain == null or !is_instance_valid(terrain):
return
if on_event_finished_name != null and on_event_finished_name != "setup":
return
if is_exit:
return
var pos = position
var gpos = global_position
z_index = pos.y if pos.y <= VisualServer.CANVAS_ITEM_Z_MAX else VisualServer.CANVAS_ITEM_Z_MAX
var color
if terrain_is_scalenodes:
last_scale = terrain.get_terrain(pos)
self.scale = last_scale
elif check_maps:
color = terrain.get_terrain(pos)
var scal = terrain.get_scale_range(color.b)
if scal != get_scale():
last_scale = scal
self.scale = last_scale
# Do not flip the entire player character, because that would conflict
# with shadows that expect to be siblings of $texture
if pose_scale == -1 and $texture.scale.x > 0:
$texture.scale.x *= pose_scale
collision.scale.x *= pose_scale
elif pose_scale == 1 and $texture.scale.x < 0:
$texture.scale.x *= -1
collision.scale.x *= -1
# if check_maps:
# color = terrain.get_light(pos)
#
# if color:
# for s in sprites:
# s.set_modulate(color)
################################################################################
func teleport(target, angle : Object = null) -> void:
"""
Teleports the item on target position.
target can be Vector2 or Object
"""
if typeof(target) == TYPE_VECTOR2:
printt("Item teleported at position", target, "with angle", angle)
position = target
elif typeof(target) == TYPE_OBJECT:
if target.get("interact_positions") != null:
position = target.interact_positions.default #.global_position
else:
position = target.position
printt("Item teleported at", target.name, "position", position, "with angle", angle)
else:
escoria.report_errors("escitem.gd:teleport()", ["Target to teleport to is null or unusable (" + target + ")"])
Movable.teleport(target, angle)
# PUBLIC FUNCTION
func walk_to(pos : Vector2, p_walk_context = null):
if not terrain:
return walk_stop(get_position())
if interact_status == INTERACT_STATES.INTERACT_WALKING:
return
if interact_status == INTERACT_STATES.INTERACT_STARTED:
interact_status = INTERACT_STATES.INTERACT_WALKING
walk_path = terrain.get_terrain_path(get_position(), pos)
walk_context = p_walk_context
if walk_path.size() == 0:
task = PLAYER_TASKS.NONE
walk_stop(get_position())
set_process(false)
return
moved = true
walk_destination = walk_path[walk_path.size()-1]
if terrain.is_solid(pos):
walk_destination = walk_path[walk_path.size()-1]
path_ofs = 0.0
task = PLAYER_TASKS.WALK
set_process(true)
# PRIVATE FUNCTION
func walk(target_pos, p_speed, context = null):
if p_speed:
orig_speed = speed
speed = p_speed
walk_to(target_pos, context)
# PRIVATE FUNCTION
func walk_stop(pos):
position = pos
interact_status = INTERACT_STATES.INTERACT_NONE
walk_path = []
if orig_speed:
speed = orig_speed
orig_speed = 0.0
task = PLAYER_TASKS.NONE
moved = false
set_process(false)
if params_queue != null && !params_queue.empty():
if animations.dir_angles.size() > 0:
if params_queue[0].interact_angle == -1:
escoria.tools.resolve_angle_to(params_queue[0])
else:
last_dir = _get_dir_deg(params_queue[0].interact_angle, animations)
animation_sprite.play(animations.idles[last_dir][0])
pose_scale = animations.idles[last_dir][1]
update_terrain()
else:
animation_sprite.play(animations.idles[last_dir][0])
pose_scale = animations.idles[last_dir][1]
get_tree().call_group_flags(SceneTree.GROUP_CALL_DEFAULT, "game", "interact", params_queue)
# Clear params queue to prevent the same action from being triggered again
params_queue = []
else:
# If we're heading to an object and reached its interaction position,
# orient towards the defined interaction direction set on the object (if any)
if walk_context.has("target_object") and walk_context.target_object.player_orients_on_arrival \
and escoria.esc_runner.get_interactive(walk_context.target_object.global_id):
var orientation = walk_context["target_object"].interaction_direction
animation_sprite.play(animations.idles[orientation][0])
pose_scale = animations.idles[orientation][1]
else:
animation_sprite.play(animations.idles[last_dir][0])
pose_scale = animations.idles[last_dir][1]
update_terrain()
if walk_context != null:
# escoria.esc_level_runner.finished(walk_context)
escoria.esc_level_runner.finished()
walk_context = null
emit_signal("arrived")
func _get_dir(angle : float, animations) -> int:
var deg = escoria.utils._get_deg_from_rad(angle)
return _get_dir_deg(deg, animations)
func _get_dir_deg(deg : int, animations) -> int:
# We turn the angle by -90° because angle_to_point gives the angle against X axis, not Y
deg = wrapi(deg - 90, 0, 360)
var dir = -1
var i = 0
for arr_angle_zone in animations.dir_angles:
if is_angle_in_interval(deg, arr_angle_zone):
dir = i
break
else:
i += 1
continue
# It's an error to have the animations misconfigured
if dir == -1:
escoria.report_errors("escitem.gd:_get_dir_deg()", ["No direction found for " + str(deg)])
return dir
"""
Returns true if given angle is inside the interval given by a starting_angle and the size.
@param angle : Angle to test
@param: interval : Array of size 2, containing the starting angle, and the size of interval
 eg: [90, 40] corresponds to angle between 90° and 130°
"""
func is_angle_in_interval(angle: float, interval : Array) -> bool:
angle = wrapi(angle, 0, 360)
if angle == 0:
angle = 360
var start_angle = wrapi(interval[0], 0, 360)
var angle_area = interval[1]
var end_angle = wrapi(interval[0] + angle_area, 0, 360)
if (angle >= 270 and angle <= 360) or (angle >= 0 and angle <= 90):
if wrapi(angle+180, 0, 360) > wrapi(interval[0]+ 180, 0, 360) \
&& wrapi(angle+180, 0, 360) <= wrapi(interval[0] + angle_area + 180, 0, 360):
return true
else:
if wrapi(angle, 0, 360) > start_angle && wrapi(angle, 0, 360) <= end_angle:
return true
return false
Movable.walk_to(pos, p_walk_context)
func start_talking():
# if animation_sprite.is_playing():
# animation_sprite.stop()
# animation_sprite.play(animations.speaks[last_dir][0])
pass
func stop_talking():
# if animation_sprite.is_playing():
# animation_sprite.stop()
pass

View File

@@ -5,7 +5,6 @@ const OBJ_DEFAULT_STATE = "default"
## Custom nodes:
#var ESCBackground = preload("res://addons/escoria-core/game/core-scripts/escbackground.gd")
#var ESCCharacter = preload("res://addons/escoria-core/game/core-scripts/esccharacter.gd")
#var ESCHotspot = preload("res://addons/escoria-core/game/core-scripts/eschotspot.gd")
#var ESCItem = preload("res://addons/escoria-core/game/core-scripts/escitem.gd")
#var ESCItemsInventory = preload("res://addons/escoria-core/game/core-scripts/items_inventory.gd")
#var ESCInventoryItem = preload("res://addons/escoria-core/game/core-scripts/inventory_item.gd")

View File

@@ -5,7 +5,15 @@ class_name ESCPlayer
func get_class():
return "ESCPlayer"
signal arrived
"""
TODO
- Currently the sprite node needs to be named "sprite". This is bad.
- Animation management doesn't allow using AnimationPlayer yet. Need to find
the best solution to manage both AnimatedSprite and AnimationPlayer.
"""
var Movable : Node
var MovableScript = load("res://addons/escoria-core/game/core-scripts/behaviors/movable.gd")
export var global_id : String
@@ -87,9 +95,15 @@ export(NodePath) var camera_position_node
func _ready():
# Adds movable behavior
Movable = Node.new()
Movable.set_script(MovableScript)
add_child(Movable)
# Connect the player to the event_done signal, so we can react to a finished
# ":setup" event. In this case, we need to run update_terrain()
escoria.esc_runner.connect("event_done", self, "update_terrain")
escoria.esc_runner.connect("event_done", Movable, "update_terrain")
# assert(is_angle_in_interval(0, [340,40])) # true
# assert(is_angle_in_interval(359, [340,40])) # true
@@ -125,7 +139,6 @@ func _ready():
return
terrain = escoria.room_terrain
last_scale = scale
set_process(true)
@@ -136,272 +149,153 @@ func _process(time):
return
$debug.text = str(z_index)
if task == PLAYER_TASKS.WALK or task == PLAYER_TASKS.SLIDE:
var pos = get_position()
var old_pos = pos
var next
if walk_path.size() > 1:
next = walk_path[path_ofs + 1]
else:
next = walk_path[path_ofs]
var dist = speed * time * pow(last_scale.x, 2) * terrain.player_speed_multiplier
if walk_context and "fast" in walk_context and walk_context.fast:
dist *= terrain.player_doubleclick_speed_multiplier
var dir = (next - pos).normalized()
# assume that x^2 + y^2 == 1, apply v_speed_damp the y axis
#printt("dir before", dir)
dir = dir * (dir.x * dir.x + dir.y * dir.y * v_speed_damp)
#printt("dir after", dir, dist)
var new_pos
if pos.distance_to(next) < dist:
new_pos = next
path_ofs += 1
else:
new_pos = pos + dir * dist
if path_ofs >= walk_path.size() - 1:
walk_stop(walk_destination)
return
pos = new_pos
var angle = (old_pos.angle_to_point(pos))
set_position(pos)
if task == PLAYER_TASKS.WALK:
last_deg = escoria.utils._get_deg_from_rad(angle)
last_dir = _get_dir_deg(last_deg, animations)
var current_animation = ""
if animation_sprite != null:
current_animation = animation_sprite.animation
# elif animation != null:
# current_animation = animation.current_animation
if current_animation != animations.directions[last_dir][0]:
animation_sprite.play(animations.directions[last_dir][0])
pose_scale = animations.directions[last_dir][1]
update_terrain()
else:
moved = false
set_process(false)
func update_terrain(on_event_finished_name = null):
if !terrain:
return
if on_event_finished_name != null and on_event_finished_name != "setup":
return
var pos = position
z_index = pos.y if pos.y <= VisualServer.CANVAS_ITEM_Z_MAX else VisualServer.CANVAS_ITEM_Z_MAX
var color
if terrain_is_scalenodes:
last_scale = terrain.get_terrain(pos)
self.scale = last_scale
elif check_maps:
color = terrain.get_terrain(pos)
var scal = terrain.get_scale_range(color.b)
if scal != get_scale():
last_scale = scal
self.scale = last_scale
# Do not flip the entire player character, because that would conflict
# with shadows that expect to be siblings of $"sprite"
if pose_scale == -1 and $"sprite".scale.x > 0:
$"sprite".scale.x *= pose_scale
collision.scale.x *= pose_scale
elif pose_scale == 1 and $"sprite".scale.x < 0:
$"sprite".scale.x *= -1
collision.scale.x *= -1
# if check_maps:
# color = terrain.get_light(pos)
# if task == PLAYER_TASKS.WALK or task == PLAYER_TASKS.SLIDE:
# var pos = get_position()
# var old_pos = pos
# var next
# if walk_path.size() > 1:
# next = walk_path[path_ofs + 1]
# else:
# next = walk_path[path_ofs]
#
# if color:
# for s in sprites:
# s.set_modulate(color)
# var dist = speed * time * pow(last_scale.x, 2) * terrain.player_speed_multiplier
# if walk_context and "fast" in walk_context and walk_context.fast:
# dist *= terrain.player_doubleclick_speed_multiplier
# var dir = (next - pos).normalized()
#
# # assume that x^2 + y^2 == 1, apply v_speed_damp the y axis
# #printt("dir before", dir)
# dir = dir * (dir.x * dir.x + dir.y * dir.y * v_speed_damp)
# #printt("dir after", dir, dist)
#
# var new_pos
# if pos.distance_to(next) < dist:
# new_pos = next
# path_ofs += 1
# else:
# new_pos = pos + dir * dist
#
# if path_ofs >= walk_path.size() - 1:
# walk_stop(walk_destination)
# return
#
# pos = new_pos
#
# var angle = (old_pos.angle_to_point(pos))
# set_position(pos)
#
# if task == PLAYER_TASKS.WALK:
# last_deg = escoria.utils._get_deg_from_rad(angle)
# last_dir = _get_dir_deg(last_deg, animations)
#
# var current_animation = ""
# if animation_sprite != null:
# current_animation = animation_sprite.animation
## elif animation != null:
## current_animation = animation.current_animation
#
# if current_animation != animations.directions[last_dir][0]:
# animation_sprite.play(animations.directions[last_dir][0])
#
# pose_scale = animations.directions[last_dir][1]
#
# update_terrain()
# else:
# moved = false
# set_process(false)
#func update_terrain(on_event_finished_name = null):
# if !terrain:
# return
# if on_event_finished_name != null and on_event_finished_name != "setup":
# return
#
# var pos = position
# z_index = pos.y if pos.y <= VisualServer.CANVAS_ITEM_Z_MAX else VisualServer.CANVAS_ITEM_Z_MAX
#
# var color
# if terrain_is_scalenodes:
# last_scale = terrain.get_terrain(pos)
# self.scale = last_scale
# elif check_maps:
# color = terrain.get_terrain(pos)
# var scal = terrain.get_scale_range(color.b)
# if scal != get_scale():
# last_scale = scal
# self.scale = last_scale
#
# # Do not flip the entire player character, because that would conflict
# # with shadows that expect to be siblings of $"sprite"
# if pose_scale == -1 and $"sprite".scale.x > 0:
# $"sprite".scale.x *= pose_scale
# collision.scale.x *= pose_scale
# elif pose_scale == 1 and $"sprite".scale.x < 0:
# $"sprite".scale.x *= -1
# collision.scale.x *= -1
#
## if check_maps:
## color = terrain.get_light(pos)
##
## if color:
## for s in sprites:
## s.set_modulate(color)
"""
Sets player angle and plays according animation.
- deg int angle to set the character
- immediate bool (currently unused, see TODO below)
If true, direction is switched immediately. Else, successive animations are
used so that the character turns to target angle.
TODO: depending on current angle and current angle, the character may directly turn around
with no "progression". We may enhance this by calculating successive directions to turn the
character to, so that he doesn't switch to opposite direction too fast.
For example, if character looks WEST and set_angle(EAST) is called, we may want the character
to first turn SOUTHWEST, then SOUTH, then SOUTHEAST and finally EAST, all more or less fast.
Whatever the implementation, this should be activated using "parameter "immediate" set to false.
"""
func set_angle(deg):
func set_angle(deg : int, immediate = true):
if deg < 0 or deg > 360:
escoria.report_errors("escplayer.gd:set_angle()", ["Invalid degree to turn to " + str(deg)])
moved = true
last_deg = deg
last_dir = _get_dir_deg(deg, animations)
last_dir = Movable._get_dir_deg(deg, animations)
# The player may have a state animation from before, which would be
# resumed, so we immediately force the correct idle animation
if animation_sprite.animation != animations.idles[last_dir][0]:
animation_sprite.play(animations.idles[last_dir][0])
pose_scale = animations.idles[last_dir][1]
update_terrain()
"""
Teleports the player on target position.
target can be Vector2 or Object
"""
func teleport(target, angle : Object = null) -> void:
if typeof(target) == TYPE_VECTOR2:
printt("Player teleported at position", target, "with angle", angle)
position = target
elif typeof(target) == TYPE_OBJECT:
if target.get("interact_positions") != null:
position = target.interact_positions.default #.global_position
else:
position = target.position
printt("Player teleported at", target.name, "position", position, "with angle", angle)
else:
escoria.report_errors("escplayer.gd", ["target to teleport player to is null or unusable (" + target + ")"])
# PUBLIC FUNCTION
func walk_to(pos : Vector2, p_walk_context = null):
if not terrain:
return walk_stop(get_position())
if interact_status == INTERACT_STATES.INTERACT_WALKING:
return
if interact_status == INTERACT_STATES.INTERACT_STARTED:
interact_status = INTERACT_STATES.INTERACT_WALKING
walk_path = terrain.get_terrain_path(get_position(), pos)
walk_context = p_walk_context
if walk_path.size() == 0:
task = PLAYER_TASKS.NONE
walk_stop(get_position())
set_process(false)
return
moved = true
walk_destination = walk_path[walk_path.size()-1]
if terrain.is_solid(pos):
walk_destination = walk_path[walk_path.size()-1]
path_ofs = 0.0
task = PLAYER_TASKS.WALK
set_process(true)
# PRIVATE FUNCTION
func walk(target_pos, p_speed, context = null):
if p_speed:
orig_speed = speed
speed = p_speed
walk_to(target_pos, context)
# PRIVATE FUNCTION
func walk_stop(pos):
position = pos
interact_status = INTERACT_STATES.INTERACT_NONE
walk_path = []
if orig_speed:
speed = orig_speed
orig_speed = 0.0
task = PLAYER_TASKS.NONE
moved = false
set_process(false)
if params_queue != null && !params_queue.empty():
if animations.dir_angles.size() > 0:
if params_queue[0].interact_angle == -1:
escoria.tools.resolve_angle_to(params_queue[0])
else:
last_dir = _get_dir_deg(params_queue[0].interact_angle, animations)
animation_sprite.play(animations.idles[last_dir][0])
pose_scale = animations.idles[last_dir][1]
update_terrain()
else:
animation_sprite.play(animations.idles[last_dir][0])
pose_scale = animations.idles[last_dir][1]
get_tree().call_group_flags(SceneTree.GROUP_CALL_DEFAULT, "game", "interact", params_queue)
# Clear params queue to prevent the same action from being triggered again
params_queue = []
else:
# If we're heading to an object and reached its interaction position,
# orient towards the defined interaction direction set on the object (if any)
if walk_context.has("target_object") and walk_context.target_object.player_orients_on_arrival \
and escoria.esc_runner.get_interactive(walk_context.target_object.global_id):
var orientation = walk_context["target_object"].interaction_direction
animation_sprite.play(animations.idles[orientation][0])
pose_scale = animations.idles[orientation][1]
else:
animation_sprite.play(animations.idles[last_dir][0])
pose_scale = animations.idles[last_dir][1]
update_terrain()
if walk_context != null:
escoria.esc_level_runner.finished(walk_context)
walk_context = null
emit_signal("arrived")
Movable.update_terrain()
func anim_finished():
pass
func get_camera_pos():
if camera_position_node and get_node(camera_position_node):
return get_node(camera_position_node).global_position
return global_position
func get_animations_list() -> PoolStringArray:
return animation_sprite.get_sprite_frames().get_animation_names()
func start_talking():
if animation_sprite.is_playing():
animation_sprite.stop()
animation_sprite.play(animations.speaks[last_dir][0])
func _get_dir(angle : float, animations) -> int:
var deg = escoria.utils._get_deg_from_rad(angle)
return _get_dir_deg(deg, animations)
func stop_talking():
if animation_sprite.is_playing():
animation_sprite.stop()
animation_sprite.play(animations.idles[last_dir][0])
func _get_dir_deg(deg : int, animations) -> int:
# We turn the angle by -90° because angle_to_point gives the angle against X axis, not Y
deg = wrapi(deg - 90, 0, 360)
var dir = -1
var i = 0
for arr_angle_zone in animations.dir_angles:
if is_angle_in_interval(deg, arr_angle_zone):
dir = i
break
else:
i += 1
continue
# It's an error to have the animations misconfigured
if dir == -1:
escoria.report_errors("escplayer.gd:_get_dir_deg()", ["No direction found for " + str(deg)])
return dir
func teleport(target, angle : Object = null) -> void:
Movable.teleport(target, angle)
# Returns true if given angle is inside the interval given by a starting_angle and the size.
# @param angle : Angle to test
# @param: interval : Array of size 2, containing the starting angle, and the size of interval
# eg: [90, 40] corresponds to angle between 90° and 130°
func is_angle_in_interval(angle: float, interval : Array) -> bool:
angle = wrapi(angle, 0, 360)
if angle == 0:
angle = 360
var start_angle = wrapi(interval[0], 0, 360)
var angle_area = interval[1]
var end_angle = wrapi(interval[0] + angle_area, 0, 360)
if (angle >= 270 and angle <= 360) or (angle >= 0 and angle <= 90):
if wrapi(angle+180, 0, 360) > wrapi(interval[0]+ 180, 0, 360) \
&& wrapi(angle+180, 0, 360) <= wrapi(interval[0] + angle_area + 180, 0, 360):
return true
else:
if wrapi(angle, 0, 360) > start_angle && wrapi(angle, 0, 360) <= end_angle:
return true
return false
func walk_to(pos : Vector2, p_walk_context = null):
Movable.walk_to(pos, p_walk_context)

View File

@@ -151,14 +151,14 @@ func do(action : String, params : Array = []) -> void:
moving_obj.walk_to(target_position, walk_context)
"hotspot_left_click", "item_left_click":
"item_left_click":
if params[0] is String:
printt("escoria.do : item_left_click on item ", params[0])
# call : ev_left_click_on_item()
ev_left_click_on_item($esc_runner.get_object(params[0]), params[1])
"hotspot_right_click", "item_right_click":
"item_right_click":
if params[0] is String:
printt("escoria.do : item_right_click on item ", params[0])
@@ -257,6 +257,6 @@ func ev_left_click_on_item(obj, event, default_action = false):
else:
esc_runner.activate(esc_runner.current_action, [obj])
else:
# escoria.fallback("")
pass
# else:
## escoria.fallback("")
# pass

View File

@@ -31,30 +31,6 @@ func _on_right_click_on_bg(position : Vector2):
##################################################################################
func _on_mouse_entered_hotspot(hotspot_global_id : String) -> void:
printt("Hotspot focused : ", hotspot_global_id)
is_hotspot_focused = true
escoria.main.current_scene.game.element_focused(hotspot_global_id)
func _on_mouse_exited_hotspot() -> void:
print("Hotspot unfocused")
is_hotspot_focused = false
escoria.main.current_scene.game.element_unfocused()
func _on_mouse_left_clicked_hotspot(hotspot_global_id : String, event : InputEvent) -> void:
printt("Hotspot left clicked", hotspot_global_id, event)
escoria.main.current_scene.game.left_click_on_hotspot(hotspot_global_id, event)
func _on_mouse_right_clicked_hotspot(hotspot_global_id : String, event : InputEvent) -> void:
printt("Hotspot right clicked", hotspot_global_id, event)
escoria.main.current_scene.game.right_click_on_hotspot(hotspot_global_id, event)
func _on_mouse_left_double_clicked_hotspot(hotspot_global_id : String, event : InputEvent) -> void:
printt("Hotspot right clicked", hotspot_global_id, event)
escoria.main.current_scene.game.left_double_click_on_hotspot(hotspot_global_id, event)
##################################################################################
func _on_mouse_left_click_inventory_item(inventory_item_global_id, event : InputEvent) -> void:
printt("Inventory item left clicked ", inventory_item_global_id)
escoria.main.current_scene.game.left_click_on_inventory_item(inventory_item_global_id, event)

View File

@@ -166,10 +166,6 @@ func check_game_scene_methods():
assert(current_scene.game.has_method("element_focused"))
assert(current_scene.game.has_method("element_unfocused"))
assert(current_scene.game.has_method("left_click_on_hotspot"))
assert(current_scene.game.has_method("right_click_on_hotspot"))
assert(current_scene.game.has_method("left_double_click_on_hotspot"))
assert(current_scene.game.has_method("left_click_on_item"))
assert(current_scene.game.has_method("right_click_on_item"))
assert(current_scene.game.has_method("left_double_click_on_item"))

View File

@@ -48,6 +48,7 @@ func say(character : String, params : Dictionary):
dialog_ui = get_resource(params.ui).instance()
get_parent().add_child(dialog_ui)
dialog_ui.say(character, params)
func finish_fast():
dialog_ui.finish_fast()

View File

@@ -1,8 +1,8 @@
extends RichTextLabel
signal dialog_line_finished
#signal dialog_line_started
#signal dialog_line_finished
export(String) var current_character
onready var tween = $Tween
onready var text_node = self
@@ -10,6 +10,10 @@ export(float, 0.0, 0.3) var text_speed_per_character = 0.1
export(float) var fast_text_speed_per_character = 0.25
export(float) var max_time_to_text_disappear = 2.0
# Current character speaking, to keep track of reference for animation purposes
var current_character
func _ready():
bbcode_enabled = true
$Tween.connect("tween_completed", self, "_on_dialog_line_typed")
@@ -29,15 +33,17 @@ func say(character : String, params : Dictionary) :
return
# Position the RichTextLabel on the character's dialog position, if any.
var character_node = escoria.esc_runner.get_object(character)
rect_position = character_node.get_node("dialog_position").get_global_transform_with_canvas().origin
current_character = escoria.esc_runner.get_object(character)
rect_position = current_character.get_node("dialog_position").get_global_transform_with_canvas().origin
rect_position.x -= rect_size.x / 2
current_character.start_talking()
# Set text color to color set in the actor
var text_color = character_node.dialog_color
var text_color = current_character.dialog_color
var text_color_html = text_color.to_html(false)
text_node.bbcode_text = "[center][color=#" + text_color_html + "]".format(text_color_html) + params["line"] + "[/color][center]"
text_node.bbcode_text = "[center][color=#" + text_color_html + "]".format([text_color_html]) + params["line"] + "[/color][center]"
text_node.percent_visible = 0.0
var time_show_full_text = text_speed_per_character * len(params["line"])
@@ -61,7 +67,7 @@ func _on_dialog_line_typed(object, key):
$Timer.connect("timeout", self, "_on_dialog_finished")
func _on_dialog_finished():
# emit_signal("dialog_line_finished")
current_character.stop_talking()
escoria.esc_level_runner.finished()
escoria.dialog_player.is_speaking = false
escoria.current_state = escoria.GAME_STATE.DEFAULT

View File

@@ -2,13 +2,14 @@
[ext_resource path="res://addons/escoria-core/game/core-scripts/escterrain.gd" type="Script" id=1]
[ext_resource path="res://addons/escoria-core/game/core-scripts/escbackground.gd" type="Script" id=2]
[ext_resource path="res://addons/escoria-core/game/core-scripts/eschotspot.gd" type="Script" id=3]
[ext_resource path="res://addons/escoria-core/game/core-scripts/escitem.gd" type="Script" id=3]
[ext_resource path="res://addons/escoria-core/game/core-scripts/escroom.gd" type="Script" id=4]
[sub_resource type="NavigationPolygon" id=1]
[node name="room" type="Node2D"]
script = ExtResource( 4 )
camera_limits = [ Rect2( 0, 0, 1289, 555 ) ]
[node name="ESCBackground" type="TextureRect" parent="."]
margin_right = 40.0
@@ -27,9 +28,9 @@ navpoly = SubResource( 1 )
[node name="Hotspots" type="Node2D" parent="."]
[node name="ESCHotspot" type="Area2D" parent="Hotspots"]
[node name="ESCItem" type="Area2D" parent="Hotspots"]
script = ExtResource( 3 )
dialog_color = Color( 1, 1, 1, 1 )
[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="Hotspots/ESCHotspot"]
polygon = PoolVector2Array( 105, 65, 157, 47, 186, 118, 87, 134, 77, 87 )
interact_positions = {
"default": null
}