beatemup combat system

Created: Jul 09, 2022Published: Mar 28, 2023Last modified: Apr 05, 2023
No tags
Word count: 10

a Char.gd that I was happy with

extends KinematicBody2D

var velocity = Vector2()
var speed = 200

enum face_dir {LEFT, RIGHT}
var facing = face_dir.LEFT

export(bool) var is_player = false

onready var animated_sprite = $AnimatedSprite
onready var facing_detector = $FacingDetector

onready var punchbox = $Punchbox
onready var kickbox = $Kickbox
onready var hurtbox = $Hurtbox

### ready #####################################################################

func _ready():
  Notif.notif("ready")

func get_move_vector():
  return Trols.move_dir() if is_player else Vector2()

### process #####################################################################

func flip_transform(area):
  match facing:
    face_dir.LEFT:
      area.transform.origin.x = -abs(area.transform.origin.x)
    face_dir.RIGHT:
      area.transform.origin.x = abs(area.transform.origin.x)

func update_facing(dir: int):
  animated_sprite.flip_h = dir == face_dir.LEFT
  flip_transform(animated_sprite)
  flip_transform(facing_detector)
  flip_transform(punchbox)
  flip_transform(kickbox)
  flip_transform(hurtbox)

func _process(_delta):
  var move_vector: Vector2 = get_move_vector()
  var old_facing = facing
  if move_vector.x > 0:
    facing = face_dir.RIGHT
  elif move_vector.x < 0:
    facing = face_dir.LEFT

  if old_facing != facing:
    update_facing(facing)

  if stunned:
    animated_sprite.animation = "stunned"
  elif knocked_back:
    animated_sprite.animation = "knocked_back"
  elif kicking:
    animated_sprite.animation = "kick"
  elif punching:
    animated_sprite.animation = "punch"
  elif move_vector.length() > 0:
    animated_sprite.animation = "walk"
  else:
    animated_sprite.animation = "idle"

### physics_process #####################################################################

const DECELERATION = 100

func _physics_process(delta):
  var move_vector: Vector2 = get_move_vector()
  if not punching and not kicking and not stunned and not knocked_back:
    velocity = move_vector * speed

  velocity = velocity.move_toward(Vector2(), DECELERATION * delta)

  velocity = move_and_slide(velocity)

### unhandled_input #####################################################################

func _unhandled_input(event):
  if is_player and Trols.is_attack(event):
    attack()

### attacking #####################################################################

var stunned = false
var knocked_back = false

var punching = false
var punch_windup = 0.1
var punch_cooldown = 0.2

var kicking = false
var kick_windup = 0.1
var kick_cooldown = 0.3

var in_punchbox = []
var in_kickbox = []

onready var combo_timer = $ComboTimer
export(float) var combo_timeout := 1
var combo_count = 0

signal punch
signal kick

func can_attack():
  return not punching and not kicking and not stunned and not knocked_back

var attack_queue = 0

func attack():
  if can_attack():
    if combo_count >= 2:
      reset_combo()
      kick()
    else:
      combo_count += 1
      punch()

      # restart the combo timer.
      combo_timer.start(combo_timeout)
  else:
    if punching or kicking:
      if attack_queue < 1:
        attack_queue += 1
      else:
        print("refusing to queue more attacks")
    elif stunned or knocked_back:
      print("dead input, attack input while stunned/knocked_back")
    else:
      print("some other dead input on attack")


func _on_ComboTimer_timeout():
  reset_combo()

# Resets the combo and the queue
func reset_combo():
  combo_count = 0
  attack_queue = 0

func punch():
  punching = true
  yield(get_tree().create_timer(punch_windup), "timeout")

  if not stunned and not knocked_back:
    emit_signal("punch", self) # maybe just for metrics/ui?

    for ch in in_punchbox:
      print("punching: ", ch)
      if ch.has_method("take_punch"):
        ch.take_punch(self)

  yield(get_tree().create_timer(punch_cooldown), "timeout")
  punching = false
  if attack_queue > 0:
    attack_queue -= 1
    attack()

func kick():
  kicking = true
  yield(get_tree().create_timer(kick_windup), "timeout")

  if not stunned and not knocked_back:
    emit_signal("kick", self)

    for ch in in_kickbox:
      print("kicking: ", ch)
      if ch.has_method("take_kick"):
        ch.take_kick(self)

  yield(get_tree().create_timer(kick_cooldown), "timeout")
  kicking = false

### defending? #####################################################################

var stunned_time = 0.3
var knocked_back_time = 0.5
export(int) var PUNCH_FORCE = 20
export(int) var KICK_FORCE = 100

func take_punch(attacker):
  reset_combo()
  face_attacker(attacker)
  stunned = true
  # TODO consider only knocking back on punch if they are close
  # i.e. don't punch them out of punch-range
  apply_attack(attacker, PUNCH_FORCE)
  yield(get_tree().create_timer(stunned_time), "timeout")
  stunned = false

func take_kick(attacker):
  reset_combo()
  face_attacker(attacker)
  knocked_back = true
  apply_attack(attacker, KICK_FORCE)
  yield(get_tree().create_timer(knocked_back_time), "timeout")
  knocked_back = false

func apply_attack(attacker, attack_force):
  var force = Vector2(1, 0) * attack_force
  if attacker.position.x > get_global_position().x:
    # attacker is on the right, so x should be negative
    force = force * -1
  velocity += force

func face_attacker(attacker):
  if attacker.position.x > get_global_position().x:
    facing = face_dir.RIGHT
  elif attacker.position.x < get_global_position().x:
    facing = face_dir.LEFT
  update_facing(facing)

### hitbox signals ##########################################################

func _on_Punchbox_area_entered(area):
  if area.is_in_group("hurtboxes") and area != hurtbox:
    in_punchbox.append(area.get_parent())

func _on_Punchbox_area_exited(area):
  in_punchbox.erase(area.get_parent())

func _on_Kickbox_area_entered(area):
  if area.is_in_group("hurtboxes") and area != hurtbox:
    in_kickbox.append(area.get_parent())

func _on_Kickbox_area_exited(area):
  in_kickbox.erase(area.get_parent())