Initial Commit

This commit is contained in:
gemdude46 2023-01-26 18:25:40 +00:00
parent 2dce133760
commit dbb0f17743
63 changed files with 2881 additions and 1 deletions

197
Scripts/Constants.gd Normal file
View file

@ -0,0 +1,197 @@
enum UnitType {
PLAYER,
NPC,
}
enum ActionType {
JUMP,
MOVE,
RECOIL,
}
enum UnitCondition {
CURRENT_ACTION,
IS_ON_GROUND,
MOVING_STATUS,
IS_INVINCIBLE,
}
enum UnitCurrentAction {
IDLE,
JUMPING,
RECOILING,
}
enum UnitMovingStatus {
IDLE,
MOVING,
}
enum PlayerInput {
UP,
DOWN,
LEFT,
RIGHT,
GBA_A,
GBA_B,
GBA_START,
GBA_SELECT,
}
enum Direction {
UP,
DOWN,
LEFT,
RIGHT,
}
enum MapElemType {
SQUARE,
SLOPE_LEFT,
SLOPE_RIGHT,
SMALL_SLOPE_LEFT_1,
SMALL_SLOPE_LEFT_2,
SMALL_SLOPE_RIGHT_1,
SMALL_SLOPE_RIGHT_2,
LEDGE,
}
enum SpriteClass {
IDLE,
WALK,
JUMP,
RECOIL,
}
const UNIT_TYPE_ACTIONS = {
UnitType.PLAYER: [
ActionType.JUMP,
ActionType.MOVE,
ActionType.RECOIL,
],
UnitType.NPC: [
ActionType.MOVE,
],
}
const UNIT_TYPE_CURRENT_ACTIONS = {
UnitType.PLAYER: [
UnitCurrentAction.IDLE,
UnitCurrentAction.JUMPING,
UnitCurrentAction.RECOILING,
],
UnitType.NPC: [
UnitCurrentAction.IDLE,
],
}
# default conditions
const UNIT_TYPE_CONDITIONS = {
UnitType.PLAYER: {
UnitCondition.CURRENT_ACTION: UnitCurrentAction.IDLE,
UnitCondition.IS_ON_GROUND: false,
UnitCondition.MOVING_STATUS: UnitMovingStatus.IDLE,
UnitCondition.IS_INVINCIBLE: false,
},
UnitType.NPC: {
UnitCondition.CURRENT_ACTION: UnitCurrentAction.IDLE,
UnitCondition.IS_ON_GROUND: false,
UnitCondition.MOVING_STATUS: UnitMovingStatus.IDLE,
},
}
# in seconds
const CURRENT_ACTION_TIMERS = {
UnitType.PLAYER: {
UnitCurrentAction.JUMPING: 0.4,
UnitCurrentAction.RECOILING: 0.67,
},
}
const UNIT_CONDITION_TIMERS = {
# condition type: [duration, on value, off value]
UnitType.PLAYER: {
UnitCondition.IS_INVINCIBLE: [2.5, true, false],
},
UnitType.NPC: {},
}
# Position relative to player's origin, list of directions to check for collision
const ENV_COLLIDERS = {
UnitType.PLAYER: [
[Vector2(0, 1.5), [Direction.LEFT, Direction.UP, Direction.RIGHT]],
[Vector2(-.25, .25), [Direction.LEFT]],
[Vector2(.25, .25), [Direction.RIGHT]],
[Vector2(-.25, 1.25), [Direction.LEFT]],
[Vector2(.25, 1.25), [Direction.RIGHT]],
# contact with ground is at (0, 0)
[Vector2(0, 0), [Direction.LEFT, Direction.DOWN, Direction.RIGHT]],
],
UnitType.NPC: [
[Vector2(0, 1.5), [Direction.LEFT, Direction.UP, Direction.RIGHT]],
[Vector2(-.25, .25), [Direction.LEFT]],
[Vector2(.25, .25), [Direction.RIGHT]],
[Vector2(-.25, 1.25), [Direction.LEFT]],
[Vector2(.25, 1.25), [Direction.RIGHT]],
[Vector2(0, 0), [Direction.LEFT, Direction.DOWN, Direction.RIGHT]],
],
}
const INPUT_MAP = {
PlayerInput.UP: "ui_up",
PlayerInput.DOWN: "ui_down",
PlayerInput.LEFT: "ui_left",
PlayerInput.RIGHT: "ui_right",
PlayerInput.GBA_A: "gba_a",
PlayerInput.GBA_B: "gba_b",
PlayerInput.GBA_START: "gba_start",
PlayerInput.GBA_SELECT: "gba_select",
}
const TILE_SET_MAP_ELEMS = {
"TestTileSet": {
MapElemType.SQUARE: [0, 1, 2, 3, 4, 5, 6, 7, 8],
MapElemType.SLOPE_LEFT: [15, 16],
MapElemType.SLOPE_RIGHT: [17, 18],
MapElemType.SMALL_SLOPE_LEFT_1: [9],
MapElemType.SMALL_SLOPE_LEFT_2: [10, 11],
MapElemType.SMALL_SLOPE_RIGHT_1: [12],
MapElemType.SMALL_SLOPE_RIGHT_2: [13, 14],
MapElemType.LEDGE: [19, 20, 21, 22],
},
}
const UNIT_SPRITES = {
# Sprite-class: [Is-animation?, Nodes]
UnitType.PLAYER: {
SpriteClass.IDLE: [false, ["Idle"]],
SpriteClass.WALK: [true, ["Walk"]],
SpriteClass.JUMP: [false, ["Jump1", "Jump2"]],
SpriteClass.RECOIL: [false, ["Recoil"]],
},
UnitType.NPC: {
SpriteClass.IDLE: [false, ["Idle"]],
SpriteClass.WALK: [true, ["Walk"]],
SpriteClass.JUMP: [false, ["Jump2"]],
},
}
const UNIT_TYPE_MOVE_SPEEDS = {
UnitType.PLAYER: 6,
UnitType.NPC: 3,
}
const UNIT_TYPE_JUMP_SPEEDS = {
UnitType.PLAYER: 5,
}
const SCALE_FACTOR = 3.5
const GRID_SIZE = 20 # pixels
const GRAVITY = 30
const MAX_FALL_SPEED = -12
const ACCELERATION = 35
const QUANTUM_DIST = 0.001
const SPAWN_DISTANCE = 10
# specialized constants
const FLASH_CYCLE = 0.15

