Add NPC and items walking.

This commit is contained in:
Julian Murgia
2020-12-28 08:39:57 +01:00
parent af8a9ea086
commit ff89dc1677
205 changed files with 972 additions and 488 deletions

View File

@@ -73,6 +73,7 @@ var commands = {
"turn_to": { "min_args": 2 },
"wait": true,
"walk": { "min_args": 2 },
"walk_to_pos": { "min_args": 3},
"walk_block": { "min_args": 2 },
"%": { "alias": "label", "min_args": 1},

View File

@@ -153,10 +153,10 @@ func add_level(p_event, p_root : bool):
func instance_level(p_event : esctypes.ESCEvent, p_root : bool):
var new_level = {
"ip": 0,
"instructions": p_event.ev_level,
"waiting": false,
"break_stop": p_root,
"ip": 0, # Current instruction id
"instructions": p_event.ev_level, # List of instructions (commands)
"waiting": false, # If true, wait for current command to be finished (esc_runner_level.finished())
"break_stop": p_root,
"labels": {},
"flags": p_event.ev_flags
}
@@ -514,8 +514,9 @@ func activate(p_action : String, p_param : Array):
else:
var errors = ["Attempted to execute inexisting action " + \
p_action + " between item " + combine_with.global_id + " and item " + what.global_id]
if combine_with.combine_is_one_way:
errors.append("Reason: " + combine_with.global_id + "'s item interaction is one-way.")
if combine_with.get("combine_is_one_way") != null \
and combine_with.combine_is_one_way:
errors.append("Reason: " + combine_with.global_id + "'s item interaction is one-way.")
escoria.report_warnings("esc_runner.gd:activate()", errors)
return esctypes.EVENT_LEVEL_STATE.YIELD
@@ -595,15 +596,24 @@ func set_state(global_id : String, p_params : Array):
if animation_node:
animation_node.stop()
if animation_node.has_animation(p_params[0]):
var actual_animator
if animation_node is AnimationPlayer:
actual_animator = animation_node
elif animation_node is AnimatedSprite:
actual_animator = animation_node.frames
if actual_animator.has_animation(p_params[0]):
if !immediate:
animation_node.play(p_params[0])
else:
# The animation is not played, we directly set it at its last frame
animation_node.current_animation = p_params[0]
var animation = animation_node.get_animation(p_params[0])
var animation_length = animation.length
animation_node.seek(animation_length)
if animation_node is AnimatedSprite:
animation_node.animation = p_params[0]
else:
animation_node.current_animation = p_params[0]
var animation = actual_animator.get_animation(p_params[0])
var animation_length = animation.length
animation_node.seek(animation_length)
"""
@@ -611,10 +621,11 @@ When object is active, it is VISIBLE.
When object is inactive, it is HIDDEN.
"""
func set_active(name : String, active):
if objects[name] is ESCInventoryItem:
return
actives[name] = active
if objects.has(name) and is_instance_valid(objects[name]):
if objects[name] is ESCInventoryItem:
return
if active:
objects[name].show()
else:
@@ -656,3 +667,11 @@ func object_exit_scene(name : String):
# break
# else:
# stack.remove(stack.size()-1)
func check_obj(name, cmd):
var obj = escoria.esc_runner.get_object(name)
if obj == null:
escoria.report_errors("", ["Global id "+name+" not found for " + cmd])
return false
return true

View File

@@ -20,14 +20,6 @@ func finished(context = null):
escoria.current_state = escoria.GAME_STATE.DEFAULT
func check_obj(name, cmd):
var obj = escoria.esc_runner.get_object(name)
if obj == null:
escoria.report_errors("", ["Global id "+name+" not found for " + cmd])
return false
return true
func resume(context):
current_context = context
if context.waiting:
@@ -111,7 +103,7 @@ Optional parameters:
flip_y flips the y axis of the object's sprites when true (object's root node needs to be Node2D)
"""
func anim(command_params : Array):
if !check_obj(command_params[0], "anim"):
if !escoria.esc_runner.check_obj(command_params[0], "anim"):
return esctypes.EVENT_LEVEL_STATE.RETURN
private_play_animation(command_params)
return esctypes.EVENT_LEVEL_STATE.RETURN
@@ -248,7 +240,7 @@ Optional parameters:
(object's root node needs to be Node2D)
"""
func cut_scene(command_params : Array):
if !check_obj(command_params[0], "cut_scene"):
if !escoria.esc_runner.check_obj(command_params[0], "cut_scene"):
return esctypes.EVENT_LEVEL_STATE.RETURN
private_play_animation(command_params)
return esctypes.EVENT_LEVEL_STATE.YIELD
@@ -437,7 +429,7 @@ Sets object as active or inactive. Active objects are displayed in scene and res
to inputs. Inactives are hidden.
"""
func set_active(command_params : Array):
if !check_obj(command_params[0], "set_active"):
if !escoria.esc_runner.check_obj(command_params[0], "set_active"):
return esctypes.EVENT_LEVEL_STATE.RETURN
var name : String = command_params[0]
var value = command_params[1]
@@ -448,7 +440,7 @@ Set the angle of an object.
Usage: set_angle object_id angle_degrees
"""
func set_angle(command_params : Array):
if !check_obj(command_params[0], "set_angle"):
if !escoria.esc_runner.check_obj(command_params[0], "set_angle"):
return esctypes.EVENT_LEVEL_STATE.RETURN
var obj = escoria.esc_runner.get_object(command_params[0])
obj.set_angle(int(command_params[1]))
@@ -539,9 +531,9 @@ angle to angle_degrees.
Usage: teleport obj1 obj2 [angle_degrees]
"""
func teleport(command_params : Array):
if !check_obj(command_params[0], "teleport"):
if !escoria.esc_runner.check_obj(command_params[0], "teleport"):
return esctypes.EVENT_LEVEL_STATE.RETURN
if !check_obj(command_params[1], "teleport"):
if !escoria.esc_runner.check_obj(command_params[1], "teleport"):
return esctypes.EVENT_LEVEL_STATE.RETURN
var angle
@@ -585,7 +577,19 @@ Make object1 walk towards object2. This command is not blocking (user input not
Usage: walk object_id1 object_id2
"""
func walk(command_params : Array):
current_context.waiting = true
escoria.do("walk", command_params)
return esctypes.EVENT_LEVEL_STATE.YIELD
"""
Make object1 walk towards object2. This command is not blocking (user input not disabled)
Usage: walk_to_pos object_id1 pos_x pos_y
"""
func walk_to_pos(command_params : Array):
current_context.waiting = true
var destination_pos = Vector2(command_params[1], command_params[2])
escoria.do("walk", [command_params[0], destination_pos])
return esctypes.EVENT_LEVEL_STATE.YIELD
"""

View File

@@ -41,7 +41,6 @@ var terrain : ESCTerrain
# If the terrain node type is scalenodes
var terrain_is_scalenodes : bool
var check_maps = true
var pose_scale : int
var last_scale : Vector2
@@ -59,7 +58,6 @@ func _ready():
init_interact_position_with_node()
terrain = escoria.room_terrain
update_terrain()

View File

@@ -41,20 +41,65 @@ export(PackedScene) var inventory_item_scene_file : PackedScene
export(Color) var dialog_color = ColorN("white")
# Animation node (null if none was found)
var animation
var animation_sprite
onready var interact_positions : Dictionary = { "default": null}
# 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 pose_scale : int
var last_scale : Vector2
var collision
# WALKING
# State machine defining the current interact state of the player
enum INTERACT_STATES {
INTERACT_STARTED, # 
INTERACT_NONE, #
INTERACT_WALKING # Player is walking
}
var interact_status # Current interact status, type INTERACT_STATES
var walk_path : Array = []
var walk_destination : Vector2
var walk_context
var target_object : Object = null
var moved : bool
var path_ofs : float
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
}
var task # type PLAYER_TASKS
var params_queue : Array
# PRIVATE VARS
var area : Area2D
# Size of the item
var size : Vector2
var last_deg : int
var last_dir : int
func _ready():
for n in get_children():
if n is AnimatedSprite:
animation_sprite = n
continue
if n is AnimationPlayer:
animation = n
animation_sprite = n
continue
if n is Area2D:
area = n
@@ -66,7 +111,8 @@ func _ready():
area.connect("input_event", self, "manage_input")
init_interact_position_with_node()
terrain = escoria.room_terrain
if !Engine.is_editor_hint():
escoria.register_object(self)
connect("mouse_entered_item", escoria.inputs_manager, "_on_mouse_entered_item")
@@ -75,13 +121,79 @@ func _ready():
connect("mouse_double_left_clicked_item", escoria.inputs_manager, "_on_mouse_left_double_clicked_item")
connect("mouse_right_clicked_item", escoria.inputs_manager, "_on_mouse_right_clicked_item")
update_terrain()
func _process(time):
if Engine.is_editor_hint():
return
if task == PLAYER_TASKS.WALK or task == PLAYER_TASKS.SLIDE:
var pos = get_position()
var old_pos = pos
var next
if walk_path.size() > 1:
next = walk_path[path_ofs + 1]
else:
next = walk_path[path_ofs]
var dist = speed * time * pow(last_scale.x, 2) * terrain.player_speed_multiplier
if walk_context and "fast" in walk_context and walk_context.fast:
dist *= terrain.player_doubleclick_speed_multiplier
var dir = (next - pos).normalized()
# assume that x^2 + y^2 == 1, apply v_speed_damp the y axis
#printt("dir before", dir)
dir = dir * (dir.x * dir.x + dir.y * dir.y * v_speed_damp)
#printt("dir after", dir, dist)
var new_pos
if pos.distance_to(next) < dist:
new_pos = next
path_ofs += 1
else:
new_pos = pos + dir * dist
if path_ofs >= walk_path.size() - 1:
walk_stop(walk_destination)
return
pos = new_pos
var angle = (old_pos.angle_to_point(pos))
set_position(pos)
if task == PLAYER_TASKS.WALK:
last_deg = escoria.utils._get_deg_from_rad(angle)
last_dir = _get_dir_deg(last_deg, animations)
var current_animation = ""
if animation_sprite != null:
current_animation = animation_sprite.animation
# elif animation != null:
# current_animation = animation.current_animation
if current_animation != animations.directions[last_dir][0]:
animation_sprite.play(animations.directions[last_dir][0])
pose_scale = animations.directions[last_dir][1]
update_terrain()
else:
moved = false
set_process(false)
func get_animation_player():
if animation == null:
if animation_sprite == null:
for n in get_children():
if n is AnimationPlayer:
animation = n
return animation
animation_sprite = n
return animation_sprite
"""
@@ -94,12 +206,15 @@ position instead.
func init_interact_position_with_node():
for c in get_children():
if c is Position2D:
# If the position2D node is part of the hotspot, it means it is not an interact position
# but a dialog position for example. Interact position node must be set in the room scene.
if c.get_owner() == self:
continue
interact_positions.default = c.global_position
break
if c is CollisionShape2D or c is CollisionPolygon2D:
interact_positions.default = c.global_position
if interact_positions.default == null:
interact_positions.default = self.global_position
interact_positions.default = c.global_position
func manage_input(viewport : Viewport, event : InputEvent, shape_idx : int):
if event is InputEventMouseButton:
@@ -121,3 +236,192 @@ func _on_mouse_entered():
func _on_mouse_exited():
emit_signal("mouse_exited_item")
func update_terrain(on_event_finished_name = null):
if !terrain or terrain == null or !is_instance_valid(terrain):
return
if on_event_finished_name != null and on_event_finished_name != "setup":
return
if is_exit:
return
var pos = position
z_index = pos.y if pos.y <= VisualServer.CANVAS_ITEM_Z_MAX else VisualServer.CANVAS_ITEM_Z_MAX
var color
if terrain_is_scalenodes:
last_scale = terrain.get_terrain(pos)
self.scale = last_scale
elif check_maps:
color = terrain.get_terrain(pos)
var scal = terrain.get_scale_range(color.b)
if scal != get_scale():
last_scale = scal
self.scale = last_scale
# Do not flip the entire player character, because that would conflict
# with shadows that expect to be siblings of $texture
if pose_scale == -1 and $texture.scale.x > 0:
$texture.scale.x *= pose_scale
collision.scale.x *= pose_scale
elif pose_scale == 1 and $texture.scale.x < 0:
$texture.scale.x *= -1
collision.scale.x *= -1
# if check_maps:
# color = terrain.get_light(pos)
#
# if color:
# for s in sprites:
# s.set_modulate(color)
func teleport(target, angle : Object = null) -> void:
"""
Teleports the item on target position.
target can be Vector2 or Object
"""
if typeof(target) == TYPE_VECTOR2:
printt("Item teleported at position", target, "with angle", angle)
position = target
elif typeof(target) == TYPE_OBJECT:
if target.get("interact_positions") != null:
position = target.interact_positions.default #.global_position
else:
position = target.position
printt("Item teleported at", target.name, "position", position, "with angle", angle)
else:
escoria.report_errors("escitem.gd:teleport()", ["Target to teleport to is null or unusable (" + target + ")"])
# PUBLIC FUNCTION
func walk_to(pos : Vector2, p_walk_context = null):
if not terrain:
return walk_stop(get_position())
if interact_status == INTERACT_STATES.INTERACT_WALKING:
return
if interact_status == INTERACT_STATES.INTERACT_STARTED:
interact_status = INTERACT_STATES.INTERACT_WALKING
walk_path = terrain.get_terrain_path(get_position(), pos)
walk_context = p_walk_context
if walk_path.size() == 0:
task = PLAYER_TASKS.NONE
walk_stop(get_position())
set_process(false)
return
moved = true
walk_destination = walk_path[walk_path.size()-1]
if terrain.is_solid(pos):
walk_destination = walk_path[walk_path.size()-1]
path_ofs = 0.0
task = PLAYER_TASKS.WALK
set_process(true)
# PRIVATE FUNCTION
func walk(target_pos, p_speed, context = null):
if p_speed:
orig_speed = speed
speed = p_speed
walk_to(target_pos, context)
# PRIVATE FUNCTION
func walk_stop(pos):
position = pos
interact_status = INTERACT_STATES.INTERACT_NONE
walk_path = []
if orig_speed:
speed = orig_speed
orig_speed = 0.0
task = PLAYER_TASKS.NONE
moved = false
set_process(false)
if params_queue != null && !params_queue.empty():
if animations.dir_angles.size() > 0:
if params_queue[0].interact_angle == -1:
escoria.tools.resolve_angle_to(params_queue[0])
else:
last_dir = _get_dir_deg(params_queue[0].interact_angle, animations)
animation_sprite.play(animations.idles[last_dir][0])
pose_scale = animations.idles[last_dir][1]
update_terrain()
else:
animation_sprite.play(animations.idles[last_dir][0])
pose_scale = animations.idles[last_dir][1]
get_tree().call_group_flags(SceneTree.GROUP_CALL_DEFAULT, "game", "interact", params_queue)
# Clear params queue to prevent the same action from being triggered again
params_queue = []
else:
# If we're heading to an object and reached its interaction position,
# orient towards the defined interaction direction set on the object (if any)
if walk_context.has("target_object") and walk_context.target_object.player_orients_on_arrival \
and escoria.esc_runner.get_interactive(walk_context.target_object.global_id):
var orientation = walk_context["target_object"].interaction_direction
animation_sprite.play(animations.idles[orientation][0])
pose_scale = animations.idles[orientation][1]
else:
animation_sprite.play(animations.idles[last_dir][0])
pose_scale = animations.idles[last_dir][1]
update_terrain()
if walk_context != null:
# escoria.esc_level_runner.finished(walk_context)
escoria.esc_level_runner.finished()
walk_context = null
emit_signal("arrived")
func _get_dir(angle : float, animations) -> int:
var deg = escoria.utils._get_deg_from_rad(angle)
return _get_dir_deg(deg, animations)
func _get_dir_deg(deg : int, animations) -> int:
# We turn the angle by -90° because angle_to_point gives the angle against X axis, not Y
deg = wrapi(deg - 90, 0, 360)
var dir = -1
var i = 0
for arr_angle_zone in animations.dir_angles:
if is_angle_in_interval(deg, arr_angle_zone):
dir = i
break
else:
i += 1
continue
# It's an error to have the animations misconfigured
if dir == -1:
escoria.report_errors("escitem.gd:_get_dir_deg()", ["No direction found for " + str(deg)])
return dir
"""
Returns true if given angle is inside the interval given by a starting_angle and the size.
@param angle : Angle to test
@param: interval : Array of size 2, containing the starting angle, and the size of interval
 eg: [90, 40] corresponds to angle between 90° and 130°
"""
func is_angle_in_interval(angle: float, interval : Array) -> bool:
angle = wrapi(angle, 0, 360)
if angle == 0:
angle = 360
var start_angle = wrapi(interval[0], 0, 360)
var angle_area = interval[1]
var end_angle = wrapi(interval[0] + angle_area, 0, 360)
if (angle >= 270 and angle <= 360) or (angle >= 0 and angle <= 90):
if wrapi(angle+180, 0, 360) > wrapi(interval[0]+ 180, 0, 360) \
&& wrapi(angle+180, 0, 360) <= wrapi(interval[0] + angle_area + 180, 0, 360):
return true
else:
if wrapi(angle, 0, 360) > start_angle && wrapi(angle, 0, 360) <= end_angle:
return true
return false

