Documentation and Optimization Part 1 (#2)

Authored-by: Dennis Ploeger <develop@dieploegers.de>
This commit is contained in:
Dennis Ploeger
2021-06-16 10:12:51 +02:00
committed by GitHub
parent a22805d0e6
commit 4e09f522ff
109 changed files with 3556 additions and 2043 deletions

View File

@@ -1,63 +1,42 @@
# Plugin script to initialize Escoria
tool
extends EditorPlugin
# Autoloads to instantiate
const autoloads = {
"escoria": "res://addons/escoria-core/game/escoria.tscn",
"esctypes": "res://addons/escoria-core/game/core-scripts/escoria_types.gd"
}
# Custom types to generate outside of Classes
const custom_types = [
{
"type_name": "ESCBackground",
"parent_type": "Sprite",
"script_res": "res://addons/escoria-core/game/core-scripts/escbackground.gd"
},
{
"type_name": "ESCItem",
"parent_type": "Area2D",
"script_res": "res://addons/escoria-core/game/core-scripts/escitem.gd"
},
{
"type_name": "ESCItemsInventory",
"parent_type": "GridContainer",
"script_res": "res://addons/escoria-core/game/core-scripts/items_inventory.gd"
},
{
"type_name": "ESCInventoryItem",
"parent_type": "TextureButton",
"script_res": "res://addons/escoria-core/game/core-scripts/escinventoryitem.gd"
},
{
"type_name": "ESCPlayer",
"parent_type": "KinematicBody2D",
"script_res": "res://addons/escoria-core/game/core-scripts/escplayer.gd"
},
{
"type_name": "ESCRoom",
"parent_type": "Node2D",
"script_res": "res://addons/escoria-core/game/core-scripts/escroom.gd"
},
{
"type_name": "ESCTerrain",
"parent_type": "Navigation2D",
"script_res": "res://addons/escoria-core/game/core-scripts/escterrain.gd"
}
]
# Setup Escoria
func _enter_tree():
add_autoloads()
for key in autoloads.keys():
add_autoload_singleton(key, autoloads[key])
for custom_type in custom_types:
add_custom_type(custom_type.type_name, custom_type.parent_type,
load(custom_type.script_res), null)
# Prepare settings
set_escoria_main_settings()
set_escoria_debug_settings()
set_escoria_ui_settings()
set_escoria_internal_settings()
set_escoria_sound_settings()
set_escoria_platform_settings()
# Prepare the settings in the Escoria UI category
func set_escoria_ui_settings():
if !ProjectSettings.has_setting("escoria/ui/tooltip_follows_mouse"):
ProjectSettings.set_setting("escoria/ui/tooltip_follows_mouse", true)
@@ -110,8 +89,21 @@ func set_escoria_ui_settings():
"hint_string": "*.tscn, *.scn"
}
ProjectSettings.add_property_info(game_scene_property_info)
if !ProjectSettings.has_setting("escoria/ui/items_autoregister_path"):
ProjectSettings.set_setting(
"escoria/ui/items_autoregister_path",
"res://game/items/escitems/"
)
var game_scene_property_info = {
"name": "escoria/ui/items_autoregister_path",
"type": TYPE_STRING,
"hint": PROPERTY_HINT_DIR
}
ProjectSettings.add_property_info(game_scene_property_info)
# Prepare the settings in the Escoria main category
func set_escoria_main_settings():
if !ProjectSettings.has_setting("escoria/main/game_start_script"):
ProjectSettings.set_setting("escoria/main/game_start_script", "")
@@ -132,7 +124,7 @@ func set_escoria_main_settings():
ProjectSettings.add_property_info(force_quit_property_info)
ProjectSettings.set_setting("application/run/main_scene", "res://addons/escoria-core/game/main_scene.tscn")
if not ProjectSettings.has_setting("escoria/main/command_directories"):
ProjectSettings.set_setting("escoria/main/command_directories", [
"res://addons/escoria-core/game/core-scripts/esc/commands"
@@ -142,7 +134,7 @@ func set_escoria_main_settings():
"type": TYPE_ARRAY,
})
if !ProjectSettings.has_setting("escoria/main/text_lang"):
ProjectSettings.set_setting("escoria/main/text_lang", TranslationServer.get_locale())
var text_lang_property_info = {
@@ -151,7 +143,7 @@ func set_escoria_main_settings():
"hint": PROPERTY_HINT_NONE
}
ProjectSettings.add_property_info(text_lang_property_info)
if !ProjectSettings.has_setting("escoria/main/voice_lang"):
ProjectSettings.set_setting("escoria/main/voice_lang", TranslationServer.get_locale())
var voice_lang_property_info = {
@@ -160,13 +152,9 @@ func set_escoria_main_settings():
"hint": PROPERTY_HINT_NONE
}
ProjectSettings.add_property_info(voice_lang_property_info)
# Prepare the settings in the Escoria debug category
func set_escoria_debug_settings():
if !ProjectSettings.has_setting("escoria/debug/terminate_on_warnings"):
ProjectSettings.set_setting("escoria/debug/terminate_on_warnings", false)
@@ -190,6 +178,7 @@ func set_escoria_debug_settings():
ProjectSettings.add_property_info(property_info)
# Prepare the settings in the Escoria internal category
func set_escoria_internal_settings():
if !ProjectSettings.has_setting("escoria/internals/save_data"):
ProjectSettings.set_setting("escoria/internals/save_data", "")
@@ -202,6 +191,7 @@ func set_escoria_internal_settings():
ProjectSettings.add_property_info(save_data_property_info)
# Prepare the settings in the Escoria sound settings
func set_escoria_sound_settings():
if !ProjectSettings.has_setting("escoria/sound/master_volume"):
ProjectSettings.set_setting("escoria/sound/master_volume", 1)
@@ -212,7 +202,7 @@ func set_escoria_sound_settings():
"hint_string": "0,1"
}
ProjectSettings.add_property_info(master_data_property_info)
if !ProjectSettings.has_setting("escoria/sound/music_volume"):
ProjectSettings.set_setting("escoria/sound/music_volume", 1)
var music_data_property_info = {
@@ -242,37 +232,26 @@ func set_escoria_sound_settings():
"hint_string": "0,1"
}
ProjectSettings.add_property_info(speech_data_property_info)
# Defines platform-specific parameters. Those are the ones that must be re-set for each platform export.
# Prepare the settings in the Escoria platform category and may need special
# setting per build
func set_escoria_platform_settings():
# Skip cache - certain platforms (esp. mobile) lack memory for caching scenes
# If true, all generic scenes (UI, inventory, etc) will be loaded as any other scene.
# Skip cache - certain platforms (esp. mobile) lack memory for caching
# scenes.
# If set to true, all generic scenes (UI, inventory, etc) will be loaded
# as any other scene.
if !ProjectSettings.has_setting("escoria/platform/skip_cache"):
ProjectSettings.set_setting("escoria/platform/skip_cache", false)
if !ProjectSettings.has_setting("escoria/platform/skip_cache.mobile"):
ProjectSettings.set_setting("escoria/platform/skip_cache.mobile", true)
func add_autoloads():
for key in autoloads.keys():
add_autoload_singleton(key, autoloads[key])
func remove_autoloads():
# Uninstall plugin
func _exit_tree():
for key in autoloads.keys():
if ProjectSettings.has_setting(key):
remove_autoload_singleton(key)
func _exit_tree():
remove_custom_type("ESCBackground")
remove_custom_type("ESCItem")
remove_custom_type("ESCInventoryItem")
remove_custom_type("ESCItemsInventory")
remove_custom_type("ESCPlayer")
remove_custom_type("ESCRoom")
remove_custom_type("ESCTerrain")
remove_autoloads()
for custom_type in custom_types:
remove_custom_type(custom_type.type_name)

View File