153
Scripts/GameScene.gd Normal file
View file

@ -0,0 +1,153 @@
extends Node
# _process(delta) is called by this class
# player input is handled here
# unit declares its intention in process_unit()
# stage environment interacts with the unit in interact()
# unit executes its resulting state in react()
# stage environment interacts with the unit once more in interact_post()
class_name GameScene
export var tile_set_name: String
const Constants = preload("res://Scripts/Constants.gd")
const Unit = preload("res://Scripts/Unit.gd")
const UNIT_DIRECTORY = {
Constants.UnitType.NPC: preload("res://Units/NPC.tscn"),
}
# positions to unit string
export var spawning : Dictionary
var spawning_map = {} # keeps track of what's alive
var paused : bool = false
var units = []
var player : Player
var player_cam : Camera2D
# [pressed?, just pressed?, just released?]
var input_table = {
Constants.PlayerInput.UP: [false, false, false],
Constants.PlayerInput.DOWN: [false, false, false],
Constants.PlayerInput.LEFT: [false, false, false],
Constants.PlayerInput.RIGHT: [false, false, false],
Constants.PlayerInput.GBA_A: [false, false, false],
Constants.PlayerInput.GBA_B: [false, false, false],
Constants.PlayerInput.GBA_SELECT: [false, false, false],
}
const I_T_PRESSED : int = 0
const I_T_JUST_PRESSED : int = 1
const I_T_JUST_RELEASED : int = 2
var stage_env
var time_elapsed : float = 0
var rng = RandomNumberGenerator.new()
# Called when the node enters the scene tree for the first time.
func _ready():
units.append(get_node("Player"))
player = units[0]
player.init_unit_w_scene(self)
player_cam = player.get_node("Camera2D")
player_cam.make_current()
stage_env = load("res://Scripts/StageEnvironment.gd").new(self)
player.get_node("Camera2D").make_current()
for spawning_key in spawning:
spawning_map[spawning_key] = null
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
# visual effects
if (player.facing == Constants.Direction.RIGHT):
player_cam.offset_h = 1
else:
player_cam.offset_h = -1
read_paused()
if not paused:
# game logic
process_spawning()
for unit in units:
unit.reset_actions()
unit.handle_input(delta)
unit.process_unit(delta, time_elapsed)
stage_env.interact(unit, delta)
unit.react(delta)
time_elapsed += delta
func read_paused():
if Input.is_action_just_pressed(Constants.INPUT_MAP[Constants.PlayerInput.GBA_START]):
paused = !paused
func process_spawning():
for one_spawn in spawning.keys():
if spawning_map[one_spawn] != null:
continue
if abs(one_spawn[0] - player.pos.x) >= Constants.SPAWN_DISTANCE + 1 or abs(one_spawn[1] - player.pos.y) >= Constants.SPAWN_DISTANCE + 1:
continue
if abs(one_spawn[0] - player.pos.x) <= Constants.SPAWN_DISTANCE:
continue
# NPCUnit
var npc_scene = UNIT_DIRECTORY[Constants.UnitType.get(spawning[one_spawn])]
var npc_instance = npc_scene.instance()
add_child(npc_instance)
units.append(npc_instance)
npc_instance.spawn_point = one_spawn
spawning_map[one_spawn] = npc_instance
npc_instance.pos.x = one_spawn[0]
npc_instance.pos.y = one_spawn[1]
npc_instance.position.x = npc_instance.pos.x * Constants.GRID_SIZE
npc_instance.position.y = -1 * npc_instance.pos.y * Constants.GRID_SIZE
npc_instance.init_unit_w_scene(self)
func handle_player_input():
# early exit
if player.get_current_action() == Constants.UnitCurrentAction.RECOILING:
player.set_action(Constants.ActionType.RECOIL)
return
for input_num in input_table.keys():
if Input.is_action_pressed(Constants.INPUT_MAP[input_num]):
input_table[input_num][I_T_PRESSED] = true
input_table[input_num][I_T_JUST_RELEASED] = false
if Input.is_action_just_pressed(Constants.INPUT_MAP[input_num]):
input_table[input_num][I_T_JUST_PRESSED] = true
else:
input_table[input_num][I_T_JUST_PRESSED] = false
else:
input_table[input_num][I_T_PRESSED] = false
input_table[input_num][I_T_JUST_PRESSED] = false
if Input.is_action_just_released(Constants.INPUT_MAP[input_num]):
input_table[input_num][I_T_JUST_RELEASED] = true
else:
input_table[input_num][I_T_JUST_RELEASED] = false
# process input_table
if input_table[Constants.PlayerInput.LEFT][I_T_PRESSED] or input_table[Constants.PlayerInput.RIGHT][I_T_PRESSED]:
if input_table[Constants.PlayerInput.LEFT][I_T_PRESSED] and input_table[Constants.PlayerInput.RIGHT][I_T_PRESSED]:
input_table[Constants.PlayerInput.LEFT][I_T_PRESSED] = false
input_table[Constants.PlayerInput.LEFT][I_T_JUST_PRESSED] = false
var input_dir
if input_table[Constants.PlayerInput.LEFT][I_T_PRESSED]:
input_dir = Constants.Direction.LEFT
else:
input_dir = Constants.Direction.RIGHT
# if action-idle or action-jumping
if (player.get_current_action() == Constants.UnitCurrentAction.IDLE
or player.get_current_action() == Constants.UnitCurrentAction.JUMPING):
# set move
player.set_action(Constants.ActionType.MOVE)
# set facing
player.facing = input_dir
if input_table[Constants.PlayerInput.GBA_A][I_T_PRESSED]:
if (player.get_current_action() == Constants.UnitCurrentAction.JUMPING
or (player.get_current_action() == Constants.UnitCurrentAction.IDLE
and player.unit_conditions[Constants.UnitCondition.IS_ON_GROUND]
and input_table[Constants.PlayerInput.GBA_A][I_T_JUST_PRESSED])):
player.set_action(Constants.ActionType.JUMP)