View File

@@ -68,6 +68,7 @@ var last_dir : int
var last_scale : Vector2
var pose_scale : int
# Animations script (for walking, idling...)
export(Script) var animations
# AnimatedSprite node (if any)
@@ -227,10 +228,13 @@ func update_terrain(on_event_finished_name = null):
# for s in sprites:
# s.set_modulate(color)
# Sets player angle and plays according animation.
"""
Sets player angle and plays according animation.
"""
func set_angle(deg):
if deg < 0 or deg > 360:
escoria.report_errors("player.gd:set_angle()", ["Invalid degree to turn to " + str(deg)])
escoria.report_errors("escplayer.gd:set_angle()", ["Invalid degree to turn to " + str(deg)])
moved = true
last_deg = deg
last_dir = _get_dir_deg(deg, animations)
@@ -242,12 +246,11 @@ func set_angle(deg):
pose_scale = animations.idles[last_dir][1]
update_terrain()
"""
Teleports the player on target position.
target can be Vector2 or Object
"""
func teleport(target, angle : Object = null) -> void:
"""
Teleports the player on target position.
target can be Vector2 or Object
"""
if typeof(target) == TYPE_VECTOR2:
printt("Player teleported at position", target, "with angle", angle)
position = target
@@ -374,7 +377,7 @@ func _get_dir_deg(deg : int, animations) -> int:
# It's an error to have the animations misconfigured
if dir == -1:
escoria.report_errors("player", ["No direction found for " + str(deg)])
escoria.report_errors("escplayer.gd:_get_dir_deg()", ["No direction found for " + str(deg)])
return dir