@@ -0,0 +1,438 @@
# Node that performs the moving (walk, teleport, terrain scaling...) actions on
# its parent node.
tool
extends Node
class_name ESCMovable
# Tasks carried out by this walkable node
# NONE - The node is inactive
# WALK - The node walks the parent somewhere
# SLIDE - The node slides the parent somewhere
enum MovableTask {
NONE,
WALK,
SLIDE
}
# Character path through the scene as calculated by the Pathfinder
var walk_path: Array = []
# Current active walk path entry
var path_ofs: int
# The destination where the character should be moving to
var walk_destination: Vector2
# The walk context currently carried out by this movable node
var walk_context: ESCWalkContext = null
# Wether the character was moved at all
var moved: bool
# Angle degrees to the last position (TODO is that correct?)
var last_deg : int
# Direction of the last position (TODO is that correct?)
var last_dir : int
# Scale of the last position (TODO is that correct?)
var last_scale : Vector2
# TODO Isn't this actually the flip state of the current animation?
var pose_scale : int
var _orig_speed: float = 0.0
# Shortcut variable that references the node's parent
onready var parent = get_parent()
# If character misses an animation, bypass it and proceed.
onready var bypass_missing_animation = false
# Currenly running task
onready var task = MovableTask.NONE
# Add the signal "arrived" to the parent node, which is emitted when
# the destination position was reached
func _ready() -> void:
parent.add_user_signal("arrived")
# Main processing loop
#
# #### Parameters
#
# - delta: Time that has passed since the last call to this function
func _process(delta: float) -> void:
if Engine.is_editor_hint():
return
if task == MovableTask.WALK or \
task == MovableTask.SLIDE:
var pos = parent.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 = parent.speed * delta * pow(last_scale.x, 2) * \
parent.terrain.player_speed_multiplier
if walk_context.fast:
dist *= parent.terrain.player_doubleclick_speed_multiplier
var dir = (next - pos).normalized()
# TODO comment what this is all about
dir = dir * (dir.x * dir.x + dir.y * dir.y * parent.v_speed_damp)
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))
parent.set_position(pos)
if task == MovableTask.WALK:
last_deg = escoria.utils.get_deg_from_rad(angle)
last_dir = _get_dir_deg(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
# FIXME This is obviously wrong as bypass_missing_animation is
# always false
bypass_missing_animation = false
if !bypass_missing_animation:
var animation_to_play = \
parent.animations.directions[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.logger.report_warnings(
"movable.gd:_process()",
[
"Character %s has no animation %s "
% [parent.global_id, animation_to_play],
"Bypassing missing animation and " +
"proceed movement."
],
true
)
pose_scale = parent.animations.directions[last_dir][1]
update_terrain()
else:
moved = false
set_process(false)
# Teleports this item to the target position.
# TODO angle is only used for logging and has no further use, so it probably
# can be removed
#
# #### Parameters
#
# - target: Vector2, Position2d or ESCItem
func teleport(target, angle : Object = null) -> void:
if typeof(target) == TYPE_VECTOR2 :
escoria.logger.info(
"Object %s teleported at position %s with angle" %
[parent.global_id, str(target)],
[angle]
)
parent.position = target
elif target is Position2D:
escoria.logger.info(
"Object %s teleported at position %s with angle" %
[parent.global_id, str(target.position)],
[angle]
)
parent.position = target.position
elif typeof(target) == TYPE_OBJECT:
# FIXME this is better written as target is ESCItem if that's
# the only case here
# if target.get("interact_positions") != null:
# parent.position = target.interact_positions.default
# else:
# parent.position = target.position
parent.position = target.get_interact_position()
escoria.logger.info("Object " + target.name + " teleported at position "
+ str(parent.position) + " with angle ", str(angle))
else:
escoria.logger.report_errors("escitem.gd:teleport()",
["Target to teleport to is null or unusable (" + target + ")"])
# Walk to a given position
#
# #### Parameters
#
# - pos: Position to walk to
# - p_walk_context: Walk context to use
func walk_to(pos : Vector2, p_walk_context: ESCWalkContext = null) -> void:
if not parent.terrain:
walk_stop(parent.get_position())
return
if task == MovableTask.WALK:
if walk_context.target_object == p_walk_context.target_object \
or walk_context.target_position == p_walk_context.target_position:
walk_context.fast = p_walk_context.fast
walk_context = p_walk_context
if task == MovableTask.NONE:
task = MovableTask.WALK
walk_path = parent.terrain.get_simple_path(parent.get_position(), pos, true)
if walk_path.size() == 0:
task = MovableTask.NONE
walk_stop(parent.get_position())
set_process(false)
return
moved = true
walk_destination = walk_path[walk_path.size()-1]
path_ofs = 0
task = MovableTask.WALK
set_process(true)
# FIXME this function doesn't seem to be used anywhere
func walk(target_pos, p_speed, context = null) -> void:
if p_speed:
_orig_speed = parent.speed
parent.speed = p_speed
walk_to(target_pos, context)
# We have finished walking. Set the idle pose and complete
#
# #### Parameters
#
# - pos: Final target position
func walk_stop(pos: Vector2) -> void:
parent.position = pos
# parent.interact_status = parent.INTERACT_STATES.INTERACT_NONE
walk_path = []
if _orig_speed > 0:
parent.speed = _orig_speed
_orig_speed = 0.0
task = MovableTask.NONE
moved = false
set_process(false)
# 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.target_object and \
walk_context.target_object.node.player_orients_on_arrival and \
walk_context.target_object.interactive:
var orientation = walk_context.target_object.node.interaction_direction
last_dir = orientation
parent.animation_sprite.play(
parent.animations.idles[orientation][0]
)
pose_scale = parent.animations.idles[orientation][1]
else:
parent.animation_sprite.play(parent.animations.idles[last_dir][0])
pose_scale = parent.animations.idles[last_dir][1]
update_terrain()
yield(parent.animation_sprite, "animation_finished")
if walk_context.target_object:
escoria.logger.debug(
"%s arrived at %s" % [
parent.global_id,
walk_context.target_object.global_id
]
)
else:
escoria.logger.debug(
"%s arrived at %s" % [
parent.global_id,
walk_context.target_position
]
)
parent.emit_signal("arrived", walk_context)
# Update the sprite scale and lighting
#
# #### Parameters
#
# - on_event_finished_name: Used if this function is called from an ESC event
func update_terrain(on_event_finished_name = null) -> void:
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
if parent.get("dont_apply_terrain_scaling"):
return
var pos = parent.position
if pos.y <= VisualServer.CANVAS_ITEM_Z_MAX:
parent.z_index = pos.y
else:
parent.z_index = VisualServer.CANVAS_ITEM_Z_MAX
var factor = parent.terrain.get_terrain(pos)
var scal = parent.terrain.get_scale_range(factor)
if scal != parent.get_scale():
last_scale = scal
parent.scale = last_scale
var color = parent.terrain.get_light(pos)
parent.modulate = color
# Do not flip the entire character, because that would conflict
# with shadows that expect to be siblings of $texture
# TODO Make the character sprite not rely on the node name
if pose_scale == -1 and parent.get_node("sprite").scale.x > 0:
parent.get_node("sprite").scale.x *= pose_scale
parent.collision.scale.x *= pose_scale
elif pose_scale == 1 and parent.get_node("sprite").scale.x < 0:
parent.get_node("sprite").scale.x *= -1
parent.collision.scale.x *= -1
# Get the player direction index based on rotation angles
#
# FIXME: This function doesn't seem to be used anymore
# #### Parameters
#
# - angle: The rotation angle
# - animations: The list of character animations
func _get_dir(angle : float, animations) -> int:
var deg = escoria.utils.get_deg_from_rad(angle)
return _get_dir_deg(deg, animations)
# Get the player direction index based on degrees
#
# #### Parameters
#
# - deg: Degrees
# - animations: Player animations script
func _get_dir_deg(deg: int, animations: Script) -> 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.logger.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.
# TODO Refactor to make this stuff understandable :D
#
# #### Parameters
#
# - angle: Angle to test
# - 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)) \
and wrapi(angle + 180, 0, 360) > wrapi(interval[0] + 180, 0, 360) \
and wrapi(angle + 180, 0, 360) <= wrapi(
interval[0] + angle_area + 180, 0, 360
):
return true
elif wrapi(angle, 0, 360) > start_angle \
and wrapi(angle, 0, 360) <= end_angle:
return true
return false
# Sets character's angle and plays according animation.
#
# 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.
#
# #### Parameters
#
# - 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.
func set_angle(deg : int, immediate = true) -> void:
if deg < 0 or deg > 360:
escoria.logger.report_errors(
"movable.gd:set_angle()",
["Invalid degree to turn to " + str(deg)]
)
moved = true
last_deg = deg
last_dir = _get_dir_deg(deg, parent.animations)
# The character may have a state animation from before, which would be
# resumed, so we immediately force the correct idle animation
if parent.animation_sprite.animation != \
parent.animations.idles[last_dir][0]:
parent.animation_sprite.play(parent.animations.idles[last_dir][0])
pose_scale = parent.animations.idles[last_dir][1]
update_terrain()

View File

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

View File

@@ -1,334 +0,0 @@
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
var walk_path : Array = []
var walk_destination : Vector2
var walk_context
var moved : bool
var path_ofs : float
var last_deg : int
var last_dir : int
var last_scale : Vector2
var pose_scale : int
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 walk_path.size() > 1:
next = walk_path[path_ofs + 1]
else:
next = walk_path[path_ofs]
var dist = parent.speed * time * pow(last_scale.x, 2) * \
parent.terrain.player_speed_multiplier
if walk_context and "fast" in walk_context and 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
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))
parent.set_position(pos)
if parent.task == parent.PLAYER_TASKS.WALK:
last_deg = escoria.utils._get_deg_from_rad(angle)
last_dir = _get_dir_deg(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[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.logger.report_warnings("movable.gd:_process()",
["Character " + parent.global_id + " has no animation " + animation_to_play,
"Bypassing missing animation and proceed movement."], true)
pose_scale = parent.animations.directions[last_dir][1]
update_terrain()
else:
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 :
escoria.logger.info("Object " + parent.global_id + " teleported at position " +
str(target) + " with angle", [angle])
parent.position = target
elif target is Position2D:
escoria.logger.info("Object " + parent.global_id + " teleported at position " +
str(target.position) + " with angle", [angle])
parent.position = target.position
elif typeof(target) == TYPE_OBJECT:
# if target.get("interact_positions") != null:
# parent.position = target.interact_positions.default #.global_position
# else:
# parent.position = target.position
parent.position = target.get_interact_position()
escoria.logger.info("Object " + target.name + " teleported at position "
+ str(parent.position) + " with angle ", str(angle))
else:
escoria.logger.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.task == parent.PLAYER_TASKS.WALK:
if walk_context.has("target_object") and p_walk_context.has("target_object"):
if walk_context["target_object"] == p_walk_context["target_object"]:
walk_context["fast"] = p_walk_context["fast"]
return true
elif walk_context.has("target") and p_walk_context.has("target"):
if walk_context["target"] == p_walk_context["target"]:
walk_context["fast"] = p_walk_context["fast"]
return true
else:
pass
if parent.task == parent.PLAYER_TASKS.NONE:
parent.task = parent.PLAYER_TASKS.WALK
walk_path = parent.terrain.get_terrain_path(parent.get_position(), pos)
walk_context = p_walk_context
if walk_path.size() == 0:
parent.task = parent.PLAYER_TASKS.NONE
walk_stop(parent.get_position())
set_process(false)
return
moved = true
walk_destination = walk_path[walk_path.size()-1]
if parent.terrain.is_solid(pos):
walk_destination = walk_path[walk_path.size()-1]
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
walk_path = []
if parent.orig_speed:
parent.speed = parent.orig_speed
parent.orig_speed = 0.0
parent.task = parent.PLAYER_TASKS.NONE
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:
last_dir = _get_dir_deg(parent.params_queue[0].interact_angle, parent.animations)
parent.animation_sprite.play(parent.animations.idles[last_dir][0])
pose_scale = parent.animations.idles[last_dir][1]
update_terrain()
else:
parent.animation_sprite.play(parent.animations.idles[last_dir][0])
pose_scale = parent.animations.idles[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 walk_context.has("target_object") \
and walk_context.target_object.player_orients_on_arrival \
and escoria.object_manager.get_object(
walk_context.target_object.global_id
).interactive:
var orientation = walk_context["target_object"].interaction_direction
last_dir = orientation
parent.animation_sprite.play(parent.animations.idles[orientation][0])
pose_scale = parent.animations.idles[orientation][1]
else:
parent.animation_sprite.play(parent.animations.idles[last_dir][0])
pose_scale = parent.animations.idles[last_dir][1]
update_terrain()
yield(parent.animation_sprite, "animation_finished")
escoria.logger.info(parent.global_id + " arrived at " + str(walk_context))
parent.emit_signal("arrived", walk_context)
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
if parent.get("dont_apply_terrain_scaling"):
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:
last_scale = parent.terrain.get_terrain(pos)
parent.scale = 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():
last_scale = scal
parent.scale = last_scale
# Do not flip the entire character, because that would conflict
# with shadows that expect to be siblings of $texture
if pose_scale == -1 and parent.get_node("sprite").scale.x > 0:
parent.get_node("sprite").scale.x *= pose_scale
parent.collision.scale.x *= pose_scale
elif 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.logger.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
"""
Sets character's 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 : int, immediate = true):
if deg < 0 or deg > 360:
escoria.logger.report_errors("movable.gd:set_angle()", ["Invalid degree to turn to " + str(deg)])
moved = true
last_deg = deg
last_dir = _get_dir_deg(deg, parent.animations)
# The character may have a state animation from before, which would be
# resumed, so we immediately force the correct idle animation
if parent.animation_sprite.animation != parent.animations.idles[last_dir][0]:
parent.animation_sprite.play(parent.animations.idles[last_dir][0])
pose_scale = parent.animations.idles[last_dir][1]
update_terrain()

View File

@@ -1,9 +1,7 @@
# `teleport object1 object2 [angle]`
# `teleport object1 object2
#
# Sets the position of object1 to the position of object2. By default,
# object2's interact_angle is used to turn object1, but angle will override
# this. Useful for doors and such with an interact_angle you don't always want
# to adhere to when re-entering a room.
# Sets the position of object1 to the position of object2.
# FIXME re-add the angle parameter here
#
# @ESC
extends ESCBaseCommand
@@ -14,8 +12,8 @@ class_name TeleportCommand
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
2,
[TYPE_STRING, TYPE_STRING, TYPE_INT],
[null, null, null]
[TYPE_STRING, TYPE_STRING],
[null, null]
)
@@ -42,9 +40,8 @@ func validate(arguments: Array):
# Run the command
func run(command_params: Array) -> int:
escoria.object_manager.get_object(command_params[0]).node\
(escoria.object_manager.get_object(command_params[0]).node as ESCPlayer)\
.teleport(
escoria.object_manager.get_object(command_params[1]).node,
command_params[2]
escoria.object_manager.get_object(command_params[1]).node
)
return ESCExecution.RC_OK

View File

@@ -19,7 +19,10 @@ func load_command(command_name: String) -> ESCBaseCommand:
):
if ResourceLoader.exists("%s/%s.gd" % [command_directory, command_name]):
registry[command_name] = load(
"%s/%s.gd" % [command_directory, command_name]
"%s/%s.gd" % [
command_directory.trim_suffix("/"),
command_name
]
).new()
return registry[command_name]

View File

@@ -19,6 +19,11 @@ const RESERVED_GLOBALS = [
export(Dictionary) var _globals = {}
func _init():
set_global("ESC_LAST_SCENE", "", true)
# Check if a global was registered
#
# #### Parameters

View File

@@ -82,7 +82,7 @@ func is_valid() -> bool:
var command_found = false
for base_path in ProjectSettings.get("escoria/main/command_directories"):
var command_path = "%s/%s.gd" % [
base_path,
base_path.trim_suffix("/"),
self.name
]
if ResourceLoader.exists(command_path):

View File

@@ -1,49 +1,53 @@
# ESCBackground's purpose is to display a background image and receive input
# events on the background. More precisely, the TextureRect under ESCBackground
# does not receive events itself - if it did, it would also eat all events like
# hotspot focusing and such. Instead, we set the TextureRect mouse filter to
# MOUSE_FILTER_IGNORE, and we use an Area2D node to receive the input events.
#
# If ESCBackground doesn't contain a texture, it is important that its rect_size
# is set over the whole scene, because its rect_size is then used to create the
# Area2D node under it. If the rect_size is wrongly set, the background may
# receive no input.
tool
extends TextureRect
class_name ESCBackground
func get_class():
return "ESCBackground"
# The background was double clicked
#
# #### Parameters
#
# - position: The position where the player clicked
signal double_left_click_on_bg(position)
signal left_click_on_bg(position)
signal right_click_on_bg(position)
signal mouse_moved
# The background was left clicked
#
# #### Parameters
#
# - position: The position where the player clicked
signal left_click_on_bg(position)
# The background was right clicked
#
# #### Parameters
#
# - position: The position where the player clicked
signal right_click_on_bg(position)
# The ESC script connected to this background
export(String, FILE, "*.esc") var esc_script = ""
# Actual size of the scene
var size : Vector2
"""
ESCBackground purpose is to display a background image and receive input events
on the background. More precisely, the TextureRect under ESCBackground does not
receive events itself - if it did, it would also eat all events like hotspot
focusing and such. Instead, we set the TextureRect mouse filter to
MOUSE_FILTER_IGNORE, and we use an Area2D node to receive the input events.
If ESCBackground doesn't contain a texture, it is important that its rect_size
is set over the whole scene, because its rect_size is then used to create the
Area2D node under it. If the rect_size is wrongly set, the background may
receive no input.
"""
# PRIVATE VARS
var area : Area2D
var actual_click_position : Vector2
# Godot doesn't do doubleclicks so we must
var last_lmb_dt = 0
var waiting_dblclick = null # null or [pos, event]
# Create the underlying Area2D as an input device
func _enter_tree():
# Use size of background texture to calculate collision shape if any
var size
if get_texture():
size = get_texture().get_size()
else:
size = rect_size
area = Area2D.new()
var area = Area2D.new()
var shape = RectangleShape2D.new()
var sid = area.create_shape_owner(area)
@@ -53,24 +57,34 @@ func _enter_tree():
transform.origin = size / 2
area.shape_owner_set_transform(sid, transform)
# Set extents of RectangleShape2D to cover entire Sprite
# Set extents of RectangleShape2D to cover entire TextureRect
shape.set_extents(size / 2)
area.shape_owner_add_shape(sid, shape)
# Handle inputs to the Area2D ourself
area.connect("input_event", self, "manage_input")
add_child(area)
# Disable mouse filter events and connect our own events to the ESC input
# manager
func _ready():
mouse_filter = MOUSE_FILTER_IGNORE
area.connect("input_event", self, "manage_input")
connect("gui_input", self, "manage_input_texturerect")
if !Engine.is_editor_hint():
connect("left_click_on_bg", escoria.inputs_manager, "_on_left_click_on_bg")
connect("right_click_on_bg", escoria.inputs_manager, "_on_right_click_on_bg")
connect("double_left_click_on_bg", escoria.inputs_manager, "_on_double_left_click_on_bg")
# connect("mouse_moved_on_bg", escoria.inputs_manager, "_on_mouse_moved_on_bg")
func manage_input(_viewport, event, _shape_idx):
# Manage inputs reaching the Area2D and emit the events to the input manager
# TODO: Don't change private variables here, use an event for BUTTON_WHEEL
#
# #### Parameters
# - _viewport: (not used)
# - event: Event received
# - _shape_idx: (not used)
func manage_input(_viewport, event, _shape_idx) -> void:
if event.is_action_pressed("switch_action_verb"):
if event.button_index == BUTTON_WHEEL_UP:
escoria.inputs_manager._on_mousewheel_action(-1)
@@ -87,21 +101,11 @@ func manage_input(_viewport, event, _shape_idx):
emit_signal("left_click_on_bg", p)
if event.button_index == BUTTON_RIGHT:
emit_signal("right_click_on_bg", p)
# elif event is InputEventMouseMotion:
# emit_signal("mouse_moved_on_bg")
func manage_input_texturerect(event):
if event is InputEventMouseButton and event.is_pressed():
if event.button_index == BUTTON_LEFT:
emit_signal("left_click_on_bg", event.position)
if event.button_index == BUTTON_RIGHT:
emit_signal("right_click_on_bg", event.position)
else:
pass
# Calculate the actual area taken by this background depending on its
# Texture or set size
# **Returns** The correct area size
func get_full_area_rect2() -> Rect2:
var area_rect2 : Rect2 = Rect2()
var pos = get_global_position()

View File

@@ -0,0 +1,251 @@
# A base class for ESC game scenes
# An extending class can be used in the project settings and is responsible
# for managing very basic game features and controls
tool
extends Node2D
class_name ESCGame
# Editor debug modes
# NONE - No debugging
# MOUSE_TOOLTIP_LIMITS - Visualize the tooltip limits
enum EDITOR_GAME_DEBUG_DISPLAY {
NONE,
MOUSE_TOOLTIP_LIMITS
}
# The safe margin around tooltips
export(float) var mouse_tooltip_margin = 50.0
# A reference to the node handling tooltips
var tooltip_node : Object
# Which (if any) debug mode for the editor is used
export(EDITOR_GAME_DEBUG_DISPLAY) var editor_debug_mode = \
EDITOR_GAME_DEBUG_DISPLAY.NONE setget _set_editor_debug_mode
# Handle debugging visualizations
func _draw():
if !Engine.is_editor_hint():
return
if editor_debug_mode == EDITOR_GAME_DEBUG_DISPLAY.NONE:
return
if editor_debug_mode == EDITOR_GAME_DEBUG_DISPLAY.MOUSE_TOOLTIP_LIMITS:
var mouse_limits : Rect2 = get_viewport_rect().grow(-mouse_tooltip_margin)
print(mouse_limits)
# Draw lines for tooltip limits
draw_rect(mouse_limits, ColorN("red"), false, 10.0)
# Called when the player left clicks on the background
# (Needs to be overridden, if supported)
#
# #### Parameters
#
# - position: Position clicked
func left_click_on_bg(position: Vector2) -> void:
pass
# Called when the player right clicks on the background
# (Needs to be overridden, if supported)
#
# #### Parameters
#
# - position: Position clicked
func right_click_on_bg(position: Vector2) -> void:
pass
# Called when the player double clicks on the background
# (Needs to be overridden, if supported)
#
# #### Parameters
#
# - position: Position clicked
func left_double_click_on_bg(position: Vector2) -> void:
pass
# Called when an element in the scene was focused
# (Needs to be overridden, if supported)
#
# #### Parameters
#
# - element_id: Global id of the element focused
func element_focused(element_id: String) -> void:
pass
# Called when no element is focused anymore
# (Needs to be overridden, if supported)
func element_unfocused() -> void:
pass
# Called when an item was left clicked
# (Needs to be overridden, if supported)
#
# #### Parameters
#
# - item_global_id: Global id of the item that was clicked
# - event: The received input event
func left_click_on_item(item_global_id: String, event: InputEvent) -> void:
pass
# Called when an item was right clicked
# (Needs to be overridden, if supported)
#
# #### Parameters
#
# - item_global_id: Global id of the item that was clicked
# - event: The received input event
func right_click_on_item(item_global_id: String, event: InputEvent) -> void:
pass
# Called when an item was double clicked
# (Needs to be overridden, if supported)
#
# #### Parameters
#
# - item_global_id: Global id of the item that was clicked
# - event: The received input event
func left_double_click_on_item(
item_global_id: String,
event: InputEvent
) -> void:
pass
# Called when an inventory item was left clicked
# (Needs to be overridden, if supported)
#
# #### Parameters
#
# - inventory_item_global_id: Global id of the inventory item was clicked
# - event: The received input event
func left_click_on_inventory_item(
inventory_item_global_id: String,
event: InputEvent
) -> void:
pass
# Called when an inventory item was right clicked
# (Needs to be overridden, if supported)
#
# #### Parameters
#
# - inventory_item_global_id: Global id of the inventory item was clicked
# - event: The received input event
func right_click_on_inventory_item(
inventory_item_global_id: String,
event: InputEvent
) -> void:
pass
# Called when an inventory item was double clicked
# (Needs to be overridden, if supported)
#
# #### Parameters
#
# - inventory_item_global_id: Global id of the inventory item was clicked
# - event: The received input event
func left_double_click_on_inventory_item(
inventory_item_global_id: String,
event: InputEvent
) -> void:
pass
# Called when an inventory item was focused
# (Needs to be overridden, if supported)
#
# #### Parameters
#
# - inventory_item_global_id: Global id of the inventory item that was focused
func inventory_item_focused(inventory_item_global_id : String) -> void:
pass
# Called when no inventory item is focused anymore
# (Needs to be overridden, if supported)
func inventory_item_unfocused() -> void:
pass
# Called when the inventory was opened
# (Needs to be overridden, if supported)
func open_inventory():
pass
# Called when the inventory was closed
# (Needs to be overridden, if supported)
func close_inventory():
pass
# Called when the mousewheel was used
# (Needs to be overridden, if supported)
#
# #### Parameter
#
# - direction: The direction in which the mouse wheel was rotated
func mousewheel_action(direction : int):
pass
# Called when the UI should be hidden
# (Needs to be overridden, if supported)
func hide_ui():
pass
# Called when the UI should be shown
# (Needs to be overridden, if supported)
func show_ui():
pass
# Function is called if Project setting escoria/ui/tooltip_follows_mouse = true
#
# #### Parameters
#
# - p_position: Position of the mouse
func update_tooltip_following_mouse_position(p_position : Vector2):
var corrected_position = p_position
# clamp TOP
if tooltip_node.tooltip_distance_to_edge_top(p_position) <= mouse_tooltip_margin:
corrected_position.y = mouse_tooltip_margin
# clamp BOTTOM
if tooltip_node.tooltip_distance_to_edge_bottom(p_position + tooltip_node.rect_size) <= mouse_tooltip_margin:
corrected_position.y = escoria.game_size.y - mouse_tooltip_margin - tooltip_node.rect_size.y
# clamp LEFT
if tooltip_node.tooltip_distance_to_edge_left(p_position - tooltip_node.rect_size/2) <= mouse_tooltip_margin:
corrected_position.x = mouse_tooltip_margin
# clamp RIGHT
if tooltip_node.tooltip_distance_to_edge_right(p_position + tooltip_node.rect_size/2) <= mouse_tooltip_margin:
corrected_position.x = escoria.game_size.x - mouse_tooltip_margin - tooltip_node.rect_size.x
tooltip_node.anchor_right = 0.2
tooltip_node.rect_position = corrected_position + tooltip_node.offset_from_cursor
# Set the Editor debug mode
func _set_editor_debug_mode(p_editor_debug_mode : int) -> void:
editor_debug_mode = p_editor_debug_mode
update()

View File

@@ -0,0 +1,93 @@
# The inventory representation of an ESC item if pickable
extends TextureButton
class_name ESCInventoryItem
# Signal emitted when the item was left clicked
#
# #### Parameters
#
# - item_id: Global ID of the clicked item
signal mouse_left_inventory_item(item_id)
# Signal emitted when the item was right clicked
#
# #### Parameters
#
# - item_id: Global ID of the clicked item
signal mouse_right_inventory_item(item_id)
# Signal emitted when the item was double clicked
#
# #### Parameters
#
# - item_id: Global ID of the clicked item
signal mouse_double_left_inventory_item(item_id)
# Signal emitted when the item was focused
#
# #### Parameters
#
# - item_id: Global ID of the clicked item
signal inventory_item_focused(item_id)
# Signal emitted when the item is not focused anymore
signal inventory_item_unfocused()
# Global ID of the ESCItem that uses this ESCInventoryItem
# Will be set by ESCItem automatically
var global_id
# Connect input handlers
func _ready():
connect("gui_input", self, "_on_inventory_item_gui_input")
connect("mouse_entered", self, "_on_inventory_item_mouse_enter")
connect("mouse_exited", self, "_on_inventory_item_mouse_exit")
# Handle the gui input and emit the respective signals
#
# #### Parameters
#
# - event: The event received
func _on_inventory_item_gui_input(event : InputEvent):
if event.is_action_pressed("switch_action_verb"):
if event.button_index == BUTTON_WHEEL_UP:
escoria.inputs_manager._on_mousewheel_action(-1)
elif event.button_index == BUTTON_WHEEL_DOWN:
escoria.inputs_manager._on_mousewheel_action(1)
if event is InputEventMouseButton:
# var p = get_global_mouse_position()
if event.doubleclick:
if event.button_index == BUTTON_LEFT:
emit_signal(
"mouse_double_left_inventory_item",
global_id,
event
)
else:
if event.is_pressed():
if event.button_index == BUTTON_LEFT:
emit_signal(
"mouse_left_inventory_item",
global_id,
event
)
if event.button_index == BUTTON_RIGHT:
emit_signal(
"mouse_right_inventory_item",
global_id,
event
)
# Handle mouse entering the item and send the respecitve signal
func _on_inventory_item_mouse_enter():
emit_signal("inventory_item_focused", global_id)
# Handle mouse leaving the item and send the respecitve signal
func _on_inventory_item_mouse_exit():
emit_signal("inventory_item_unfocused")

View File

@@ -0,0 +1,349 @@
# ESCItem is a Sprite that defines an item, potentially interactive
tool
extends Area2D
class_name ESCItem
# Emitted when the mouse has entered this item
#
# #### Parameters
#
# - items: The inventory item node
signal mouse_entered_item(item)
# Emitted when the mouse has exited this item
#
# #### Parameters
#
# - items: The inventory item node
signal mouse_exited_item(item)
# Emitted when the item was left cliced
#
# #### Parameters
#
# - global_id: ID of this item
signal mouse_left_clicked_item(global_id)
# Emitted when the item was double cliced
#
# #### Parameters
#
# - global_id: ID of this item
signal mouse_double_left_clicked_item(global_id)
# Emitted when the item was right cliced
#
# #### Parameters
#
# - global_id: ID of this item
signal mouse_right_clicked_item(global_id)
# Emitted when the item walked to a destination
#
# #### Parameters
#
# - walk_context: The walk context of the command
signal arrived(walk_context)
# The global ID of this item
export(String) var global_id
# The ESC script for this item
export(String, FILE, "*.esc") var esc_script
# If true, the ESC script may have an ":exit_scene" event to manage scene changes
export(bool) var is_exit
# If true, object is considered as trigger. Allows using :trigger_in and
# :trigger_out verbs in ESC scripts.
export(bool) var is_trigger
# The verb used for the trigger in ESC events
export(String) var trigger_in_verb = "trigger_in"
# The verb used for the trigger out ESC events
export(String) var trigger_out_verb = "trigger_out"
# If true, the player can interact with this item
export(bool) var is_interactive = true
# If true, player orients towards 'interaction_direction' as
# player character arrives.
export(bool) var player_orients_on_arrival = true
# Let the player turn to this direction when the player arrives at the
# item
export(int) var interaction_direction
# The name for the tooltip of this item
export(String) var tooltip_name
# Default action to use if object is not in the inventory
export(String) var default_action
# Default action to use if object is in the inventory
export(String) var default_action_inventory
# If action used by player is in this list, the 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 = []
# If true, combination must be done in the way it is written in ESC script
# ie. :use ON_ITEM
# If false, combination will be tried in the other way.
export(bool) var combine_is_one_way = false
# If true, then the object must have been picked up before using it.
# A false value is useful for items in the background, such as buttons.
export(bool) var use_from_inventory_only = false
# Scene based on ESCInventoryItem used in inventory for the object if it is
# picked up, that displays and handles the item
export(PackedScene) var inventory_item_scene_file : PackedScene
# Color used for dialogs
export(Color) var dialog_color = ColorN("white")
# If true, terrain scaling will not be applied and
# node will remain at the scale set in the scene.
export(bool) var dont_apply_terrain_scaling = false
# Speed of this item ifmovable
export(int) var speed : int = 300
# Speed damp of this item if movable
export(float) var v_speed_damp : float = 1.0
# Animations script (for walking, idling...)
export(Script) var animations
# The movable subnode
var movable: ESCMovable = null
# Reference to the animation node (null if none was found)
var animation_sprite = null
# Reference to the current terrain
var terrain: ESCTerrain
# Reference to this items collision shape node
var collision: Node
# The representation of this item in the scene. Will
# be loaded, if inventory_item_scene_file is set.
var inventory_item: ESCInventoryItem = null setget ,_get_inventory_item
# Add the movable node, connect signals, detect child nodes
# and register this item
func _ready():
movable = ESCMovable.new()
add_child(movable)
_detect_children()
connect("mouse_entered", self, "_on_mouse_entered")
connect("mouse_exited", self, "_on_mouse_exited")
connect("input_event", self, "manage_input")
# Register and connect all elements to Escoria backoffice.
if not Engine.is_editor_hint():
escoria.event_manager.connect("event_finished", self, "_update_terrain")
escoria.object_manager.register_object(
ESCObject.new(
global_id,
self
),
true
)
terrain = escoria.room_terrain
if !is_trigger:
connect("mouse_entered_item", escoria.inputs_manager, "_on_mouse_entered_item")
connect("mouse_exited_item", escoria.inputs_manager, "_on_mouse_exited_item")
connect("mouse_left_clicked_item", escoria.inputs_manager, "_on_mouse_left_clicked_item")
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")
else:
connect("area_entered", self, "element_entered")
connect("area_exited", self, "element_exited")
connect("body_entered", self, "element_entered")
connect("body_exited", self, "element_exited")
# If object can be in the inventory, set default_action_inventory to same as
# default_action, if default_action_inventory is not set
if use_from_inventory_only and default_action_inventory.empty():
default_action_inventory = default_action
# Perform a first terrain scaling if we have to.
if !is_exit or dont_apply_terrain_scaling:
movable.last_scale = scale
movable.update_terrain()
# Return the animation player node
func get_animation_player():
return animation_sprite
# Return the position the player needs to walk to to interact with this
# item. That can either be a direct Position2D child or a collision shape
#
# **Returns** The interaction position
func get_interact_position() -> Vector2:
var interact_position = null
for c in get_children():
if c is Position2D:
if c.get_owner() == self:
continue
interact_position = c.global_position
if interact_position == null and collision != null:
interact_position = collision.global_position
return interact_position
# Manage mouse button clicks on this item by sending out signals
#
# #### Parameters
#
# - _viewport: not used
# - event: Triggered event
# - _shape_idx: not used
func manage_input(
_viewport : Viewport,
event : InputEvent,
_shape_idx : int
) -> void:
if event is InputEventMouseButton:
if event.doubleclick:
if event.button_index == BUTTON_LEFT:
emit_signal("mouse_double_left_clicked_item", self, event)
else:
if event.is_pressed():
if event.button_index == BUTTON_LEFT:
emit_signal("mouse_left_clicked_item", self, event)
elif event.button_index == BUTTON_RIGHT:
emit_signal("mouse_right_clicked_item", self, event)
# React to the mouse entering the item by emitting the respective signal
func _on_mouse_entered():
emit_signal("mouse_entered_item", self)
# React to the mouse exiting the item by emitting the respective signal
func _on_mouse_exited():
emit_signal("mouse_exited_item", self)
# Another item (e.g. the player) has entered this item
#
# #### Parameters
#
# - body: Other object that has entered the item
func element_entered(body):
if body is ESCBackground or body.get_parent() is ESCBackground:
return
escoria.do("trigger_in", [global_id, body.global_id, trigger_in_verb])
# Another item (e.g. the player) has exited this element
# #### Parameters
#
# - body: Other object that has entered the item
func element_exited(body):
if body is ESCBackground or body.get_parent() is ESCBackground:
return
escoria.do("trigger_out", [global_id, body.global_id, trigger_out_verb])
# Use the movable node to teleport this item to the target item
#
# #### Parameters
#
# - target: Target item to teleport to
func teleport(target: Node) -> void:
movable.teleport(target)
# Use the movable node to make the item walk to the given position
#
# #### Parameters
#
# - pos: Position to walk to
# - p_walk_context: Walk context to use
func walk_to(pos : Vector2, p_walk_context: ESCWalkContext = null) -> void:
movable.walk_to(pos, p_walk_context)
# Set the moving speed
#
# #### Parameters
#
# - speed_value: Set the new speed
func set_speed(speed_value : int) -> void:
speed = speed_value
# Set the angle
#
# #### Parameters
#
# Set the angle
func set_angle(deg : int, immediate = true):
movable.set_angle(deg, immediate)
# Play the talking animation
func start_talking():
if animation_sprite.is_playing():
animation_sprite.stop()
animation_sprite.play(animations.speaks[movable.last_dir][0])
# Stop playing the talking animation
func stop_talking():
if animation_sprite.is_playing():
animation_sprite.stop()
animation_sprite.play(animations.idles[movable.last_dir][0])
# Detect the child nodes and set respective references
func _detect_children() -> void:
# Detect animation player
for n in get_children():
if n is AnimatedSprite:
animation_sprite = n
continue
if n is AnimationPlayer:
animation_sprite = n
continue
# Initialize collision variable.
for c in get_children():
if c is CollisionShape2D or c is CollisionPolygon2D:
collision = c
# Upate the terrain when an event finished
func _update_terrain(rc: int, event_name: String) -> void:
movable.update_terrain(event_name)
# Get inventory item from the inventory item scene
# **Returns** The inventory item of this ESCitem
func _get_inventory_item() -> ESCInventoryItem:
if not inventory_item and inventory_item_scene_file:
inventory_item = inventory_item_scene_file.instance()
inventory_item.global_id = self.global_id
return inventory_item

View File

@@ -0,0 +1,20 @@
# A playable character
# 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.
tool
extends ESCItem
class_name ESCPlayer
# The node that references the camera position
export(NodePath) var camera_position_node
# Return the camera position if a camera_position_node exists or the
# global position of the player
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

View File

@@ -1,6 +1,6 @@
# A cache for resources
extends Object
class_name ResourceCache
class_name ESCResourceCache
var thread : Thread
var mutex : Mutex

View File

@@ -1,41 +1,62 @@
# A room in an Escora based game
tool
extends Node2D
class_name ESCRoom
func get_class():
return "ESCRoom"
export(String) var global_id = ""
export(String, FILE, "*.esc") var esc_script = ""
export(PackedScene) var player_scene
export(Array, Rect2) var camera_limits : Array = [Rect2()] setget set_camera_limits
var player
onready var game = $game
### EDITOR TOOLS ###
enum EDITOR_ROOM_DEBUG_DISPLAY {
# Debugging displays for a room
# NONE: No debug display
# CAMERA_LIMITS: Display the camera limits
enum EditorRoomDebugDisplay {
NONE,
CAMERA_LIMITS
}
export(EDITOR_ROOM_DEBUG_DISPLAY) var editor_debug_mode = EDITOR_ROOM_DEBUG_DISPLAY.NONE setget set_editor_debug_mode
onready var camera_limits_colors : Array = [
ColorN("red"), ColorN("blue"), ColorN("green")
]
### END EDITOR TOOLS ###
# The global id of this room
export(String) var global_id = ""
# The ESC script of this room
export(String, FILE, "*.esc") var esc_script = ""
# The player inside this scene
export(PackedScene) var player_scene
# The camera limits available in this room
export(Array, Rect2) var camera_limits : Array = [Rect2()] setget set_camera_limits
# The editor debug display mode
export(int) var editor_debug_mode = EditorRoomDebugDisplay.NONE setget set_editor_debug_mode
# The player scene instance
var player
# The game scene instance
var game
# Start the random number generator when the camera limits should be displayed
func _enter_tree():
randomize()
if editor_debug_mode == EditorRoomDebugDisplay.CAMERA_LIMITS:
randomize()
# Sanitize camera limits, add player node and set the global id to the
# name of this node if it's not set manually
func _ready():
if camera_limits.empty():
camera_limits.push_back(Rect2())
if camera_limits.size() == 1 and camera_limits[0].has_no_area():
camera_limits[0] = Rect2(0, 0, $background.rect_size.x, $background.rect_size.y)
camera_limits[0] = \
Rect2(0, 0, $background.rect_size.x, $background.rect_size.y)
if Engine.is_editor_hint():
return
game = $game
if player_scene:
player = player_scene.instance()
add_child(player)
@@ -59,12 +80,18 @@ func _ready():
if global_id.empty():
global_id = name
# Draw the camera limits visualization if enabled
func _draw():
if !Engine.is_editor_hint():
return
if editor_debug_mode == EDITOR_ROOM_DEBUG_DISPLAY.NONE:
if editor_debug_mode == EditorRoomDebugDisplay.NONE:
return
var camera_limits_colors : Array = [
ColorN("red"), ColorN("blue"), ColorN("green")
]
# If there are more camera limits than colors defined for them, add more.
if camera_limits.size() > camera_limits_colors.size():
@@ -79,12 +106,22 @@ func _draw():
draw_string(default_font, Vector2(camera_limits[i].position.x + 30,
camera_limits[i].position.y + 30), str(i), camera_limits_colors[i])
return
func set_camera_limits(p_camera_limits : Array) -> void:
# Set the camera limits
#
# #### Parameters
#
# - p_camera_limits: An array of Rect2Ds as camera limits
func set_camera_limits(p_camera_limits: Array) -> void:
camera_limits = p_camera_limits
update()
func set_editor_debug_mode(p_editor_debug_mode : int) -> void:
# Set the editor debug mode
#
# #### Parameters
#
# - p_editor_debug_mode: The debug mode to set for the room
func set_editor_debug_mode(p_editor_debug_mode: int) -> void:
editor_debug_mode = p_editor_debug_mode
update()

View File

@@ -0,0 +1,228 @@
# A walkable Terrains
tool
extends Navigation2D
class_name ESCTerrain
# Visualize scales or the lightmap for debugging purposes
enum DebugMode {
NONE
SCALES
LIGHTMAP
}
# Scaling texture
export(Texture) var scales setget _set_scales
# Minimum scaling
export(float) var scale_min = 0.3
# Maximum scaling
export(float) var scale_max = 1.0
# Lightmap texture
export(Texture) var lightmap setget _set_lightmap
# The scaling factor for the scale and light maps
export(Vector2) var bitmaps_scale = Vector2(1,1) setget _set_bm_scale
# Multiplier applied to the player speed on this terrain
export(float) var player_speed_multiplier = 1.0
# Multiplier how much faster the player will walk when fast mode is on
# (double clicked)
export(float) var player_doubleclick_speed_multiplier = 1.5
# Additional modulator to the lightmap texture
export(Color) var lightmap_modulate = Color(1, 1, 1, 1)
# Currently selected debug visualize mode
export(int, "None", "Scales", "Lightmap") var debug_mode = DebugMode.NONE \
setget _set_debug_mode
# The currently activ navigation polygon
var current_active_navigation_instance: NavigationPolygonInstance = null
# Currently visualized texture for debug mode
var _texture = null
# The image from the lightmap texture
var _lightmap_data
# Prohibits multiple calls to update_texture
var _texture_in_update = false
# Set a reference to the active navigation polygon, register to Escoria
# and update the texture
func _ready():
var navigation_enabled_found = false
for n in get_children():
if n is NavigationPolygonInstance:
if n.enabled:
if navigation_enabled_found:
escoria.logger.report_errors(
"ESCTerrain:_ready()",
[
"Multiple NavigationPolygonInstances enabled " + \
"at the same time."
]
)
navigation_enabled_found = true
current_active_navigation_instance = n
if !Engine.is_editor_hint():
escoria.room_terrain = self
_update_texture()
# Return the Color of the lightmap pixel for the specified position
#
# #### Parameters
#
# - pos: Position to calculate lightmap for
# **Returns** The color of the given point
func get_light(pos: Vector2) -> Color:
if not lightmap or lightmap.get_data().is_empty():
return Color(1, 1, 1, 1)
var c = _get_color(_lightmap_data, pos)
return _get_color(_lightmap_data, pos) * lightmap_modulate
# Calculate the scale inside the scale range for a given scale factor
#
# #### Parameters
#
# - factor: The factor for the scaling according to the scale map
# **Returns** The scaling
func get_scale_range(factor: float) -> Vector2:
factor = scale_min + (scale_max - scale_min) * factor
return Vector2(factor, factor)
# Get the terrain scale factor for a given position
#
# #### Parameters
#
# - pos: The position to calculate for
# **Returns** The scale factor for the given position
func get_terrain(pos: Vector2) -> float:
if scales == null || scales.get_data().is_empty():
return 1.0
return _get_color(scales.get_data(), pos).v
# Small helper to get the color of an image at a position
func _get_color(image: Image, pos: Vector2) -> Color:
image.lock()
var color=image.get_pixel(pos.x, pos.y)
image.unlock()
return color
# Set the bitmap scaling
#
# #### Parameters
#
# - p_scale: Scale to set
func _set_bm_scale(p_scale: Vector2):
bitmaps_scale = p_scale
_update_texture()
# Set the lightmap texture
#
# #### Parameters
#
# - p_lightmap: Lightmap texture to set
func _set_lightmap(p_lightmap: Texture):
var need_init = (lightmap != p_lightmap) or (lightmap and not _lightmap_data)
lightmap = p_lightmap
# It's bad enough a new copy is created when reading a pixel, we don't
# also need to get the data for every read to make yet another copy
if need_init:
if _lightmap_data:
_lightmap_data.unlock()
_lightmap_data = lightmap.get_data()
_lightmap_data.lock()
_update_texture()
# Set the scales texture
#
# #### Parameters
#
# - p_scales: Scale texture to set
func _set_scales(p_scales: Texture):
scales = p_scales
_update_texture()
# Set the debug mode
#
# #### Parameters
#
# - p_mode: Debug mode to set
func _set_debug_mode(p_mode: int):
debug_mode = p_mode
_update_texture()
# Update the debug texture, if it is dirty
func _update_texture():
if _texture_in_update:
return
_texture_in_update = true
call_deferred("_do_update_texture")
# Update the texture and optionally set the debug texture
func _do_update_texture():
_texture_in_update = false
if !is_inside_tree() or !Engine.is_editor_hint():
return
if debug_mode == DebugMode.NONE:
update()
return
_texture = ImageTexture.new()
if debug_mode == DebugMode.SCALES:
if scales != null:
_texture = scales
elif debug_mode == DebugMode.LIGHTMAP:
if lightmap != null:
_texture = lightmap
update()
# Draw debugging visualizations
func _draw():
if _texture == null or \
not Engine.is_editor_hint() or \
debug_mode == DebugMode.NONE:
if current_active_navigation_instance:
current_active_navigation_instance.visible = true
return
var scale_vect = bitmaps_scale
if current_active_navigation_instance:
current_active_navigation_instance.visible = false
var src = Rect2(0, 0, _texture.get_width(), _texture.get_height())
var dst = Rect2(
0,
0,
_texture.get_width() * scale_vect.x,
_texture.get_height() * scale_vect.y
)
draw_texture_rect_region(_texture, dst, src)

View File

@@ -0,0 +1,24 @@
# The walk context describes the target of a walk command and if that command
# should be executed fast
extends Object
class_name ESCWalkContext
# Target object that the walk command tries to reach
var target_object: ESCObject = null
# The target position
var target_position: Vector2 = Vector2()
# Wether to move fast
var fast: bool
func _init(
p_target_object: ESCObject,
p_target_position: Vector2,
p_fast: bool
):
target_object = p_target_object
target_position = p_target_position
fast = p_fast

View File

@@ -1,129 +0,0 @@
tool
extends Node2D
class_name ESCGame
func get_class():
return "ESCGame"
export(float) var mouse_tooltip_margin = 50.0
var tooltip_node : Object
### EDITOR TOOLS ###
enum EDITOR_GAME_DEBUG_DISPLAY {
NONE,
MOUSE_TOOLTIP_LIMITS
}
export(EDITOR_GAME_DEBUG_DISPLAY) var editor_debug_mode = EDITOR_GAME_DEBUG_DISPLAY.NONE setget set_editor_debug_mode
func set_editor_debug_mode(p_editor_debug_mode : int) -> void:
editor_debug_mode = p_editor_debug_mode
update()
func _draw():
if !Engine.is_editor_hint():
return
if editor_debug_mode == EDITOR_GAME_DEBUG_DISPLAY.NONE:
return
if editor_debug_mode == EDITOR_GAME_DEBUG_DISPLAY.MOUSE_TOOLTIP_LIMITS:
var mouse_limits : Rect2 = get_viewport_rect().grow(-mouse_tooltip_margin)
print(mouse_limits)
# Draw lines for tooltip limits
draw_rect(mouse_limits, ColorN("red"), false, 10.0)
return
## BACKGROUND ##
func left_click_on_bg(position : Vector2) -> void:
pass
func right_click_on_bg(position : Vector2) -> void:
pass
func left_double_click_on_bg(position : Vector2) -> void:
pass
## ITEM/HOTSPOT FOCUS ##
func element_focused(element_id : String) -> void:
pass
func element_unfocused() -> void:
pass
## ITEMS ##
func left_click_on_item(item_global_id : String, event : InputEvent) -> void:
pass
func right_click_on_item(item_global_id : String, event : InputEvent) -> void:
pass
func left_double_click_on_item(item_global_id : String, event : InputEvent) -> void:
pass
## INVENTORY ##
func left_click_on_inventory_item(inventory_item_global_id : String, event : InputEvent) -> void:
pass
func right_click_on_inventory_item(inventory_item_global_id : String, event : InputEvent) -> void:
pass
func left_double_click_on_inventory_item(inventory_item_global_id : String, event : InputEvent) -> void:
pass
func inventory_item_focused(inventory_item_global_id : String) -> void:
pass
func inventory_item_unfocused() -> void:
pass
func open_inventory():
pass
func close_inventory():
pass
## MOUSEWHEEL ACTION ##
func mousewheel_action(direction : int):
pass
## UI SPECIFICS
func hide_ui():
pass
func show_ui():
pass
## FUNCTIONS BELOW THIS POINT DON'T NEED TO BE REIMPLEMENTED BY USER
## (Although they can be, if required)
# This function is called if Project setting escoria/ui/tooltip_follows_mouse = true
func update_tooltip_following_mouse_position(p_position : Vector2):
var corrected_position = p_position
# clamp TOP
if tooltip_node.tooltip_distance_to_edge_top(p_position) <= mouse_tooltip_margin:
corrected_position.y = mouse_tooltip_margin
# clamp BOTTOM
if tooltip_node.tooltip_distance_to_edge_bottom(p_position + tooltip_node.rect_size) <= mouse_tooltip_margin:
corrected_position.y = escoria.game_size.y - mouse_tooltip_margin - tooltip_node.rect_size.y
# clamp LEFT
if tooltip_node.tooltip_distance_to_edge_left(p_position - tooltip_node.rect_size/2) <= mouse_tooltip_margin:
corrected_position.x = mouse_tooltip_margin
# clamp RIGHT
if tooltip_node.tooltip_distance_to_edge_right(p_position + tooltip_node.rect_size/2) <= mouse_tooltip_margin:
corrected_position.x = escoria.game_size.x - mouse_tooltip_margin - tooltip_node.rect_size.x
tooltip_node.anchor_right = 0.2
tooltip_node.rect_position = corrected_position + tooltip_node.offset_from_cursor

View File

@@ -1,45 +0,0 @@
extends TextureButton
class_name ESCInventoryItem
func get_class():
return "ESCInventoryItem"
export(String) var global_id
signal mouse_left_inventory_item(item_id)
signal mouse_right_inventory_item(item_id)
signal mouse_double_left_inventory_item(item_id)
signal inventory_item_focused(item_id)
signal inventory_item_unfocused()
func _ready():
connect("gui_input", self, "_on_inventory_item_gui_input")
connect("mouse_entered", self, "_on_inventory_item_mouse_enter")
connect("mouse_exited", self, "_on_inventory_item_mouse_exit")
func _on_inventory_item_gui_input(event : InputEvent):
if event.is_action_pressed("switch_action_verb"):
if event.button_index == BUTTON_WHEEL_UP:
escoria.inputs_manager._on_mousewheel_action(-1)
elif event.button_index == BUTTON_WHEEL_DOWN:
escoria.inputs_manager._on_mousewheel_action(1)
if event is InputEventMouseButton:
# var p = get_global_mouse_position()
if event.doubleclick:
if event.button_index == BUTTON_LEFT:
emit_signal("mouse_double_left_inventory_item", global_id, event)
else:
if event.is_pressed():
if event.button_index == BUTTON_LEFT:
emit_signal("mouse_left_inventory_item", global_id, event)
if event.button_index == BUTTON_RIGHT:
emit_signal("mouse_right_inventory_item", global_id, event)
func _on_inventory_item_mouse_enter():
# Notify UI that item is focused (room.game.ui.Label UI)
emit_signal("inventory_item_focused", global_id)
func _on_inventory_item_mouse_exit():
# Notify UI that item is unfocused (room.game.ui.Label UI)
emit_signal("inventory_item_unfocused")

View File

@@ -1,269 +0,0 @@
tool
extends Area2D
class_name ESCItem
"""
ESCItem is a Sprite that defines an item, potentially interactive
"""
signal mouse_entered_item(item)
signal mouse_exited_item(item)
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
export(bool) var is_exit
# is_trigger If true, object is considered as trigger. Allows using :trigger_in and
# :trigger_out verbs in ESC scripts.
export(bool) var is_trigger
export(String) var trigger_in_verb = "trigger_in"
export(String) var trigger_out_verb = "trigger_out"
# is_interactive : If true, object is not "focusable".
export(bool) var is_interactive = true
# player_orients_on_arrival : If true, player orients towards 'interaction_direction' as
# player character arrives.
export(bool) var player_orients_on_arrival = true
export(ESCPlayer.Directions) var interaction_direction
export(String) var tooltip_name
# Default action to use if object is not in the inventory
export(String) var default_action
# Default action to use if object is in the inventory
export(String) var default_action_inventory
# 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 = []
# If true, combination must be done in the way it is written in ESC script
# ie. :use ON_ITEM
# If false, combination will be tried in the other way.
export(bool) var combine_is_one_way = false
# If use_from_inventory_only is true, then the object must have been picked up before using it.
# A false value is useful for items in the background, such as buttons.
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
# Color used for dialogs
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.
#var interact_positions : Dictionary = { "default": null}
# Animation node (null if none was found)
var animation_sprite
# Animations script (for walking, idling...)
export(Script) var animations
# TERRAIN
var terrain : ESCTerrain
# If the terrain node type is scalenodes
var terrain_is_scalenodes : bool
var check_maps = true
var collision
# If dont_apply_terrain_scaling is true, terrain scaling will not be applied and
# node will remain at the scale set in the scene.
export(bool) var dont_apply_terrain_scaling = false
export(int) var speed : int = 300
export(float) var v_speed_damp : float = 1.0
var orig_speed : float
enum PLAYER_TASKS {
NONE,
WALK,
SLIDE
}
onready var task = PLAYER_TASKS.NONE # type PLAYER_TASKS
var params_queue : Array
# PRIVATE VARS
# Size of the item
var size : Vector2
var last_deg : int
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
continue
if n is AnimationPlayer:
animation_sprite = n
continue
connect("mouse_entered", self, "_on_mouse_entered")
connect("mouse_exited", self, "_on_mouse_exited")
connect("input_event", self, "manage_input")
# Initialize collision variable.
for c in get_children():
if c is CollisionShape2D or c is CollisionPolygon2D:
collision = c
# Register and connect all elements to Escoria backoffice.
if !Engine.is_editor_hint():
escoria.object_manager.register_object(
ESCObject.new(
global_id,
self
),
true
)
terrain = escoria.room_terrain
if !is_trigger:
connect("mouse_entered_item", escoria.inputs_manager, "_on_mouse_entered_item")
connect("mouse_exited_item", escoria.inputs_manager, "_on_mouse_exited_item")
connect("mouse_left_clicked_item", escoria.inputs_manager, "_on_mouse_left_clicked_item")
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")
else:
connect("area_entered", self, "element_entered")
connect("area_exited", self, "element_exited")
connect("body_entered", self, "element_entered")
connect("body_exited", self, "element_exited")
# If object can be in the inventory, set default_action_inventory to same as
# default_action, if default_action_inventory is not set
if use_from_inventory_only and default_action_inventory.empty():
default_action_inventory = default_action
# Perform a first terrain scaling if we have to.
if !is_exit or dont_apply_terrain_scaling:
Movable.last_scale = scale
Movable.update_terrain()
func get_animation_player():
if animation_sprite == null:
for n in get_children():
if n is AnimationPlayer:
animation_sprite = n
return animation_sprite
#"""
#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.
#"""
#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
# # but a dialog position for example. Interact position node must be set in the room scene.
# if c.get_owner() == self:
# continue
# var globalpos = c.global_position
# interact_positions.default = c.global_position
#
# if c is CollisionShape2D or c is CollisionPolygon2D:
# collision = c
# if interact_positions.default == null:
# interact_positions.default = c.global_position
func get_interact_position() -> Vector2:
var interact_position = 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
# but a dialog position for example. Interact position node must be set in the room scene.
if c.get_owner() == self:
continue
interact_position = c.global_position
if c is CollisionShape2D or c is CollisionPolygon2D:
if interact_position == null:
interact_position = c.global_position
return interact_position
func manage_input(viewport : Viewport, event : InputEvent, shape_idx : int):
if event is InputEventMouseButton:
if event.doubleclick:
if event.button_index == BUTTON_LEFT:
emit_signal("mouse_double_left_clicked_item", self, event)
else:
if event.is_pressed():
if event.button_index == BUTTON_LEFT:
emit_signal("mouse_left_clicked_item", self, event)
elif event.button_index == BUTTON_RIGHT:
emit_signal("mouse_right_clicked_item", self, event)
func _on_mouse_entered():
emit_signal("mouse_entered_item", self)
func _on_mouse_exited():
emit_signal("mouse_exited_item", self)
################################################################################
# TRIGGER functions
func element_entered(body):
if body is ESCBackground or body.get_parent() is ESCBackground:
return
escoria.do("trigger_in", [global_id, body.global_id, trigger_in_verb])
func element_exited(body):
if body is ESCBackground or body.get_parent() is ESCBackground:
return
escoria.do("trigger_out", [global_id, body.global_id, trigger_out_verb])
################################################################################
# MOVING OBJECT functions
func teleport(target, angle : Object = null) -> void:
Movable.teleport(target, angle)
func walk_to(pos : Vector2, p_walk_context = null) -> void:
Movable.walk_to(pos, p_walk_context)
func set_speed(speed_value : int) -> void:
speed = speed_value
################################################################################
# TALKATIVE object functions
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

@@ -1,12 +0,0 @@
extends Node
const OBJ_DEFAULT_STATE = "default"
enum EVENT_LEVEL_STATE {
RETURN, # 0
YIELD, # 1
BREAK, # 2
REPEAT, # 3
CALL, # 4
JUMP # 5
}

View File

@@ -1,173 +0,0 @@
tool
extends KinematicBody2D
class_name ESCPlayer
func get_class():
return "ESCPlayer"
"""
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
var params_queue : Array
var terrain : ESCTerrain
# If the terrain node type is scalenodes
var terrain_is_scalenodes : bool
var check_maps = true
export(int) var speed : int = 300
export(float) var v_speed_damp : float = 1.0
var orig_speed : float
enum PLAYER_TASKS {
NONE,
WALK,
SLIDE
}
onready var task = PLAYER_TASKS.NONE # type PLAYER_TASKS
enum Directions {
NORTH = 0, # 0
NORTHEAST = 1, # 1
EAST = 2, # 2
SOUTHEAST = 3, # 3
SOUTH = 4, # 4
SOUTHWEST = 5, # 5
WEST = 6, # 6
NORTHWEST = 7, # 7
TOP = 0,
TOP_RIGHT = 1
RIGHT = 2,
BOTTOM_RIGHT = 3,
BOTTOM = 4,
BOTTOM_LEFT = 5,
LEFT = 6,
TOP_LEFT = 7,
}
# Animations script (for walking, idling...)
export(Script) var animations
# AnimatedSprite node (if any)
var animation_sprite
# AnimationPlayer node (if any)
## NOT USED YET
#var animation
var collision
# Dialogs parameters
export(NodePath) var dialog_position_node
export(Color) var dialog_color = ColorN("white")
# Camera parameters
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.event_manager.connect("event_finished", self, "update_terrain")
# assert(is_angle_in_interval(0, [340,40])) # true
# assert(is_angle_in_interval(359, [340,40])) # true
# assert(is_angle_in_interval(1, [340,40])) # true
# assert(!is_angle_in_interval(90, [340,40])) # false
#
# assert(is_angle_in_interval(90, [70,40])) #true
# assert(!is_angle_in_interval(180, [70,40])) #false
#
# assert(is_angle_in_interval(179, [160, 40])) #true
# assert(is_angle_in_interval(180, [160, 40])) #true
# assert(is_angle_in_interval(181, [160, 40])) #true
# assert(!is_angle_in_interval(0, [160, 40])) #false
#
# assert(is_angle_in_interval(270, [250, 40])) # true
# assert(!is_angle_in_interval(270, [70,40])) #false
for n in get_children():
if n is AnimatedSprite:
animation_sprite = n
# for sprite_child in n.get_children():
# if sprite_child is AnimationPlayer:
# animation = sprite_child
# break
if n is CollisionShape2D or n is CollisionPolygon2D:
collision = n
animation_sprite.connect("animation_finished", self, "anim_finished")
if Engine.is_editor_hint():
return
terrain = escoria.room_terrain
Movable.last_scale = scale
# set_process(true)
func _process(time):
if Engine.is_editor_hint():
return
$debug.text = str(z_index)
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[Movable.last_dir][0])
func stop_talking():
if animation_sprite.is_playing():
animation_sprite.stop()
animation_sprite.play(animations.idles[Movable.last_dir][0])
func teleport(target, angle : Object = null) -> void:
Movable.teleport(target, angle)
func walk_to(pos : Vector2, p_walk_context = null):
return Movable.walk_to(pos, p_walk_context)
func set_angle(deg : int, immediate = true):
Movable.set_angle(deg, immediate)
func set_speed(speed_value : int) -> void:
speed = speed_value
func update_terrain(rc: int, event_name: String) -> void:
Movable.update_terrain(event_name)

View File

@@ -1,63 +0,0 @@
tool
extends "res://addons/escoria-core/game/core-scripts/escterrain_base.gd"
class_name ESCTerrain
func get_class():
return "ESCTerrain"
export var scale_min = 0.3
export var scale_max = 1.0
var current_active_navigation_instance : NavigationPolygonInstance
func _ready():
var navigation_enabled_found = false
for n in get_children():
if n is NavigationPolygonInstance:
if n.enabled:
if navigation_enabled_found:
escoria.logger.report_errors("escterrain.gd:_ready()", ["Multiple NavigationPolygonInstances enabled at the same time."])
navigation_enabled_found = true
current_active_navigation_instance = n
if !Engine.is_editor_hint():
escoria.room_terrain = self
#path = ImagePathFinder.new()
_update_texture()
func get_scale_range(r):
r = scale_min + (scale_max - scale_min) * r
return Vector2(r, r)
func get_terrain(pos):
if scales == null || scales.get_data().is_empty():
return Color(1, 1, 1, 1)
return get_pixel(pos, scales.get_data())
func get_pixel(pos, p_image):
if pos.x + 1 >= p_image.get_width() || pos.y + 1 >= p_image.get_height() || pos.x < 0 || pos.y < 0:
return Color(1.0, 0.0, 0.0)
# `get_pixel()` is slow; this is accurate enough
# without interpolating neighboring pixels and accounting for fractions
p_image.lock()
var pixel = p_image.get_pixel(pos.x, pos.y)
p_image.unlock()
return pixel
func _draw():
if typeof(texture) == typeof(null):
return
if !Engine.is_editor_hint():
return
if debug_mode == 0:
return
var scale_vect = bitmaps_scale
var src = Rect2(0, 0, texture.get_width(), texture.get_height())
var dst = Rect2(0, 0, texture.get_width() * scale_vect.x, texture.get_height() * scale_vect.y)
draw_texture_rect_region(texture, dst, src)
#draw_texture(texture, Vector2(0, 0))

View File

@@ -1,177 +0,0 @@
tool
extends Navigation2D
export(Texture) var scales setget set_scales,get_scales
export var bitmaps_scale = Vector2(1,1) setget set_bm_scale,get_bm_scale
export(Texture) var lightmap setget set_lightmap,get_lightmap
var lightmap_data
#warning-ignore:unused_class_variable
export var player_speed_multiplier = 1.0 # Override player speed in current scene
#warning-ignore:unused_class_variable
export var player_doubleclick_speed_multiplier = 1.5 # Make the player move faster when doubleclicked
export var lightmap_modulate = Color(1, 1, 1, 1)
export(int, "None", "Scales", "Lightmap") var debug_mode = 1 setget debug_mode_updated
var texture
var img_area
var _texture_dirty = false
func set_bm_scale(p_scale):
bitmaps_scale = p_scale
_update_texture()
func get_bm_scale():
return bitmaps_scale
func set_lightmap(p_lightmap):
var need_init = (lightmap != p_lightmap) or (lightmap and not lightmap_data)
lightmap = p_lightmap
# It's bad enough a new copy is created when reading a pixel, we don't
# also need to get the data for every read to make yet another copy
if need_init:
if lightmap_data:
lightmap_data.unlock()
lightmap_data = lightmap.get_data()
lightmap_data.lock()
_update_texture()
func get_lightmap():
return lightmap
func set_scales(p_scales):
scales = p_scales
_update_texture()
func get_scales():
return scales
func debug_mode_updated(p_mode):
debug_mode = p_mode
_update_texture()
func _update_texture():
if _texture_dirty:
return
_texture_dirty = true
call_deferred("_do_update_texture")
func _do_update_texture():
_texture_dirty = false
if !is_inside_tree():
return
if !Engine.is_editor_hint():
return
if debug_mode == 0:
update()
return
texture = ImageTexture.new()
if debug_mode == 1:
if scales != null:
#texture.create_from_image(scales)
texture = scales
else:
if lightmap != null:
#texture.create_from_image(lightmap)
texture = lightmap
update()
func make_local(pos):
pos = pos - get_position()
pos = pos * 1.0 / get_scale()
pos = get_closest_point(pos)
return pos
func make_global(pos):
pos = pos * get_scale()
pos = pos + get_position()
return pos
func get_terrain_path(p_src, p_dest):
# printt("get path ", p_src, p_dest)
p_src = make_local(p_src)
p_dest = make_local(p_dest)
var r_path = get_simple_path(p_src, p_dest, true)
r_path = Array(r_path)
for i in range(0, r_path.size()):
r_path[i] = make_global(r_path[i])
return r_path
func is_solid(pos):
pos = pos - get_position()
pos = pos * 1.0 / get_scale()
var closest = get_closest_point(pos)
return pos == closest
func _color_mul(a, b):
var c = Color()
c.r = a.r * b.r
c.g = a.g * b.g
c.b = a.b * b.b
c.a = a.a * b.a
return c
func get_light(pos):
if not lightmap or lightmap.get_data().is_empty():
return
return _color_mul(get_pixel(pos, lightmap_data), lightmap_modulate)
func get_pixel(pos, p_image):
p_image.lock()
pos = make_local(pos)
pos = pos * 1.0 / bitmaps_scale
if pos.x + 1 >= p_image.get_width() || pos.y + 1 >= p_image.get_height() || pos.x < 0 || pos.y < 0:
return Color()
var ll = p_image.get_pixel(pos.x, pos.y)
var ndif = Vector2()
ndif.x = pos.x - floor(pos.x)
ndif.y = pos.y - floor(pos.y)
var ur
img_area = Rect2(0, 0, p_image.get_width(), p_image.get_height())
var lr = ll
if ndif.x > 0 && img_area.has_point(Vector2(pos.x+1, pos.y)):
lr = p_image.get_pixel(pos.x+1, pos.y)
#if lr.a < 128:
# lr = ll
ur = lr
var ul = ll
if ndif.y > 0 && img_area.has_point(Vector2(pos.x, pos.y+1)):
ul = p_image.get_pixel(pos.x, pos.y+1)
#if ul.a < 128:
# ul = ll
ur = ul
if ndif.x > 0 && ndif.y > 0 && img_area.has_point(Vector2(pos.x+1, pos.y+1)):
var pix = p_image.get_pixel(pos.x+1, pos.y+1)
#if pix.a > 128:
ur = pix
var bottom = ll.linear_interpolate(lr, ndif.x)
var top
if ur != null:
top = ul.linear_interpolate(ur, ndif.x)
else:
top = ul
var final = bottom.linear_interpolate(top, ndif.y)
p_image.unlock()
return final

View File

@@ -1,92 +0,0 @@
tool
extends "terrain_base.gd"
const DIST_EPSILON = 0.000001
var scale_nodes = []
onready var scale_min = $"scale_min"
onready var scale_max = $"scale_max"
func debug_mode_updated(p_mode):
debug_mode = p_mode
._update_texture()
func _do_update_texture():
_texture_dirty = false
if !is_inside_tree():
return
if !Engine.is_editor_hint():
return
if debug_mode == 0:
update()
return
texture = ImageTexture.new()
if lightmap != null:
#texture.create_from_image(lightmap)
texture = lightmap
update()
static func sort_by_y(a, b):
return a.global_position.y < b.global_position.y
# Return a "scale range" immediately based on the interpolated scale size
func get_terrain(pos):
# printt("Called", pos)
var prev
var next
var prev_target
var node_target
for i in range(1, scale_nodes.size()):
prev = scale_nodes[i - 1]
next = scale_nodes[i]
if prev.global_position.y < pos.y and pos.y < next.global_position.y:
# printt("1:", prev.global_position.y, " < ", pos.y, " and ", pos.y, " < ", next.global_position.y)
prev_target = prev.target_scale.y
node_target = next.target_scale.y
break
var nodes_dist = next.global_position.y - prev.global_position.y
if nodes_dist < DIST_EPSILON:
nodes_dist = DIST_EPSILON
var interp_dist = (pos.y - prev.global_position.y) / nodes_dist
var y_1 = Vector2(0, prev_target)
var y_2 = Vector2(0, node_target)
var interp = y_1.linear_interpolate(y_2, interp_dist)
return Vector2(interp.y, interp.y)
func get_pixel(pos, p_image):
if pos.x + 1 >= p_image.get_width() || pos.y + 1 >= p_image.get_height() || pos.x < 0 || pos.y < 0:
return Color(1.0, 0.0, 0.0)
# `get_pixel()` is slow; this is accurate enough
# without interpolating neighboring pixels and accounting for fractions
return p_image.get_pixel(pos.x, pos.y)
func _draw():
if not texture:
return
if debug_mode == 0:
return
draw_texture(texture, Vector2(0, 0))
func _ready():
for c in get_children():
if c is preload("scalenode.gd"):
scale_nodes.push_back(c)
scale_nodes.sort_custom(self, "sort_by_y")
scale_nodes.push_front(scale_min)
scale_nodes.push_back(scale_max)

View File

@@ -1,9 +0,0 @@
extends Node
func get_inventory_item(item_id : String) -> ESCInventoryItem:
for c in get_children():
if c.global_id == item_id:
if c.inventory_item_scene_file:
return c.inventory_item_scene_file.instance()
return null

View File

@@ -1,5 +0,0 @@
extends Position2D
#warning-ignore:unused_class_variable
export(Vector2) var target_scale = Vector2(1.0, 1.0)

View File

@@ -1,4 +0,0 @@
extends Position2D
#warning-ignore:unused_class_variable
export(Vector2) var target_scale = Vector2(1.0, 1.0)

View File

@@ -1,6 +1,15 @@
# A set of common utilities
extends Object
class_name ESCUtils
# Helpers to deal with player's and items' angles
func _get_deg_from_rad(rad_angle : float):
# Convert radians to degrees
#
# #### Parameters
#
# - rad_angle: Angle in radians
# **Returns** Degrees
func get_deg_from_rad(rad_angle : float):
var deg = rad2deg(rad_angle)
if deg >= 360.0:
deg = clamp(deg, 0.0, 360.0)

View File

@@ -1,14 +1,24 @@
# The escorie main script
extends Node
# Scripts
onready var main = $main
onready var inputs_manager = $inputs_manager
onready var utils = load("res://addons/escoria-core/game/core-scripts/utils/utils.gd").new()
onready var save_data = load("res://addons/escoria-core/game/core-scripts/save_data/save_data.gd").new()
# Current game state
# * DEFAULT: Common game function
# * DIALOG: Game is playing a dialog
# * WAIT: Game is waiting
enum GAME_STATE {
DEFAULT,
DIALOG,
WAIT
}
# Logger used
var logger: ESCLogger
# Several utilities
var utils: ESCUtils
# The inventory manager instance
var inventory_manager: ESCInventoryManager
@@ -30,30 +40,24 @@ var object_manager: ESCObjectManager
# ESC command registry instance
var command_registry: ESCCommandRegistry
var resource_cache: ResourceCache
# Resource cache handler
var resource_cache: ESCResourceCache
# INSTANCES
# Instance of the main menu
var main_menu_instance
## Dialog player instantiator. This instance is called directly for dialogs.
var dialog_player
## Inventory scene
var inventory
# Game variables
# Terrain of the current room
var room_terrain
enum GAME_STATE {
DEFAULT,
DIALOG,
WAIT
}
onready var current_state = GAME_STATE.DEFAULT
# Dialog player instantiator. This instance is called directly for dialogs.
var dialog_player
onready var game_size = get_viewport().size
# Inventory scene
var inventory
# These are settings that the player can affect and save/load later
var settings : Dictionary
# These are default settings
var settings_default : Dictionary = {
# Text language
@@ -64,7 +68,7 @@ var settings_default : Dictionary = {
"speech_enabled": ProjectSettings.get_setting("escoria/sound/speech_enabled"),
# Master volume (max is 1.0)
"master_volume": ProjectSettings.get_setting("escoria/sound/master_volume"),
# Music volume (max is 1.0)
# Music volume (max is 1.0)
"music_volume": ProjectSettings.get_setting("escoria/sound/music_volume"),
# Sound effects volume (max is 1.0)
"sfx_volume": ProjectSettings.get_setting("escoria/sound/sfx_volume"),
@@ -79,8 +83,26 @@ var settings_default : Dictionary = {
}
# The current state of the game
onready var current_state = GAME_STATE.DEFAULT
# The game resolution
onready var game_size = get_viewport().size
# The main scene
onready var main = $main
# The escoria inputs manager
onready var inputs_manager = $inputs_manager
# Savegame management
onready var save_data = load("res://addons/escoria-core/game/core-scripts/save_data/save_data.gd").new()
# Initialize various objects
func _init():
self.logger = ESCLogger.new()
self.utils = ESCUtils.new()
self.inventory_manager = ESCInventoryManager.new()
self.action_manager = ESCActionManager.new()
self.event_manager = ESCEventManager.new()
@@ -89,10 +111,11 @@ func _init():
self.object_manager = ESCObjectManager.new()
self.command_registry = ESCCommandRegistry.new()
self.esc_compiler = ESCCompiler.new()
self.resource_cache = ResourceCache.new()
self.resource_cache = ESCResourceCache.new()
self.resource_cache.start()
# Load settings
func _ready():
save_data.start()
save_data.check_settings()
@@ -101,8 +124,6 @@ func _ready():
escoria._on_settings_loaded(escoria.settings)
##################################################################################
# Called by Main menu "start new game"
func new_game():
var script = self.esc_compiler.load_esc_file(
@@ -112,19 +133,21 @@ func new_game():
var rc = yield(event_manager, "event_finished")
while rc[1] != "start":
rc = yield(event_manager, "event_finished")
if rc[0] != ESCExecution.RC_OK:
self.logger.report_errors(
"Start event of the start script returned unsuccessful: %d" % rc[0],
[]
)
return
"""
Generic action function that runs an action on an element of the room (eg player walk)
action: type of the action ()
"""
# Run a generic action
#
# #### Parameters
#
# - action: type of the action to run
# - params: Parameters for the action
func do(action : String, params : Array = []) -> void:
if current_state == GAME_STATE.DEFAULT:
match action:
@@ -134,10 +157,10 @@ func do(action : String, params : Array = []) -> void:
# Check moving object.
if not self.object_manager.has(params[0]):
self.logger.report_errors(
"escoria.gd:do()",
"escoria.gd:do()",
[
"Walk action requested on inexisting object: %s "\
% params[0]
"Walk action requested on inexisting " + \
"object: %s " % params[0]
]
)
return
@@ -151,14 +174,18 @@ func do(action : String, params : Array = []) -> void:
var is_fast : bool = false
if params.size() > 2 and params[2] == true:
is_fast = true
var walk_context = {"fast": is_fast, "target": target_position}
var walk_context = ESCWalkContext.new(
null,
target_position,
is_fast
)
moving_obj.walk_to(target_position, walk_context)
# Walk to object from its id
elif params[1] is String:
if not self.object_manager.has(params[1]):
self.logger.report_errors(
"escoria.gd:do()",
"escoria.gd:do()",
[
"Walk action requested TOWARDS " +\
"inexisting object: %s" % params[1]
@@ -168,31 +195,45 @@ func do(action : String, params : Array = []) -> void:
var object = self.object_manager.get_object(params[1])
if object:
var target_position : Vector2 = object.node.interact_position
var target_position : Vector2 = \
object.node.interact_position
var is_fast : bool = false
if params.size() > 2 and params[2] == true:
is_fast = true
var walk_context = {"fast": is_fast, "target_object" : object}
var walk_context = ESCWalkContext.new(
object,
Vector2(),
is_fast
)
moving_obj.walk_to(target_position, walk_context)
"item_left_click":
if params[0] is String:
self.logger.info("escoria.do() : item_left_click on item ", [params[0]])
self.logger.info(
"escoria.do() : item_left_click on item ",
[params[0]]
)
var item = self.object_manager.get_object(params[0])
ev_left_click_on_item(item, params[1])
_ev_left_click_on_item(item, params[1])
"item_right_click":
if params[0] is String:
self.logger.info("escoria.do() : item_right_click on item ", [params[0]])
self.logger.info(
"escoria.do() : item_right_click on item ",
[params[0]]
)
var item = self.object_manager.get_object(params[0])
ev_left_click_on_item(item, params[1], true)
_ev_left_click_on_item(item, params[1], true)
"trigger_in":
var trigger_id = params[0]
var object_id = params[1]
var trigger_in_verb = params[2]
self.logger.info("escoria.do() : trigger_in " + trigger_id + " by " + object_id)
self.logger.info("escoria.do() : trigger_in %s by %s" % [
trigger_id,
object_id
])
self.event_manager.queue_event(
object_manager.get_object(trigger_id).events[
trigger_in_verb
@@ -203,7 +244,10 @@ func do(action : String, params : Array = []) -> void:
var trigger_id = params[0]
var object_id = params[1]
var trigger_out_verb = params[2]
self.logger.info("escoria.do() : trigger_out " + trigger_id + " by " + object_id)
self.logger.info("escoria.do() : trigger_out %s by %s" % [
trigger_id,
object_id
])
self.event_manager.queue_event(
object_manager.get_object(trigger_id).events[
trigger_out_verb
@@ -211,26 +255,27 @@ func do(action : String, params : Array = []) -> void:
)
_:
self.logger.report_warnings("escoria.gd:do()",
self.logger.report_warnings("escoria.gd:do()",
["Action received:", action, "with params ", params])
elif current_state == GAME_STATE.WAIT:
pass
# PRIVATE
func ev_left_click_on_item(obj, event, default_action = false):
"""
Event occurring when an object/item is left clicked
obj : object that was left clicked
event :
"""
# Event handler when an object/item was clicked
# FIXME this method is way to complex
#
# #### Parameters
#
# - ob: Object that was left clicked
# - event: Input event that was received
# - default_action: Run the inventory default action
func _ev_left_click_on_item(obj, event, default_action = false):
if obj is String:
obj = object_manager.get_object(obj)
self.logger.info(obj.global_id + " left-clicked with event ", [event])
var need_combine = false
# Check if current_action and current_tool are already set
# Check if current_action and current_tool are already set
if self.action_manager.current_action:
if self.action_manager.current_tool:
if self.action_manager.current_action in self.action_manager\
@@ -251,23 +296,24 @@ func ev_left_click_on_item(obj, event, default_action = false):
self.action_manager.current_tool = obj
# Don't interact after player movement towards object (because object is inactive for example)
# Don't interact after player movement towards object
# (because object is inactive for example)
var dont_interact = false
var destination_position : Vector2 = main.current_scene.player.global_position
var destination_position : Vector2 = main.current_scene.player.\
global_position
# Create walk context
var walk_context = {"fast": event.doubleclick, "target_object" : obj.node}
var walk_context = ESCWalkContext.new(
obj,
Vector2(),
event.doubleclick
)
# If object not in inventory, player walks towards it
if not inventory_manager.inventory_has(obj.global_id):
var clicked_object_has_interact_position = false
if object_manager.get_object(obj.global_id).interactive:
# if obj.interact_positions.default != null:
# destination_position = obj.interact_positions.default#.global_position
# clicked_object_has_interact_position = true
# else:
# destination_position = obj.position
if obj.node.get_interact_position() != null:
destination_position = obj.node.get_interact_position()
clicked_object_has_interact_position = true
@@ -277,20 +323,25 @@ func ev_left_click_on_item(obj, event, default_action = false):
destination_position = event.position
dont_interact = true
# Use ESC for this?
var is_already_walking = main.current_scene.player.walk_to(destination_position, walk_context)
main.current_scene.player.walk_to(
destination_position,
walk_context
)
# Wait for the player to arrive before continuing with action.
var context = yield(main.current_scene.player, "arrived")
var context: ESCWalkContext = yield(
main.current_scene.player,
"arrived"
)
self.logger.info("Context arrived: ", [context])
if context.has("target_object") and walk_context.has("target_object"):
if (context.target_object.global_id != walk_context.target_object.global_id) \
or (context.target_object.global_id == walk_context.target_object.global_id and is_already_walking):
dont_interact = true
elif context.has("target") and walk_context.has("target"):
if (context.target.global_id != walk_context.target.global_id) \
or (context.target.global_id == walk_context.target.global_id and is_already_walking):
dont_interact = true
if context.target_object and \
context.target_object.global_id != walk_context.\
target_object.global_id:
dont_interact = true
elif context.target_position != walk_context.target_position:
dont_interact = true
# If no interaction should happen after player has arrived, leave immediately.
if dont_interact:
@@ -318,22 +369,24 @@ func ev_left_click_on_item(obj, event, default_action = false):
# If apply_interact, perform combine between items
if need_combine:
self.action_manager.activate(
self.action_manager.current_action,
self.action_manager.current_tool,
self.action_manager.current_action,
self.action_manager.current_tool,
obj
)
else:
self.action_manager.activate(
self.action_manager.current_action,
self.action_manager.current_action,
obj
)
# else:
## escoria.fallback("")
# pass
func _on_settings_loaded(p_settings : Dictionary):
# Apply the loaded settings
#
# #### Parameters
#
# * p_settings: Loaded settings
func _on_settings_loaded(p_settings : Dictionary) -> void:
escoria.logger.info("******* settings loaded", p_settings)
if p_settings != null:
settings = p_settings
@@ -346,9 +399,18 @@ func _on_settings_loaded(p_settings : Dictionary):
# TODO Apply globally
# AudioServer.set_fx_global_volume_scale(settings.sfx_volume)
AudioServer.set_bus_volume_db(AudioServer.get_bus_index("Master"), linear2db(settings.master_volume))
AudioServer.set_bus_volume_db(AudioServer.get_bus_index("SFX"), linear2db(settings.sfx_volume))
AudioServer.set_bus_volume_db(AudioServer.get_bus_index("Music"), linear2db(settings.music_volume))
AudioServer.set_bus_volume_db(
AudioServer.get_bus_index("Master"),
linear2db(settings.master_volume)
)
AudioServer.set_bus_volume_db(
AudioServer.get_bus_index("SFX"),
linear2db(settings.sfx_volume)
)
AudioServer.set_bus_volume_db(
AudioServer.get_bus_index("Music"),
linear2db(settings.music_volume)
)
TranslationServer.set_locale(settings.text_lang)
# music_volume_changed()

View File

@@ -1,15 +1,22 @@
# Escoria inputs manager
# Catches, handles and distributes input events for the game
tool
extends Node
# A LIFO stack of hovered items
onready var hover_stack : Array = []
# The global id fo the topmost item from the hover_stack
onready var hotspot_focused : String = ""
func _ready():
set_process_input(true)
func _input(event):
# Input event handler
#
# #### Parameters
#
# - event: Godot input event received
func _input(event: InputEvent) -> void:
if event.is_action_pressed("esc_show_debug_prompt"):
escoria.main.get_node("layers/debug_layer/esc_prompt_popup").popup()
@@ -21,57 +28,97 @@ func _input(event):
if event is InputEventMouseMotion:
escoria.main.current_scene.game.update_tooltip_following_mouse_position(event.position)
###################################################################################
func _on_left_click_on_bg(position : Vector2):
# The background was clicked with the LMB
#
# #### Parameters
#
# - position: Position of the click
func _on_left_click_on_bg(position: Vector2) -> void:
if hotspot_focused.empty():
escoria.logger.info("Left click on background at ", [str(position)])
escoria.main.current_scene.game.left_click_on_bg(position)
func _on_double_left_click_on_bg(position : Vector2):
# The background was double-clicked with the LMB
#
# #### Parameters
#
# - position: Position of the click
func _on_double_left_click_on_bg(position: Vector2) -> void:
if hotspot_focused.empty():
escoria.logger.info("Double left click on background at ", [str(position)])
escoria.main.current_scene.game.left_double_click_on_bg(position)
func _on_right_click_on_bg(position : Vector2):
# The background was clicked with the RMB
#
# #### Parameters
#
# - position: Position of the click
func _on_right_click_on_bg(position: Vector2) -> void:
if hotspot_focused.empty():
escoria.logger.info("Right click on background at ", [str(position)])
escoria.main.current_scene.game.right_click_on_bg(position)
##################################################################################
func _on_mouse_left_click_inventory_item(inventory_item_global_id, event : InputEvent) -> void:
# An inventory item was clicked with the LMB
#
# #### Parameters
#
# - inventory_item_global_id: The global id of the clicked inventory item
# - event: The input event received
func _on_mouse_left_click_inventory_item(inventory_item_global_id: String, event: InputEvent) -> void:
escoria.logger.info("Inventory item left clicked ", [inventory_item_global_id])
escoria.main.current_scene.game.left_click_on_inventory_item(inventory_item_global_id, event)
func _on_mouse_right_click_inventory_item(inventory_item_global_id, event : InputEvent) -> void:
# An inventory item was clicked with the RMB
#
# #### Parameters
#
# - inventory_item_global_id: The global id of the clicked inventory item
# - event: The input event received
func _on_mouse_right_click_inventory_item(inventory_item_global_id: String, event: InputEvent) -> void:
escoria.logger.info("Inventory item right clicked ", [inventory_item_global_id])
escoria.main.current_scene.game.right_click_on_inventory_item(inventory_item_global_id, event)
func _on_mouse_double_left_click_inventory_item(inventory_item_global_id, event : InputEvent) -> void:
# An inventory item was doublce-clicked with the LMB
#
# #### Parameters
#
# - inventory_item_global_id: The global id of the clicked inventory item
# - event: The input event received
func _on_mouse_double_left_click_inventory_item(inventory_item_global_id: String, event: InputEvent) -> void:
escoria.logger.info("Inventory item double left clicked ", [inventory_item_global_id])
escoria.main.current_scene.game.left_double_click_on_inventory_item(inventory_item_global_id, event)
func _on_mouse_entered_inventory_item(inventory_item_global_id) -> void:
# The mouse entered an inventory item
#
# #### Parameters
#
# - inventory_item_global_id: The global id of the inventory item that is hovered
func _on_mouse_entered_inventory_item(inventory_item_global_id: String) -> void:
escoria.logger.info("Inventory item focused ", [inventory_item_global_id])
escoria.main.current_scene.game.inventory_item_focused(inventory_item_global_id)
# The mouse exited an inventory item
func _on_mouse_exited_inventory_item() -> void:
escoria.logger.info("Inventory item unfocused")
escoria.main.current_scene.game.inventory_item_unfocused()
##################################################################################
func _on_mouse_entered_item(item : ESCItem) -> void:
# The mouse entered an Escoria item
#
# #### Parameters
#
# - item: The Escoria item hovered
func _on_mouse_entered_item(item: ESCItem) -> void:
escoria.logger.info("Item focused : ", [item.global_id])
clean_hover_stack()
_clean_hover_stack()
if !hover_stack.empty():
if item.z_index > hover_stack.back().z_index:
@@ -85,9 +132,14 @@ func _on_mouse_entered_item(item : ESCItem) -> void:
escoria.main.current_scene.game.element_focused(item.global_id)
# The mouse exited an Escoria item
#
# #### Parameters
#
# - item: The Escoria item hovered
func _on_mouse_exited_item(item : ESCItem) -> void:
escoria.logger.info("Item unfocused : ", [item.global_id])
hover_stack_pop(item)
_hover_stack_pop(item)
if hover_stack.empty():
hotspot_focused = ""
escoria.main.current_scene.game.element_unfocused()
@@ -95,40 +147,62 @@ func _on_mouse_exited_item(item : ESCItem) -> void:
hotspot_focused = hover_stack.back().global_id
escoria.main.current_scene.game.element_focused(hotspot_focused)
# An Escoria item was clicked with the LMB
#
# #### Parameters
#
# - item: The Escoria item clicked
# - event: The input event from the click
func _on_mouse_left_clicked_item(item : ESCItem, event : InputEvent) -> void:
if hover_stack.empty() or hover_stack.back() == item:
escoria.logger.info("Item left clicked", [item.global_id, event])
escoria.main.current_scene.game.left_click_on_item(item.global_id, event)
# An Escoria item was double-clicked with the LMB
#
# #### Parameters
#
# - item: The Escoria item clicked
# - event: The input event from the click
func _on_mouse_left_double_clicked_item(item : ESCItem, event : InputEvent) -> void:
escoria.logger.info("Item left double clicked", [item.global_id, event])
escoria.main.current_scene.game.left_double_click_on_item(item.global_id, event)
# An Escoria item was clicked with the RMB
#
# #### Parameters
#
# - item: The Escoria item clicked
# - event: The input event from the click
func _on_mouse_right_clicked_item(item : ESCItem, event : InputEvent) -> void:
escoria.logger.info("Item right clicked", [item.global_id, event])
escoria.main.current_scene.game.right_click_on_item(item.global_id, event)
##################################################################################
# The mousewheel was turned
#
# #### Parameters
#
# - direction: The direction the wheel was turned. 1 = up, -1 = down
func _on_mousewheel_action(direction : int):
escoria.main.current_scene.game.mousewheel_action(direction)
##################################################################################
# Event when the pause menu was requested
func _on_pause_menu_requested():
escoria.main.current_scene.game.pause_game()
func clean_hover_stack():
# Clean the hover stack
func _clean_hover_stack():
for e in hover_stack:
if e == null or !is_instance_valid(e):
hover_stack.erase(e)
func hover_stack_pop(item):
# Remove the given item from the stack
func _hover_stack_pop(item):
hover_stack.erase(item)
################################################################################
func _on_pause_menu_requested():
escoria.main.current_scene.game.pause_game()

View File

@@ -1,43 +1,55 @@
# Escoria main room handling and scene switcher
extends Node
# This script is basically the scene-switcher.
# Global id of the last scene the player was before current scene
var last_scene_global_id
# Current scene room being displayed
var current_scene
var last_scene_global_id: String
# Current scene room being displayed
var current_scene: Node
# The Escoria context currently in wait state
var wait_level
# FIXME Document this variable
var screen_ofs = Vector2(0, 0)
# ESCBackgroundMusic node
# Reference to the ESCBackgroundMusic node
onready var bg_music = $bg_music
# Reference to the scene transition node
onready var scene_transition = $layers/curtain/scene_transition
# Set the new current scene
# Connect the wait timer event
func _ready() -> void:
$layers/wait_timer.connect("timeout", self, "_on_wait_finished")
# Set current scene
#
# #### Parameters
#
# - p_scene: Current scene to set
func set_scene(p_scene: Node):
# - p_scene: Scene to set
func set_scene(p_scene: Node) -> void:
if !p_scene:
escoria.logger.report_errors("main", ["Trying to set empty scene"])
if current_scene != null:
clear_scene()
add_child(p_scene)
move_child(p_scene, 0)
current_scene = p_scene
check_game_scene_methods()
set_camera_limits()
func clear_scene():
# Cleanup the current scene
func clear_scene() -> void:
if current_scene == null:
return
@@ -48,14 +60,18 @@ func clear_scene():
current_scene.free()
current_scene = null
func wait(params : Array, level):
wait_level = level
$layers/wait_timer.set_wait_time(float(params[0]))
$layers/wait_timer.set_one_shot(true)
$layers/wait_timer.start()
# Triggered, when the wait has finished
func _on_wait_finished() -> void:
escoria.esc_level_runner.finished(wait_level)
func set_camera_limits(camera_limit_id : int = 0):
# Set the camera limits
#
# #### Parameters
#
# * camera_limits_id: The id of the room's camera limits to set
func set_camera_limits(camera_limit_id : int = 0) -> void:
var limits = {}
var scene_camera_limits = current_scene.camera_limits[camera_limit_id]
if scene_camera_limits.size.x == 0 and scene_camera_limits.size.y == 0:
@@ -65,10 +81,14 @@ func set_camera_limits(camera_limit_id : int = 0):
area = child.get_full_area_rect2()
break
# if the background is smaller than the viewport, we want the camera to stick centered on the background
if area.size.x == 0 or area.size.y == 0 or area.size < get_viewport().size:
escoria.logger.report_warning("main.gd:set_camera_limits()",
"No limit area! Using viewport.")
# if the background is smaller than the viewport, we want the camera
# to stick centered on the background
if area.size.x == 0 or area.size.y == 0 \
or area.size < get_viewport().size:
escoria.logger.report_warning(
"main.gd:set_camera_limits()",
"No limit area! Using viewport."
)
area.size = get_viewport().size
escoria.logger.info("Setting camera limits from scene ", [area])
@@ -82,21 +102,25 @@ func set_camera_limits(camera_limit_id : int = 0):
else:
limits = {
"limit_left": scene_camera_limits.position.x,
"limit_right": scene_camera_limits.position.x + scene_camera_limits.size.x,
"limit_right": scene_camera_limits.position.x + \
scene_camera_limits.size.x,
"limit_top": scene_camera_limits.position.y,
"limit_bottom": scene_camera_limits.position.y + scene_camera_limits.size.y + screen_ofs.y * 2,
"limit_bottom": scene_camera_limits.position.y + \
scene_camera_limits.size.y + screen_ofs.y * 2,
"set_default": true,
}
escoria.logger.info("Setting camera limits from parameter ", [scene_camera_limits])
escoria.logger.info(
"Setting camera limits from parameter ",
[scene_camera_limits]
)
current_scene.game.get_node("camera").set_limits(limits)
current_scene.game.get_node("camera").set_offset(screen_ofs * 2)
"""
The game.tscn scene's root node script MUST implement the following methods.
If they do not exist, stop immediately. Implement them, even if empty
"""
# Sanity check that the game.tscn scene's root node script MUST
# implement the following methods. If they do not exist, stop immediately.
# Implement them, even if empty
func check_game_scene_methods():
assert(current_scene.game.has_method("left_click_on_bg"))
assert(current_scene.game.has_method("right_click_on_bg"))

View File

@@ -1,10 +1,13 @@
extends Node
# Main_scene is the entry point for Godot Engine.
# This scene sets up the main menu scene to load.
extends Node
# Start the main menu
func _ready():
var main_menu_path = ProjectSettings.get_setting("escoria/ui/main_menu_scene")
var main_menu_path = ProjectSettings.get_setting(
"escoria/ui/main_menu_scene"
)
var main_menu_scene = load(main_menu_path).instance()
# get_tree().get_root().call_deferred("add_child", main_menu_scene)
escoria.call_deferred("add_child", main_menu_scene)

View File

@@ -177,7 +177,7 @@ func _process(_delta):
if typeof(target) == TYPE_VECTOR2 or typeof(target) == TYPE_ARRAY:
self.global_position = resolve_target_pos()
elif "moved" in target and target.moved \
or "moved" in target.Movable and target.Movable.moved:
or "moved" in target.movable and target.movable.moved:
self.global_position = resolve_target_pos()
func _ready():

View File

@@ -46,23 +46,30 @@ func add_new_item_by_id(item_id : String) -> void:
item_id = item_id.rsplit("i/", false)[0]
if not items_ids_in_inventory.has(item_id):
if not escoria.object_manager.has(item_id):
escoria.logger.report_errors(
"inventory_ui.gd:add_new_item_by_id()",
[
"Item global id '%s' does not exist." % item_id,
"Check item's id in ESCORIA_ALL_ITEMS scene."
]
)
if not all_items.get_inventory_item(item_id):
escoria.logger.report_errors(
"inventory_ui.gd:add_new_item_by_id()",
[
"Item global id '%s' doesn't have a " +\
"corresponding inventory item." % item_id,
"Check item's id in ESCORIA_ALL_ITEMS scene."
]
)
var item_inventory_button = all_items.get_inventory_item(item_id).duplicate()
var inventory_file = "%s/%s.tscn" % [
ProjectSettings.get_setting(
"escoria/ui/items_autoregister_path"
).trim_suffix("/"),
item_id
]
if ResourceLoader.exists(inventory_file):
escoria.object_manager.register_object(
ESCObject.new(
item_id,
ResourceLoader.load(inventory_file).instance()
)
)
else:
escoria.logger.report_errors(
"inventory_ui.gd:add_new_item_by_id()",
[
"Item global id '%s' does not exist." % item_id,
"Check item's id in ESCORIA_ALL_ITEMS scene."
]
)
var item_inventory_button = (
escoria.object_manager.get_object(item_id).node as ESCItem
).inventory_item.duplicate()
items_ids_in_inventory[item_id] = item_inventory_button
get_node(inventory_ui_container).add_item(item_inventory_button)

View File

@@ -1,6 +1,6 @@
[gd_scene load_steps=3 format=2]
[ext_resource path="res://addons/escoria-core/game/core-scripts/escinventoryitem.gd" type="Script" id=1]
[ext_resource path="res://addons/escoria-core/game/core-scripts/esc_inventory_item.gd" type="Script" id=1]
[ext_resource path="res://addons/escoria-core/game/assets/images/no_image.png" type="Texture" id=2]
[node name="inventory_item" type="TextureButton"]

View File

@@ -1,10 +0,0 @@
[gd_scene load_steps=2 format=2]
[ext_resource path="res://addons/escoria-core/game/core-scripts/items_inventory.gd" type="Script" id=1]
[node name="items_inventory" type="GridContainer"]
columns = 5
script = ExtResource( 1 )
__meta__ = {
"_edit_use_anchors_": false
}

View File

@@ -1,9 +1,9 @@
[gd_scene load_steps=6 format=2]
[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/escitem.gd" type="Script" id=3]
[ext_resource path="res://addons/escoria-core/game/core-scripts/escroom.gd" type="Script" id=4]
[ext_resource path="res://addons/escoria-core/game/core-scripts/esc_terrain.gd" type="Script" id=1]
[ext_resource path="res://addons/escoria-core/game/core-scripts/esc_background.gd" type="Script" id=2]
[ext_resource path="res://addons/escoria-core/game/core-scripts/esc_item.gd" type="Script" id=3]
[ext_resource path="res://addons/escoria-core/game/core-scripts/esc_room.gd" type="Script" id=4]
[sub_resource type="NavigationPolygon" id=1]

View File

@@ -107,7 +107,7 @@ var result_angles = []
func _ready():
# Find player animations
$player_animations.add_item("")
for anim_name in $player.get_animations_list():
for anim_name in $player.get_animation_player.get_sprite_frames().get_animation_names():
$player_animations.add_item(anim_name)
# Set initial angles