64
Scripts/GameUtils.gd Normal file
View file

@ -0,0 +1,64 @@
extends Node
# also returns x, y of intersection
static func path_intersects_border(path_from : Vector2, path_to : Vector2, border_pt_a : Vector2, border_pt_b : Vector2):
# y = mx + b
# if path is vertical
if path_from.x == path_to.x:
# if border is vertical
if border_pt_a.x == border_pt_b.x:
return [false, Vector2()]
# if border is not vertical
else:
# if path x is not within range
if path_from.x < min(border_pt_a.x, border_pt_b.x) or path_from.x > max(border_pt_a.x, border_pt_b.x):
return [false, Vector2()]
# find m and b and solve for where border intersects with vertical line
var m = get_m(border_pt_b, border_pt_a)
var b = get_b(border_pt_a, m)
var intersect_y = m * path_from.x + b
var intersects : bool = intersect_y >= min(path_from.y, path_to.y) and intersect_y <= max(path_from.y, path_to.y)
return [intersects, Vector2(path_from.x, intersect_y)]
# else if border is vertical
elif border_pt_a.x == border_pt_b.x:
# if border x is not within range
if border_pt_a.x < min(path_from.x, path_to.x) or border_pt_a.x > max(path_from.x, path_to.x):
return [false, Vector2()]
# find m and b and solve for where border intersects with vertical line
var m = get_m(path_to, path_from)
var b = get_b(path_from, m)
var intersect_y = m * border_pt_a.x + b
return [intersect_y >= min(border_pt_a.y, border_pt_b.y) and intersect_y <= max(border_pt_a.y, border_pt_b.y), Vector2(border_pt_a.x, intersect_y)]
# else if path and border are parallel
elif get_m(path_to, path_from) == get_m(border_pt_b, border_pt_a):
return [false, Vector2()]
else:
var path_m = get_m(path_to, path_from)
var path_b = get_b(path_from, path_m)
var border_m = get_m(border_pt_b, border_pt_a)
var border_b = get_b(border_pt_a, border_m)
# m1x + b1 = m2x + b2
# m1x - m2x = b2 - b1
# x = (b2 - b1) / (m1 - m2)
var intersect_x = (border_b - path_b) / (path_m - border_m)
return [(intersect_x >= min(border_pt_a.x, border_pt_b.x) and intersect_x <= max(border_pt_a.x, border_pt_b.x)
and intersect_x >= min(path_from.x, path_to.x) and intersect_x <= max(path_from.x, path_to.x)), Vector2(intersect_x, path_m * intersect_x + path_b)]
static func get_m(pt_a : Vector2, pt_b : Vector2):
return (pt_b.y - pt_a.y) / (pt_b.x - pt_a.x)
static func get_b(pt : Vector2, m : float):
return pt.y - (m * pt.x)
static func reangle_move(unit, angle_helper):
# pythagoras
var unit_magnitude = sqrt(pow(unit.h_speed, 2) + pow(unit.v_speed, 2))
var helper_magnitude = sqrt(pow(angle_helper[1].x - angle_helper[0].x, 2) + pow(angle_helper[1].y - angle_helper[0].y, 2))
if helper_magnitude == 0:
unit.h_speed = 0
unit.v_speed = 0
return
var factor = unit_magnitude / helper_magnitude
unit.h_speed = (angle_helper[1].x - angle_helper[0].x) * factor
unit.v_speed = (angle_helper[1].y - angle_helper[0].y) * factor

306
Scripts/StageEnvironment.gd Normal file
View file