View File

@@ -111,32 +111,42 @@ func do(action : String, params : Array = []) -> void:
if current_state == GAME_STATE.DEFAULT:
match action:
"walk":
# Reset current action
# Reset current action.
esc_runner.set_current_action("")
# Walk to position2D
if params[1] is Vector2:
# Check moving object.
if !escoria.esc_runner.check_obj(params[0], "escoria.do(walk)"):
report_errors("escoria.gd:do()",
["Walk action requested on inexisting object: " + params[0]])
return
var moving_obj = escoria.esc_runner.get_object(params[0])
# Walk to Position2D.
if params[1] is Vector2:
var target_position = params[1]
var is_fast : bool = false
if params.size() > 2 and params[2] == true:
is_fast = true
var walk_context = {"fast": is_fast}
main.current_scene.player.walk_to(target_position, walk_context)
moving_obj.walk_to(target_position, walk_context)
# Walk to object from its id
elif params[1] is String:
elif params[1] is String:
if !escoria.esc_runner.check_obj(params[1], "escoria.do(walk)"):
report_errors("escoria.gd:do()",
["Walk action requested TOWARDS inexisting object: " + params[1]])
return
var object = escoria.esc_runner.get_object(params[1])
if object:
var target_position : Vector2 = object.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}
if params[0] == main.current_scene.player.global_id:
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}
main.current_scene.player.walk_to(target_position, walk_context)
else:
report_errors("escoria.gd: do() > walk", ["TODO: code NPC walking"])
moving_obj.walk_to(target_position, walk_context)
"hotspot_left_click", "item_left_click":
if params[0] is String:

View File

@@ -45,6 +45,10 @@ func add_new_item_by_id(item_id : String) -> void:
if item_id.begins_with("i/"):
item_id = item_id.rsplit("i/", false)[0]
if !items_ids_in_inventory.has(item_id):
if !escoria.esc_runner.check_obj(item_id, "add_new_item_by_id"):
escoria.report_errors("inventory_ui.gd:add_new_item_by_id()",
["Item global id '"+ item_id + "' does not exist.",
"Check item's id in ESCORIA_ALL_ITEMS scene."])
var item_inventory_button = all_items.get_inventory_item(item_id).duplicate()
items_ids_in_inventory[item_id] = item_inventory_button
get_node(items_container).add_item(item_inventory_button)