@ -0,0 +1,306 @@
# Handles unit-environment iteraction
extends Object
const GameUtils = preload("res://Scripts/GameUtils.gd")
const Constants = preload("res://Scripts/Constants.gd")
const GameScene = preload("res://Scripts/GameScene.gd")
const Unit = preload("res://Scripts/Unit.gd")
var scene : GameScene
var colliders = []
# if unit's move vector has at least one of these directional components,
# do the collision check
var collision_into_direction_arrays = [] # nested array
var unit_collision_bounds = {} # maps unit type to [upper, lower, left, right]
func _init(the_scene : GameScene):
scene = the_scene
var stage : TileMap = scene.get_node("Stage")
init_stage_grid(stage)
stage.scale.x = Constants.SCALE_FACTOR
stage.scale.y = Constants.SCALE_FACTOR
# populate unit_collision_bounds
for unit_type in Constants.ENV_COLLIDERS.keys():
var initial_detect_pt = Constants.ENV_COLLIDERS[unit_type][0]
var upper : float = initial_detect_pt[0].y
var lower : float = initial_detect_pt[0].y
var left : float = initial_detect_pt[0].x
var right: float = initial_detect_pt[0].x
for detect_pt in Constants.ENV_COLLIDERS[unit_type]:
if (detect_pt[1].find(Constants.Direction.UP) != -1
and detect_pt[0].y > upper):
upper = detect_pt[0].y
if (detect_pt[1].find(Constants.Direction.DOWN) != -1
and detect_pt[0].y < lower):
lower = detect_pt[0].y
if (detect_pt[1].find(Constants.Direction.LEFT) != -1
and detect_pt[0].x < left):
left = detect_pt[0].x
if (detect_pt[1].find(Constants.Direction.RIGHT) != -1
and detect_pt[0].x > right):
right = detect_pt[0].x
unit_collision_bounds[unit_type] = [upper, lower, left, right]
func init_stage_grid(tilemap : TileMap):
for map_elem in tilemap.get_used_cells():
var stage_x = floor(tilemap.map_to_world(map_elem).x / Constants.GRID_SIZE)
var stage_y = floor(-1 * tilemap.map_to_world(map_elem).y / Constants.GRID_SIZE) - 1
var map_elem_type : int
var cellv = tilemap.get_cellv(map_elem)
var found_map_elem_type : bool = false
for test_map_elem_type in [
Constants.MapElemType.SQUARE,
Constants.MapElemType.SLOPE_LEFT,
Constants.MapElemType.SLOPE_RIGHT,
Constants.MapElemType.SMALL_SLOPE_LEFT_1,
Constants.MapElemType.SMALL_SLOPE_LEFT_2,
Constants.MapElemType.SMALL_SLOPE_RIGHT_1,
Constants.MapElemType.SMALL_SLOPE_RIGHT_2,
Constants.MapElemType.LEDGE]:
for test_cell_v in Constants.TILE_SET_MAP_ELEMS[scene.tile_set_name][test_map_elem_type]:
if test_cell_v == cellv:
map_elem_type = test_map_elem_type
found_map_elem_type = true
break
if found_map_elem_type:
break
match map_elem_type:
Constants.MapElemType.SQUARE:
insert_grid_collider(stage_x, stage_y, Constants.Direction.UP, 1)
insert_grid_collider(stage_x, stage_y, Constants.Direction.DOWN, 1)
insert_grid_collider(stage_x, stage_y, Constants.Direction.LEFT, 1)
insert_grid_collider(stage_x, stage_y, Constants.Direction.RIGHT, 1)
Constants.MapElemType.SLOPE_LEFT:
try_insert_collider(
Vector2(stage_x, stage_y),
Vector2(stage_x + 1, stage_y + 1),
[Constants.Direction.RIGHT, Constants.Direction.DOWN]
)
insert_grid_collider(stage_x, stage_y, Constants.Direction.LEFT, 1)
insert_grid_collider(stage_x, stage_y, Constants.Direction.UP, 1)
Constants.MapElemType.SLOPE_RIGHT:
try_insert_collider(
Vector2(stage_x, stage_y + 1),
Vector2(stage_x + 1, stage_y),
[Constants.Direction.LEFT, Constants.Direction.DOWN]
)
insert_grid_collider(stage_x, stage_y, Constants.Direction.RIGHT, 1)
insert_grid_collider(stage_x, stage_y, Constants.Direction.UP, 1)
Constants.MapElemType.SMALL_SLOPE_LEFT_1:
try_insert_collider(
Vector2(stage_x, stage_y),
Vector2(stage_x + 1, stage_y + .5),
[Constants.Direction.RIGHT, Constants.Direction.DOWN]
)
insert_grid_collider(stage_x, stage_y, Constants.Direction.LEFT, .5)
insert_grid_collider(stage_x, stage_y, Constants.Direction.UP, 1)
Constants.MapElemType.SMALL_SLOPE_LEFT_2:
try_insert_collider(
Vector2(stage_x, stage_y + .5),
Vector2(stage_x + 1, stage_y + 1),
[Constants.Direction.RIGHT, Constants.Direction.DOWN]
)
insert_grid_collider(stage_x, stage_y, Constants.Direction.RIGHT, .5)
insert_grid_collider(stage_x, stage_y, Constants.Direction.LEFT, 1)
insert_grid_collider(stage_x, stage_y, Constants.Direction.UP, 1)
Constants.MapElemType.SMALL_SLOPE_RIGHT_1:
try_insert_collider(
Vector2(stage_x, stage_y + .5),
Vector2(stage_x + 1, stage_y),
[Constants.Direction.LEFT, Constants.Direction.DOWN]
)
insert_grid_collider(stage_x, stage_y, Constants.Direction.RIGHT, .5)
insert_grid_collider(stage_x, stage_y, Constants.Direction.UP, 1)
Constants.MapElemType.SMALL_SLOPE_RIGHT_2:
try_insert_collider(
Vector2(stage_x, stage_y + 1),
Vector2(stage_x + 1, stage_y + .5),
[Constants.Direction.LEFT, Constants.Direction.DOWN]
)
insert_grid_collider(stage_x, stage_y, Constants.Direction.RIGHT, 1)
insert_grid_collider(stage_x, stage_y, Constants.Direction.LEFT, .5)
insert_grid_collider(stage_x, stage_y, Constants.Direction.UP, 1)
Constants.MapElemType.LEDGE:
insert_grid_collider(stage_x, stage_y, Constants.Direction.DOWN, 1)
func insert_grid_collider(stage_x, stage_y, direction : int, fractional_height : float):
var check_colliders = []
var insert_colliders = []
var point_a : Vector2
var point_b : Vector2
match direction:
Constants.Direction.UP:
point_a = Vector2(stage_x, stage_y)
point_b = Vector2(stage_x + 1, stage_y)
Constants.Direction.DOWN:
point_a = Vector2(stage_x, stage_y + 1)
point_b = Vector2(stage_x + 1, stage_y + 1)
Constants.Direction.LEFT:
point_a = Vector2(stage_x + 1, stage_y + (1 * fractional_height))
point_b = Vector2(stage_x + 1, stage_y)
Constants.Direction.RIGHT:
point_a = Vector2(stage_x, stage_y + (1 * fractional_height))
point_b = Vector2(stage_x, stage_y)
try_insert_collider(point_a, point_b, [direction])
func try_insert_collider(point_a : Vector2, point_b : Vector2, directions : Array):
if directions.size() == 1:
# aligned with grid
for i in range(len(colliders)):
if (colliders[i][0] == point_a
and colliders[i][1] == point_b
and are_inverse_directions(collision_into_direction_arrays[i][0], directions[0])):
colliders.remove(i)
collision_into_direction_arrays.remove(i)
return
colliders.append([point_a, point_b])
collision_into_direction_arrays.append(directions)
func are_inverse_directions(d1, d2):
return ((d1 == Constants.Direction.LEFT and d2 == Constants.Direction.RIGHT)
or (d1 == Constants.Direction.RIGHT and d2 == Constants.Direction.LEFT)
or (d1 == Constants.Direction.UP and d2 == Constants.Direction.DOWN)
or (d1 == Constants.Direction.DOWN and d2 == Constants.Direction.UP))
func interact(unit : Unit, delta):
if unit.unit_conditions[Constants.UnitCondition.IS_ON_GROUND]:
if unit.v_speed < 0:
# reassign the move speeds so that it reflects the true movement
reangle_grounded_move(unit)
else:
# apply gravity
unit.v_speed = max(unit.v_speed - (Constants.GRAVITY * delta), Constants.MAX_FALL_SPEED)
if not unit.h_speed == 0 or not unit.v_speed == 0:
# regular collision
for i in range(colliders.size()):
if check_collision(unit, colliders[i], collision_into_direction_arrays[i], delta):
break
# Do this a second time in case the unit's new move displacement needs
# fixing
for i in range(colliders.size()):
if check_collision(unit, colliders[i], collision_into_direction_arrays[i], delta):
break
func reangle_grounded_move(unit : Unit):
var has_ground_collision : bool = false
for i in range(colliders.size()):
var collider = colliders[i]
var collision_into_directions = collision_into_direction_arrays[i]
if collider[0].x == collider[1].x:
continue
if collision_early_exit(unit, collider, collision_into_directions):
continue
if collision_into_directions.find(Constants.Direction.DOWN) == -1:
continue
# returns [collision?, x, y]
var intersects_results = GameUtils.path_intersects_border(
Vector2(unit.pos.x, unit.pos.y),
Vector2(unit.pos.x, unit.pos.y - .5),
collider[0],
collider[1])
if intersects_results[0]:
has_ground_collision = true
unit.pos.y = intersects_results[1].y + Constants.QUANTUM_DIST
unit.last_contacted_ground_collider = collider
reangle_move(unit, collider, true)
if !has_ground_collision:
reangle_move(unit, unit.last_contacted_ground_collider, true)
unit.set_unit_condition(Constants.UnitCondition.IS_ON_GROUND, false)
# nullify_h_speed should be true if we are reangling a ground movement vector
func reangle_move(unit : Unit, collider, nullify_h_speed : bool):
var angle_helper
if unit.h_speed > 0:
angle_helper = collider
else:
angle_helper = [collider[1], collider[0]]
if nullify_h_speed:
unit.h_speed = 0
GameUtils.reangle_move(unit, angle_helper)
func check_collision(unit : Unit, collider, collision_into_directions, delta):
if collision_early_exit(unit, collider, collision_into_directions):
return false
var is_ground_collision : bool = collision_into_directions.find(Constants.Direction.DOWN) != -1
for unit_env_collider in Constants.ENV_COLLIDERS[unit.unit_type]:
if unit_env_collider_early_exit(unit_env_collider[1], collision_into_directions):
continue
var collision_check_location : Vector2 = unit.pos + unit_env_collider[0]
var collision_check_try_location : Vector2 = collision_check_location + Vector2(unit.h_speed * delta, unit.v_speed * delta)
# returns [collision?, (x, y)]
var intersects_results = GameUtils.path_intersects_border(
collision_check_location,
collision_check_try_location,
collider[0],
collider[1])
if intersects_results[0]:
if is_ground_collision:
if unit_env_collider[0] == Vector2(0, 0):
unit.pos.y = intersects_results[1].y + Constants.QUANTUM_DIST
unit.pos.x = intersects_results[1].x
if unit.unit_conditions[Constants.UnitCondition.IS_ON_GROUND]:
# preserve magnitude
reangle_move(unit, collider, false)
else:
if unit.get_current_action() != Constants.UnitCurrentAction.JUMPING:
unit.set_unit_condition(Constants.UnitCondition.IS_ON_GROUND, true)
# landed on ground, horizontal component to become magnitude
unit.v_speed = 0
reangle_move(unit, collider, false)
else:
if collider[0].x == collider[1].x:
# vertical wall collision
var new_cc_x : float
if unit.h_speed < 0:
new_cc_x = intersects_results[1].x + Constants.QUANTUM_DIST
else:
new_cc_x = intersects_results[1].x - Constants.QUANTUM_DIST
var target_h_speed : float = (new_cc_x - collision_check_location.x) / delta
var factor : float = target_h_speed / unit.h_speed
unit.h_speed *= factor
if unit.get_condition(Constants.UnitCondition.IS_ON_GROUND, false):
# also shorten vertical component to preserve move vector direction
unit.v_speed *= factor
else:
# ceiling collision (horizontal only for now)
var new_cc_y : float = intersects_results[1].y - Constants.QUANTUM_DIST
unit.v_speed = (new_cc_y - collision_check_location.y) / delta
if unit.get_current_action() == Constants.UnitCurrentAction.JUMPING:
unit.set_current_action(Constants.UnitCurrentAction.IDLE)
if !is_ground_collision or unit_env_collider[0] == Vector2(0, 0):
# return true if there's a collision
# don't return true if it's a ground collision but the unit environment collider is not the (0, 0) collider
return true
return false
func collision_early_exit(unit : Unit, collider, collision_into_directions):
if (collider[0].y > unit.pos.y + unit_collision_bounds[unit.unit_type][0] + 1
and collider[1].y > unit.pos.y + unit_collision_bounds[unit.unit_type][0] + 1):
return true
if (collider[0].y < unit.pos.y + unit_collision_bounds[unit.unit_type][1] - 1
and collider[1].y < unit.pos.y + unit_collision_bounds[unit.unit_type][1] - 1):
return true
if collider[1].x < unit.pos.x + unit_collision_bounds[unit.unit_type][2] - 1:
return true
if collider[0].x > unit.pos.x + unit_collision_bounds[unit.unit_type][3] + 1:
return true
for collision_into_direction in collision_into_directions:
if collision_into_direction == Constants.Direction.UP and unit.v_speed > 0:
return false
if collision_into_direction == Constants.Direction.DOWN and unit.v_speed < 0:
return false
if collision_into_direction == Constants.Direction.LEFT and unit.h_speed < 0:
return false
if collision_into_direction == Constants.Direction.RIGHT and unit.h_speed > 0:
return false
return true
func unit_env_collider_early_exit(env_collider_directions, collision_into_directions):
var found_matching_direction : bool = false
for env_collider_direction in env_collider_directions:
for collision_into_direction in collision_into_directions:
if env_collider_direction == collision_into_direction:
return false
return true

282
Scripts/Unit.gd Normal file
View file

@ -0,0 +1,282 @@
extends Area2D
# base class for units
# we assume every unit can move and jump so we see their handlers here
# sprite management is handled here (and subclasses) as well
class_name Unit
const Constants = preload("res://Scripts/Constants.gd")
const GameUtils = preload("res://Scripts/GameUtils.gd")
var scene
# position
export var unit_type : int
var actions = {}
var unit_conditions = {}
var facing : int = Constants.Direction.RIGHT
var current_action_time_elapsed : float = 0
var unit_condition_timers = {}
var pos : Vector2
var h_speed : float = 0
var v_speed : float = 0
var target_move_speed : float
var last_contacted_ground_collider : Array
var current_sprite : Node2D
var sprite_class_nodes = {} # sprite class to node list dictionary
var hit_queued : bool = false
var hit_dir : int
var time_elapsed : float
var is_flash : bool = false
var flash_start_timestamp : float
# Called when the node enters the scene tree for the first time
func _ready():
for action_num in Constants.UNIT_TYPE_ACTIONS[unit_type]:
actions[action_num] = false
for condition_num in Constants.UNIT_TYPE_CONDITIONS[unit_type].keys():
set_unit_condition(condition_num, Constants.UNIT_TYPE_CONDITIONS[unit_type][condition_num])
for condition_num in Constants.UNIT_CONDITION_TIMERS[unit_type].keys():
unit_condition_timers[condition_num] = 0
target_move_speed = Constants.UNIT_TYPE_MOVE_SPEEDS[unit_type]
# populate sprite_class_nodes
for sprite_class in Constants.UNIT_SPRITES[unit_type]:
sprite_class_nodes[sprite_class] = []
for node_name in Constants.UNIT_SPRITES[unit_type][sprite_class][1]:
sprite_class_nodes[sprite_class].append(get_node(node_name))
pos = Vector2(position.x / Constants.GRID_SIZE, -1 * position.y / Constants.GRID_SIZE)
position.x = position.x * Constants.SCALE_FACTOR
position.y = position.y * Constants.SCALE_FACTOR
scale.x = Constants.SCALE_FACTOR
scale.y = Constants.SCALE_FACTOR
func init_unit_w_scene(scene):
self.scene = scene
func set_action(action : int):
assert(action in Constants.UNIT_TYPE_ACTIONS[unit_type])
actions[action] = true
func set_unit_condition(condition_type : int, condition):
assert(condition_type in Constants.UNIT_TYPE_CONDITIONS[unit_type].keys())
unit_conditions[condition_type] = condition
func set_unit_condition_with_timer(condition_type : int):
assert(condition_type in Constants.UNIT_CONDITION_TIMERS[unit_type].keys())
set_unit_condition(condition_type, Constants.UNIT_CONDITION_TIMERS[unit_type][condition_type][1])
unit_condition_timers[condition_type] = Constants.UNIT_CONDITION_TIMERS[unit_type][condition_type][0]
func get_condition(condition_num : int, default):
if condition_num in Constants.UNIT_TYPE_CONDITIONS[unit_type].keys():
return unit_conditions[condition_num]
else:
return default
func is_current_action_timer_done(current_action : int):
assert(current_action in Constants.CURRENT_ACTION_TIMERS[unit_type].keys())
return current_action_time_elapsed >= Constants.CURRENT_ACTION_TIMERS[unit_type][current_action]
func reset_actions():
for action_num in Constants.UNIT_TYPE_ACTIONS[unit_type]:
actions[action_num] = false
func process_unit(delta, time_elapsed : float):
current_action_time_elapsed += delta
execute_actions(delta)
handle_idle()
advance_timers(delta)
handle_moving_status(delta)
handle_recoil() # must be after handle_moving_status
reset_current_action()
self.time_elapsed = time_elapsed
func advance_timers(delta):
for condition_num in Constants.UNIT_CONDITION_TIMERS[unit_type].keys():
unit_condition_timers[condition_num] = move_toward(unit_condition_timers[condition_num], 0, delta)
if unit_condition_timers[condition_num] == 0:
set_unit_condition(condition_num, Constants.UNIT_CONDITION_TIMERS[unit_type][condition_num][2])
if condition_num == Constants.UnitCondition.IS_INVINCIBLE:
invincibility_ended()
func reset_current_action():
# process CURRENT_ACTION
if get_current_action() == Constants.UnitCurrentAction.JUMPING:
if not actions[Constants.ActionType.JUMP]:
set_current_action(Constants.UnitCurrentAction.IDLE)
# process MOVING_STATUS
if not actions[Constants.ActionType.MOVE]:
set_unit_condition(Constants.UnitCondition.MOVING_STATUS, Constants.UnitMovingStatus.IDLE)
func handle_input(delta):
# implemented in subclass
pass
func get_current_action():
return unit_conditions[Constants.UnitCondition.CURRENT_ACTION]
func set_current_action(current_action : int):
assert(current_action in Constants.UNIT_TYPE_CURRENT_ACTIONS[unit_type])
if get_current_action() != current_action:
current_action_time_elapsed = 0
set_unit_condition(Constants.UnitCondition.CURRENT_ACTION, current_action)
func execute_actions(delta):
for action_num in Constants.UNIT_TYPE_ACTIONS[unit_type]:
if !actions[action_num]:
continue
match action_num:
Constants.ActionType.JUMP:
jump()
Constants.ActionType.MOVE:
move()
func jump():
set_current_action(Constants.UnitCurrentAction.JUMPING)
if (unit_conditions[Constants.UnitCondition.IS_ON_GROUND]):
# hit ground
v_speed = max(Constants.UNIT_TYPE_JUMP_SPEEDS[unit_type], v_speed)
else:
# airborne
v_speed = max(Constants.UNIT_TYPE_JUMP_SPEEDS[unit_type], move_toward(v_speed, Constants.UNIT_TYPE_JUMP_SPEEDS[unit_type], get_process_delta_time() * Constants.GRAVITY))
set_unit_condition(Constants.UnitCondition.IS_ON_GROUND, false)
if get_current_action() == Constants.UnitCurrentAction.JUMPING and v_speed > 0:
set_sprite(Constants.SpriteClass.JUMP, 0)
if is_current_action_timer_done(Constants.UnitCurrentAction.JUMPING):
set_current_action(Constants.UnitCurrentAction.IDLE)
func move():
set_unit_condition(Constants.UnitCondition.MOVING_STATUS, Constants.UnitMovingStatus.MOVING)
if (get_current_action() == Constants.UnitCurrentAction.IDLE
and unit_conditions[Constants.UnitCondition.IS_ON_GROUND]):
set_sprite(Constants.SpriteClass.WALK)
func handle_recoil():
# implemented in subclass
pass
func handle_moving_status(delta):
# what we have: facing, current speed, move status, grounded
# we want: to set the new intended speed
var magnitude : float
if unit_conditions[Constants.UnitCondition.IS_ON_GROUND]:
magnitude = sqrt(pow(v_speed, 2) + pow(h_speed, 2))
else:
magnitude = abs(h_speed)
# if move status is idle
if unit_conditions[Constants.UnitCondition.MOVING_STATUS] == Constants.UnitMovingStatus.IDLE:
# slow down
magnitude = move_toward(magnitude, 0, Constants.ACCELERATION * delta)
# if move status is not idle
else:
# if is facing-aligned
if (h_speed <= 0 and facing == Constants.Direction.LEFT) or (h_speed >= 0 and facing == Constants.Direction.RIGHT):
# speed up
magnitude = move_toward(magnitude, target_move_speed, Constants.ACCELERATION * delta)
# if is not facing-aligned
else:
# slow down
magnitude = move_toward(magnitude, 0, Constants.ACCELERATION * delta)
# if is grounded
if unit_conditions[Constants.UnitCondition.IS_ON_GROUND]:
# make magnitude greater than quantum distance
if magnitude > 0 and magnitude < Constants.QUANTUM_DIST:
magnitude = Constants.QUANTUM_DIST * 2
# make move vector point down
if magnitude > 0:
if h_speed > 0:
h_speed = Constants.QUANTUM_DIST # preserve h direction
elif h_speed < 0:
h_speed = -1 * Constants.QUANTUM_DIST
else:
# from still to moving
if facing == Constants.Direction.RIGHT:
h_speed = Constants.QUANTUM_DIST
else:
h_speed = -1 * Constants.QUANTUM_DIST
else:
h_speed = 0
v_speed = -1 * magnitude
# if is not grounded
else:
# set h_speed
if magnitude > 0:
if h_speed > 0:
h_speed = magnitude
elif h_speed < 0:
h_speed = -1 * magnitude
else:
# from no lateral movement to having lateral movement
if facing == Constants.Direction.RIGHT:
h_speed = magnitude
else:
h_speed = -1 * magnitude
else:
h_speed = 0
func handle_idle():
if get_current_action() == Constants.UnitCurrentAction.IDLE:
if unit_conditions[Constants.UnitCondition.IS_ON_GROUND]:
if unit_conditions[Constants.UnitCondition.MOVING_STATUS] == Constants.UnitMovingStatus.IDLE:
set_sprite(Constants.SpriteClass.IDLE)
elif v_speed > 0:
set_sprite(Constants.SpriteClass.JUMP, 0)
else:
set_sprite(Constants.SpriteClass.JUMP, 1)
func set_sprite(sprite_class : int, index : int = 0):
assert(unit_type in Constants.UNIT_SPRITES)
assert(sprite_class in Constants.UNIT_SPRITES[unit_type])
var node_list = sprite_class_nodes[sprite_class]
var true_index : int = index
if true_index > len(node_list) - 1:
true_index = 0
var new_sprite : Node2D = node_list[true_index]
if (is_flash):
if int((time_elapsed - flash_start_timestamp) / Constants.FLASH_CYCLE) % 2 == 1:
new_sprite.set_modulate(Color(2, 1, 1))
else:
new_sprite.set_modulate(Color(1, .5, .5))
else:
new_sprite.set_modulate(Color(1, 1, 1))
if current_sprite == null or current_sprite != new_sprite:
if current_sprite != null:
current_sprite.visible = false
current_sprite = new_sprite
current_sprite.visible = true
if (Constants.UNIT_SPRITES[unit_type][sprite_class][0]):
current_sprite.set_frame(0)
current_sprite.play()
if facing == Constants.Direction.LEFT:
current_sprite.scale.x = -1
else:
current_sprite.scale.x = 1
func react(delta):
pos.x = pos.x + h_speed * delta
pos.y = pos.y + v_speed * delta
position.x = pos.x * Constants.GRID_SIZE * Constants.SCALE_FACTOR
position.y = -1 * pos.y * Constants.GRID_SIZE * Constants.SCALE_FACTOR
func hit(dir : int):
# implemented in subclass
hit_queued = true
hit_dir = dir
func start_flash():
is_flash = true
flash_start_timestamp = time_elapsed
func invincibility_ended():
# implemented in subclass
pass

View file

@ -0,0 +1,8 @@
extends NPCUnit
func before_tick():
if scene.rng.randf() < 0.5:
facing = Constants.Direction.RIGHT
else:
facing = Constants.Direction.LEFT

79
Scripts/Units/NPCUnit.gd Normal file
View file

@ -0,0 +1,79 @@
extends Unit
class_name NPCUnit
var spawn_point : Vector2
export var tick_duration : float
var tick_timer : float = 0
export(Dictionary) var action_sequence_map # action sequence to weight, [] = do nothing
# action sequence is an array of action type and an array of timestamps
# action type is the string representation of Constants.ActionType
# example action map: {[[action1], [0]]: 1, [[action2, action3], [0, 1]]: 2}
var weight_sum : float
export var action_duration_map = {} # specific durations for given action
var current_npc_action_times_elapsed = {}
var current_npc_action_active = {}
var current_action_sequence = null
var current_action_sequence_time_elapsed : float = 0
var current_action_sequence_index : int = 0
func _ready():
for action_sequence in action_sequence_map.keys():
weight_sum += action_sequence_map[action_sequence]
for action in action_duration_map:
current_npc_action_times_elapsed[action] = 0
current_npc_action_active[action] = false
func before_tick():
pass
func handle_input(delta):
if current_action_sequence != null:
for action in current_npc_action_active:
if current_npc_action_active[action]:
if current_npc_action_times_elapsed[action] < action_duration_map[action]:
set_action(Constants.ActionType.get(action))
current_npc_action_times_elapsed[action] += delta
else:
current_npc_action_active[action] = false
if (current_action_sequence_index < current_action_sequence[1].size()
and current_action_sequence_time_elapsed >= current_action_sequence[1][current_action_sequence_index]):
var action = current_action_sequence[0][current_action_sequence_index]
set_action(Constants.ActionType.get(action))
if action_duration_map.has(action):
current_npc_action_active[action] = true
current_npc_action_times_elapsed[action] = 0
current_action_sequence_index += 1
var current_action_sequence_duration : float = current_action_sequence[1][-1]
if action_duration_map.has(current_action_sequence[0][-1]):
current_action_sequence_duration += action_duration_map[current_action_sequence[0][-1]]
if current_action_sequence_time_elapsed > current_action_sequence_duration:
reset_npc_unit()
current_action_sequence_time_elapsed += delta
else:
if tick_timer == 0:
before_tick()
var rand_num : float = scene.rng.randf() * weight_sum
var temp_sum : float = 0
for action_sequence in action_sequence_map.keys():
temp_sum += action_sequence_map[action_sequence]
if temp_sum >= rand_num:
if action_sequence == []:
tick_timer = tick_duration
else:
current_action_sequence = action_sequence
current_action_sequence_time_elapsed = 0
current_action_sequence_index = 0
break
else:
tick_timer = max(0, tick_timer - delta)
func reset_npc_unit():
current_action_sequence = null
tick_timer = tick_duration
for action in current_npc_action_active:
current_npc_action_active[action] = false

92
Scripts/Units/Player.gd Normal file
View file

@ -0,0 +1,92 @@
extends Unit
# Player-specific code
class_name Player
const RECOIL_PUSHBACK = 15
func _init():
pos = Vector2(position.x / Constants.GRID_SIZE, -1 * position.y / Constants.GRID_SIZE)
position.x = position.x * Constants.SCALE_FACTOR
position.y = position.y * Constants.SCALE_FACTOR
func execute_actions(delta):
.execute_actions(delta)
for action_num in Constants.UNIT_TYPE_ACTIONS[Constants.UnitType.PLAYER]:
if !actions[action_num]:
continue
match action_num:
# handle custom actions
Constants.ActionType.RECOIL:
recoil()
_:
pass
func recoil():
if is_current_action_timer_done(Constants.UnitCurrentAction.RECOILING):
set_current_action(Constants.UnitCurrentAction.IDLE)
else:
set_current_action(Constants.UnitCurrentAction.RECOILING)
set_sprite(Constants.SpriteClass.RECOIL)
func handle_input(delta):
scene.handle_player_input()
func _on_Player_area_entered(area: Area2D) -> void:
if get_condition(Constants.UnitCondition.IS_INVINCIBLE, false):
return
if area is Unit:
hit_from_area(area)
func hit_from_area(other_area : Area2D):
var collision_dir : int
if other_area.position > position:
collision_dir = Constants.Direction.RIGHT
else:
collision_dir = Constants.Direction.LEFT
hit(collision_dir)
func hit(dir : int):
.hit(dir)
set_unit_condition_with_timer(Constants.UnitCondition.IS_INVINCIBLE)
start_flash()
set_action(Constants.ActionType.RECOIL)
set_current_action(Constants.UnitCurrentAction.RECOILING)
set_unit_condition(Constants.UnitCondition.MOVING_STATUS, Constants.UnitMovingStatus.IDLE)
func invincibility_ended():
is_flash = false
if get_overlapping_areas().size() > 0:
if get_overlapping_areas()[0] is Unit:
hit_from_area(get_overlapping_areas()[0])
func handle_recoil():
if not hit_queued:
return
hit_queued = false
if get_condition(Constants.UnitCondition.IS_ON_GROUND, true):
if h_speed > 0:
if hit_dir == Constants.Direction.LEFT:
v_speed -= RECOIL_PUSHBACK
else:
v_speed += RECOIL_PUSHBACK
elif h_speed < 0:
if hit_dir == Constants.Direction.LEFT:
v_speed += RECOIL_PUSHBACK
else:
v_speed -= RECOIL_PUSHBACK
else:
v_speed = -RECOIL_PUSHBACK
if hit_dir == Constants.Direction.LEFT:
h_speed = Constants.QUANTUM_DIST
else:
h_speed = -Constants.QUANTUM_DIST
if v_speed > 0:
h_speed *= -1
v_speed = -v_speed
else:
if hit_dir == Constants.Direction.LEFT:
h_speed += RECOIL_PUSHBACK
else:
h_speed -= RECOIL_PUSHBACK
facing = hit_dir