Opponent AI

Opponent AI

in

Without searching up guides or answers, I programmed my own AI from scratch.

Probalistic Modeling

My AI controls the opponent when the user chooses to play offline. However, the difficulty is in designing an opponent that plays like a human–you know, unpredictable and random. So, my AI uses “Probalistic Modeling”: instead of a simple “yes/no” decision, these models utilize a percentage chance of an event occurring (e.g. a 5% chance to jump). Therefore, every action of the opponent AI is determined by set chances. For example, the AI may randomly parry based on a low percentage, or rather when hit, there is a chance to parry the attack. This model keeps the decisions of the AI fresh and varied. Instead of fighting the same opponent continuously, you experience a new game each time you press play.

Right of Way

Settings

However, the opponent can not be entirely random. I lied when I joked that humans are random–in reality, human fencers have strengths which they are aware of, and even worse, they are aware of YOUR weaknesses. They will do anything to abuse your downfalls and score a point. Similarly, human players of fighting games have their own playstyles, which they represent through their actions and chosen characters. All in all, humans are not random, but intentional. They press “Play” already knowing their gameplan, then adapt it over the course of the match. My AI needs to reflect this. It needs to be customizable towards specific archetypes. Furthermore, during bouts, it needs to adapt in real-time, shifting its predetermined archetype. Thus, my AI has several settings that range from 0.0 to 1.0. These settings manipulate the aforementioned chances.

Later, I’ll go more in depth on the individual settings. For now, the settings are named as follows:

#All are from 0 to 1
	#EDIT PER CHARACTER
@export var keepsDistance : float
	#EDIT THESE PER DIFFICULTY
@export var reactionSetting : float
@export var spamSetting : float
@export var agroSetting : float
@export var parrySetting : float
@export var carefulSetting : float
@export var awakeSetting : float
@export var punishSetting : float
@export var blockSetting : float
@export var beatSetting : float

Weight-Based AI

Remember my example of “… or rather when hit, there is a chance to parry the attack.”? Well, this can’t be set-in-stone, now can it? Imagine you’re fighting an AI with a rushdown archetype (so, close-up and aggressive, e.g. high spamSetting and agroSetting but low carefulSetting). Let’s say the AI keeps running up and spamming attacks. If you keep utilizing right of way to score double-touches (so, no matter what the AI attacks you with, you still get the point), shouldn’t the AI change its gameplan? A real human would! Therefore, my AI adapts its probability model over time to the actions of the player. If you keep hitting the rushdown AI, it will note that you attack often, therefore it will increase its chance to parry. That aforementioned “chance to parry the attack” will go up because the AI will expect you to attack more often.

The AI stores your actions as follows:

var weightsTypes = {
	"Attack": 0.0,
	"Defense":0.0,
	"Movement":0.0,
	"Beat":0.0,
	"Throw":0.0,
	"Utility":0.0,
	"Super":0.0,
	"Taunt":0.0
	}

#... skip a lot of lines ...

func enemyActionDone(player : CharacterBody2D, actionName : String, actionType : String, actionCategory : String, range : Vector2,duration : float): #note that "enemy" refers to the enemy of the AI, thus the player in our example
	if player != chara: #chara is the AI's CharacterBody2D
		if actionType == "Attack":
			if actionCategory =="Light":
				weightsTypes[actionType] = clampf(weightsTypes[actionType] + 1.0,0.0,40.0)
			else:
				weightsTypes[actionType] = clampf(weightsTypes[actionType] + 2.5,0.0,40.0)
		else:
			weightsTypes[actionType] = clampf(weightsTypes[actionType] + 1.0,0.0,40.0)
		#maximumRange = range
		beginReaction(actionCategory == "Heavy")

#... skip lines ...

func _physics_process(delta : float) -> void:
    for i in range(weightsTypes.size()):
		weightsTypes[weightsTypes.keys()[i]] = snapped(clampf(weightsTypes[weightsTypes.keys()[i]] - 0.003,0.0,80.0),.001) #slowly forget about the enemy's decisions

As you can see, many action types are stored by the AI. When you commit to specific actions, the AI notes accordingly (and if you attack with a more committal, larger attack, the AI adds more weight to it). But, over time, these values are decreased. After all, if you the player change your gameplan, the AI should not be stuck in its old ways. This is especially important so that the player can not simply act one way to fool the AI, then completely switch up to get easy points. For example, to speedrun the Arcade mode, the player may spam attacks at the start of every round, and when the AI begins to spam parries in response, the player constantly runs up and grabs to push the AI off the strip. I don’t want any of that!

Right of Way (the rule)

Remember, you lose right of way if you miss an attack. The AI should know this. The probilistic model shifts depending on whether the opponent AI has right of way. For example, as seen below, if RoW is possessed, the AI will get closer and be less likely to do a raw attack. Of course, losing right of way is also its own chance–the AI may suddenly walk backward and lose RoW all because it got unlucky. Note: this “chanceToLoseRoW” is what enables most other possible actions; this chance must occur before the AI can run the chance of giving up right of way (walking backward, randomly attacking, etc.) This chance starts at 1.0 (100% likely to occur), meaning that the AI can do whatever it wants without fear of losing RoW. However, if RoW is possessed, this number decreases drastically. If you’re still confused, here’s an easy explanation:

  1. Have right of way?
    • Less likely to give it up.
  2. Don’t have right of way?
    • Everything is free game.

Here’s the code as promised:

func setVariables():
    #...
	chanceToLoseRoW = 1.0
	if roW == chara:
		minComfortDistance-=15.0
		maxComfortDistance-=20.0
		chanceToParryNew -= .26
		chanceToChangeMovement -= 0.2
		newChanceToSkipAction -= .1 #the chance is 1.0 - this... oops, too late, whatever
		chanceRawAttack -= .2
		chanceRawBigAttack += .12
		chanceToLoseRoW = snapped(0.4-(carefulSetting/6.0)+(spamSetting/5.0)-(awakeSetting/5.0),.001)
		chanceToBlock -= .2
		chanceToBeat += .15
	elif roW != chara and roW != null:
		minComfortDistance+=12.0
		maxComfortDistance+=17.0
		chanceToParryNew += .25
		chanceToChangeMovement += 0.2
		chanceRawAttack += .2
		chanceRawBigAttack -= .07
		chanceToParryReact -= .1
		chanceToBlock += .2
		chanceToBind += .1

WIP

Trial and Error

Unfortunantely, all these numbers seen thus far have to be manually set. That takes A LOT of trial and error. WIP

Future Plans

The AI will change so much over time, especially as I continue my trial-and-error.

Ideally, I want to implement a “Bayesian Interface”, which is a statistics method of updating probability based on new information.

Entire Code

Note: this will change a lot, but will not be updated often; I change specific chance numbers almost every day. This is the May 5th code:

extends Node

var roW : CharacterBody2D = null

var moveList
var attackList : Array[String]
var blockAll = false

#All are from 0 to 1
	#EDIT PER CHARACTER
@export var keepsDistance : float
	#EDIT THESE PER DIFFICULTY
@export var reactionSetting : float
@export var spamSetting : float
@export var agroSetting : float
@export var parrySetting : float
@export var carefulSetting : float
@export var awakeSetting : float
@export var punishSetting : float
@export var blockSetting : float
@export var beatSetting : float

var globalSettings = null
var prevCurrentAction : String
var currentAction := ""
var previousAction = ["Action","Type"]
var weightsTypes = {
	"Attack": 0.0,
	"Defense":0.0,
	"Movement":0.0,
	"Beat":0.0,
	"Throw":0.0,
	"Utility":0.0,
	"Super":0.0,
	"Taunt":0.0
	}
@onready var chara = get_parent()
var enemy : CharacterBody2D
var enemySM : Node
var rng = RandomNumberGenerator.new()
var connected = false

var enemyActionName
var enemyActionType
var enemyActionRange
var enemyActionDuration

var comfortable := false
var additionalRange : float = 0.0

var minComfortDistance #max distance away the AI wants to be
var maxComfortDistance #max closeness the AI wants to be
var chanceToChangeMovement
var chanceToIdle
var chanceToLoseRoW := 1.0 #chance to idle or walk backwards

var reactionTime
var punishTime
var chanceToSkipAction #really, the "chance" would be (1.0 - this)
var newChanceToSkipAction

var chanceToRawAttack
var chanceToAllowRawBigAttack
var chanceRawAttack
var chanceRawBigAttack
var chanceToParry
var chanceToParryNew
var chanceToParryReact
var chanceToPunish
var chanceToBlock
var chanceToBeat
var chanceToBind

 
var additionalRangeForSpam

var variablesSet := false
"""
@onready var minComfortDistance = 65.0 + int(keepsDistance*20) - int(agroSetting*10) #max distance away the AI wants to be
@onready var maxComfortDistance = 35.0 + int(keepsDistance*20) - int(agroSetting*10) #max closeness the AI wants to be
var chanceToChangeMovement = snapped(.23+(carefulSetting/5.0),.001)
var chanceToIdle = snapped(.18-(agroSetting/6.0),.001)
var comfortable := false

@onready var reactionTime = (23-(round(6.5*reactionSetting))) * (0.0167)
@onready var chanceToSkipAction = snapped(.7-(carefulSetting/5.0),.001) #really, the "chance" would be (1.0 - this)
var newChanceToSkipAction = chanceToSkipAction


@onready var chanceToRawAttack = clampf(snapped(.05+(agroSetting/7.0)+(spamSetting/2.0)-(carefulSetting/6.0),.001),0.05,0.7)
@onready var chanceToAllowRawBigAttack = clampf(snapped(.1+(agroSetting/6.0)-(spamSetting/5.0)-(carefulSetting/3.0),.001),.05,.15)
var chanceRawAttack = chanceToRawAttack
var chanceRawBigAttack = chanceToAllowRawBigAttack
@onready var chanceToParry = snapped(.5+(parrySetting/3.0)+(carefulSetting/8.0)-(agroSetting/6.0),.001)
var chanceToParryNew = chanceToParry
 
var additionalRange : float = 0.0
@onready var additionalRangeForSpam = snapped(15.0+(chanceToAllowRawBigAttack*40),.001)
"""

var maximumRange : Vector2
var reacting := false
var responding := false #once reacting timer expires, the AI is now "responding"
var punishing := false
var queuing := false
var queuedType
var queuedCategory
var queuedMustBeCategory
var distance : float

@onready var stateMachine = chara.get_node("StateMachine")
@onready var currentState = stateMachine.currentState


@onready var label = chara.get_node("AILabel")
@onready var label1 = chara.get_node("AILabel1")
@onready var labelWeights = chara.get_node("AILabelWeights")
@onready var AILabelThinking = chara.get_node("AILabelThinking")
@onready var SliderReaction = chara.get_node("SliderReaction")
@onready var SliderSpam = chara.get_node("SliderSpam")
@onready var SliderAgro = chara.get_node("SliderAgro")
@onready var SliderParry = chara.get_node("SliderParry")
@onready var SliderCareful = chara.get_node("SliderCareful")




func setLists():
	var moveListStorage = chara.get_node("MoveListStorage")
	moveList = moveListStorage.getListFromCharacter[chara.character][0]
	#if chara.character == "Sho":
	#	moveList = moveListStorage.shoMoveList
	#	attackList = moveListStorage.shoAttackList

func updateOriginalVariables():
	reactionSetting = SliderReaction.value
	spamSetting = SliderSpam.value
	agroSetting = SliderAgro.value
	parrySetting = SliderParry.value
	carefulSetting = SliderCareful.value
	
func setVariables():
	if not variablesSet:
		if globalSettings != null: #spamSetting,agroSetting,parrySetting,carefulSetting
			reactionSetting=globalSettings[0]
			spamSetting=globalSettings[1]
			agroSetting=globalSettings[2]
			parrySetting=globalSettings[3]
			carefulSetting=globalSettings[4]
			awakeSetting=globalSettings[5]
			punishSetting=globalSettings[6]
			blockSetting=globalSettings[7]
			beatSetting=globalSettings[8]
		else: #TODO this is where the base se
			reactionSetting=get_node("/root/Selections").aiTypes[0][0]
			spamSetting=get_node("/root/Selections").aiTypes[0][1]
			agroSetting=get_node("/root/Selections").aiTypes[0][2]
			parrySetting=get_node("/root/Selections").aiTypes[0][3]
			carefulSetting=get_node("/root/Selections").aiTypes[0][4]
			awakeSetting=get_node("/root/Selections").aiTypes[0][5]
			punishSetting=get_node("/root/Selections").aiTypes[0][6]
			blockSetting =get_node("/root/Selections").aiTypes[0][7]
			beatSetting =get_node("/root/Selections").aiTypes[0][8]
	var weightAdd = weightsTypes["Attack"]/7.0-weightsTypes["Defense"]/3.5
	minComfortDistance = 85.0 + int(keepsDistance*17) - int(agroSetting*13) + weightAdd #max distance away the AI wants to be
	maxComfortDistance = 60.0 + int(keepsDistance*17) - int(agroSetting*13) + weightAdd #max closeness the AI wants to be
	chanceToChangeMovement = snapped(.05-(carefulSetting/15.0)+(spamSetting/10.0)+(agroSetting/6.0),.001)
	chanceToIdle = snapped(.21-(agroSetting/6.0),.001)

	var reactionTimeFrames = 24 - round(7*reactionSetting) - round(6*awakeSetting) + randi_range(-1,3)
	reactionTime = (reactionTimeFrames - round(weightsTypes["Attack"]/15.0)) * (0.0167)
	punishTime = ((reactionTimeFrames) - (round(5*punishSetting))) * (0.0167)
	chanceToPunish = snapped(.39+(punishSetting/2.5)+(awakeSetting/3.5)+(carefulSetting/8.0)-(spamSetting/5.0)-(agroSetting/7.0) - (distance/700.0),.001)
	chanceToSkipAction = snapped(.5+(carefulSetting/11.0)+(punishSetting/7.0)-(spamSetting/5.0),.001) #really, the "chance" would be (1.0 - this)
	newChanceToSkipAction = chanceToSkipAction

	chanceToRawAttack = .27+(agroSetting/6.0)+(spamSetting/1.5)-(carefulSetting/6.5)-(blockSetting/10.0)-(awakeSetting/9.0)+(beatSetting/10.0)-(parrySetting/9.0)
	chanceToAllowRawBigAttack = .1+(agroSetting/4.0)-(spamSetting/5.0)-(carefulSetting/4.0)+(awakeSetting/9.0)-(punishSetting/8.0)
	chanceRawAttack = chanceToRawAttack
	chanceRawBigAttack = chanceToAllowRawBigAttack
	chanceToParry = snapped(.12+(parrySetting/2.25)+(carefulSetting/10.0)-(agroSetting/6.0)-(spamSetting/7.0)+(awakeSetting/6.0)+(punishSetting/9.0),.001)
	chanceToParryNew = chanceToParry
	chanceToParryReact = snapped(chanceToParry + (awakeSetting/2.0)+(punishSetting/5.0) - (spamSetting / 7.0) - (agroSetting/8.0),.001)
	
	chanceToBlock = snapped(.13+(blockSetting/2.0)+(carefulSetting/9.0)-(agroSetting/3.5)-(spamSetting / 3.5)+(awakeSetting/3.0),.001)
	chanceToBeat = snapped(.24+(beatSetting/3.0)-(carefulSetting/11.0)+(agroSetting/6.0)+(spamSetting / 7.0)+(punishSetting/8.0),.001)
	chanceToBind = snapped(.14+(carefulSetting/11.0)-(spamSetting / 10.0)+(punishSetting/3.5)+(awakeSetting/7.0)+(agroSetting/9.0),.001)
	 
	additionalRangeForSpam = snapped(15.0+(chanceToAllowRawBigAttack*40),.001)
	
	chanceToLoseRoW = 1.0
	if roW == chara:
		minComfortDistance-=15.0
		maxComfortDistance-=20.0
		chanceToParryNew -= .26
		chanceToChangeMovement -= 0.2
		newChanceToSkipAction -= .1 #the chance is 1.0 - this
		chanceRawAttack -= .2
		chanceRawBigAttack += .12
		chanceToLoseRoW = snapped(0.4-(carefulSetting/6.0)+(spamSetting/5.0)-(awakeSetting/5.0),.001)
		chanceToBlock -= .2
		chanceToBeat += .15
	elif roW != chara and roW != null:
		minComfortDistance+=12.0
		maxComfortDistance+=17.0
		chanceToParryNew += .25
		chanceToChangeMovement += 0.2
		chanceRawAttack += .2
		chanceRawBigAttack -= .07
		chanceToParryReact -= .1
		chanceToBlock += .2
		chanceToBind += .1
		
	variablesSet = true
	
func updateChanceVariables():
	chanceToParryNew=clampf(snapped(chanceToParryNew+weightsTypes["Attack"]/140.0-weightsTypes["Defense"]/60.0-(distance/260.0),.01),.058,0.75)
	chanceToParryReact=clampf(snapped(chanceToParryReact+weightsTypes["Attack"]/110.0-weightsTypes["Defense"]/80.0-(distance/200.0),.01),.058,0.75)
	chanceRawAttack = clampf(snapped(chanceRawAttack-(distance/1300.0),.001),0.05,0.92)
	chanceRawBigAttack = clampf(snapped(chanceRawBigAttack+(distance/900.0),.001),.04,.6)
	chanceToChangeMovement = clampf(snapped(chanceToChangeMovement-(distance/1300.0),.001),0.03,0.25)
	#chanceToBeat=clampf(snapped(chanceToBeat+weightsTypes["Attack"]/110.0-weightsTypes["Defense"]/80.0-(distance/200.0),.01),.058,0.75)
	chanceToBlock=clampf(snapped(chanceToBlock+weightsTypes["Attack"]/120.0-weightsTypes["Defense"]/90.0-(weightsTypes["Beat"]/90.0),.01),.2,0.77)
	chanceToBind=clampf(snapped(chanceToBind+(weightsTypes["Beat"]/55.0),.01),.05,0.6)

func _ready() -> void:
	#print("waiting...")
	await chara.get_node("LastToLoad").ready
	if not get_parent().aiMode:
		label.queue_free()
		label1.queue_free()
		labelWeights.queue_free()
		SliderReaction.queue_free()
		SliderSpam.queue_free()
		SliderAgro.queue_free()
		SliderParry.queue_free()
		SliderCareful.queue_free()
		get_parent().get_node("FreezeAIButton").queue_free()
		chara.get_node("SliderInfo").queue_free()
		queue_free()
	setVariables()
	setLists()
	SliderReaction.value = reactionSetting
	SliderSpam.value=spamSetting
	SliderAgro.value=agroSetting
	SliderParry.value=parrySetting
	SliderCareful.value=carefulSetting

func _process(delta: float) -> void:
	#updateOriginalVariables()
	setVariables()
	updateChanceVariables()
	setLabelText()
	
func _physics_process(delta: float) -> void:
	if not variablesSet:
		return
	if enemy != null and not connected:
		connected = true
		enemy.actionDone.connect(enemyActionDone)
		enemy.hitboxGone.connect(enemyHitboxGone)
	currentState = stateMachine.currentState
	distance = snapped(chara.position.distance_to(enemy.position),.001)
	if distance > minComfortDistance or distance < maxComfortDistance:
		comfortable = false
	else:
		comfortable = true
	"""
	if rng.randf_range(0,1) < chanceToChangeMovement:
		if distance > minComfortDistance or rng.randf_range(0,1) < chanceToChangeMovement / 5.0:
			doConstantAction("WalkForward",Vector2(1,0))
			comfortable = false
		elif distance < maxComfortDistance or rng.randf_range(0,1) < chanceToChangeMovement / 12.0:
			doConstantAction("WalkBackward",Vector2(-1,0))
			comfortable = false
		elif rng.randf_range(0,1) < chanceToIdle: #dont wanna lose right of way yk
			doConstantAction("Idle",Vector2(0,0))
			comfortable = true
	"""
	
	for i in range(weightsTypes.size()):
		weightsTypes[weightsTypes.keys()[i]] = snapped(clampf(weightsTypes[weightsTypes.keys()[i]] - 0.003,0.0,80.0),.001)
		
	#if responding:
	#	return
	if queuing and currentState == currentState.neutralState: #TODO what about reacting?
		chooseAction(queuedType,queuedCategory,0,queuedMustBeCategory)
	#if queuing:
	#	return
	#if punishing:
	#	return
	if responding or queuing or punishing:
		return
		
	if comfortable:
		processComfortable()
	elif distance < maxComfortDistance:
		processTooClose()
	elif distance > minComfortDistance:
		processTooFar()
	"""
	if rng.randf() < chanceToChangeMovement/5.0:
		if comfortable:
			if rng.randf() < .2:
				chooseAction("Movement","Dash",0,false)
			elif rng.randf() < .3:
				chooseAction("Movement","Backdash",0,false)
		elif distance<maxComfortDistance and rng.randf() < .5:
			chooseAction("Movement","Backdash",0,false)
		elif distance>minComfortDistance and rng.randf() < .2:
			chooseAction("Movement","Dash",0,false)
	elif (rng.randf_range(0,1) < chanceRawAttack/5.0) or (comfortable and rng.randf_range(0,1) < chanceRawAttack): #spam raw attack when at a comfortable distance
		if rng.randf_range(0,1)<chanceToParryNew:
			chooseAction("Defense","Parry",0,true)
		elif rng.randf_range(0,1) < chanceRawBigAttack:
			#print("AI is allowing a raw big attack!") #allowing; wont always be big, just allows the possibility
			chooseAction("Attack","Medium",0,false)
		else:
			additionalRange = additionalRangeForSpam
			chooseAction("Attack","Light",0,true)
	"""
	
func processComfortable():
	if rng.randf() < chanceToChangeMovement:
		if roW == enemy and rng.randf() < .2:
			doConstantAction("WalkForward",Vector2(1,0))
		elif roW != enemy and rng.randf() < .4:
			doConstantAction("WalkForward",Vector2(1,0))
		elif rng.randf() < chanceToLoseRoW and rng.randf() < .19: #the "and" is important here. the first will only be false if in possession of RoW (see setVariables()).
			doConstantAction("Idle",Vector2(0,0))
		elif (roW == enemy and rng.randf() < .8) or (roW == self and rng.randf() < chanceToLoseRoW): #the "and" is important here. the first will only be false if in possession of RoW (see setVariables()).
			doConstantAction("WalkBackward",Vector2(-1,0))
		return
	
	if rng.randf() < chanceToChangeMovement/32.0:
		if rng.randf() < .35:
			chooseAction("Movement","Dash",0,false)
		elif rng.randf() < chanceToLoseRoW:
			chooseAction("Movement","Backdash",0,true)
	elif rng.randf_range(0,1) < chanceRawAttack: #spam raw attack when at a comfortable distance
		if rng.randf_range(0,1)<chanceToBind:
			chooseAction("Beat","Bind",0,true)
		elif rng.randf_range(0,1)<chanceToBeat:
			chooseAction("Beat","Beat",0,true)
		elif rng.randf_range(0,1)<chanceToParryNew:
			chooseAction("Defense","Parry",0,true)
		elif rng.randf_range(0,1) < chanceRawBigAttack:
			#print("AI is allowing a raw big attack!") #allowing; wont always be big, just allows the possibility
			chooseAction("Attack","Medium",0,false)
		else:
			additionalRange = additionalRangeForSpam
			chooseAction("Attack","Light",0,true)
			
func processTooClose():
	if rng.randf() < chanceToChangeMovement / 1.25:
		if rng.randf() < .05:
			if rng.randf() < .08:
				chooseAction("Movement","Dash",0,false)
			else:
				doConstantAction("WalkForward",Vector2(1,0))
		elif rng.randf() < .12:
			doConstantAction("Idle",Vector2(0,0))
		elif rng.randf() < chanceToLoseRoW:
			if rng.randf() < .35:
				chooseAction("Movement","Backdash",0,true)
				return
			else:
				doConstantAction("WalkBackward",Vector2(-1,0))
		"""
	if rng.randf() < chanceToChangeMovement/5.0:
		if rng.randf() < .05:
			chooseAction("Movement","Dash",0,false)
			return
		elif rng.randf() < .4:
			chooseAction("Movement","Backdash",0,false)
			return
			"""
	if (rng.randf_range(0,1) < chanceRawAttack):
		if rng.randf_range(0,1)<chanceToBind:
			chooseAction("Beat","Bind",0,true)
		elif rng.randf_range(0,1)<chanceToBeat/1.3:
			chooseAction("Beat","Beat",0,true)
		elif rng.randf_range(0,1)<chanceToParryNew * 1.8:
			chooseAction("Defense","Parry",0,true)
		elif rng.randf_range(0,1) < chanceRawBigAttack / 4.0:
			#print("AI is allowing a raw big attack!") #allowing; wont always be big, just allows the possibility
			chooseAction("Attack","Medium",0,false)
		else:
			additionalRange = additionalRangeForSpam
			chooseAction("Attack","Light",0,true)
			
func processTooFar():
	if rng.randf() < chanceToChangeMovement * 1.25:
		if rng.randf() < .03:
			if rng.randf() < .2:
				chooseAction("Movement","Backdash",0,true)
			else:
				doConstantAction("WalkBackward",Vector2(-1,0))
		elif rng.randf() < .12:
			doConstantAction("Idle",Vector2(0,0))
		else:
			if rng.randf() < .32:
				chooseAction("Movement","Dash",0,true)
			else:
				doConstantAction("WalkForward",Vector2(1,0))
		return
	if (rng.randf_range(0,1) < chanceRawAttack/7.0):
		if rng.randf_range(0,1)<chanceToBind/2.0:
			chooseAction("Beat","Bind",0,true)
		elif rng.randf_range(0,1)<chanceToBeat/2.5:
			chooseAction("Beat","Beat",0,true)
		if rng.randf_range(0,1)<chanceToParryNew / 2.5:
			chooseAction("Defense","Parry",0,true)
		elif rng.randf_range(0,1) < chanceRawBigAttack * 4.0:
			#print("AI is allowing a raw big attack!") #allowing; wont always be big, just allows the possibility
			chooseAction("Attack","Medium",0,false)
		else:
			additionalRange = additionalRangeForSpam
			chooseAction("Attack","Light",0,true)

func enemyActionDone(player : CharacterBody2D, actionName : String, actionType : String, actionCategory : String, range : Vector2,duration : float):
	if player != chara:
		if actionType == "Attack":
			if actionCategory =="Light":
				weightsTypes[actionType] = clampf(weightsTypes[actionType] + 1.0,0.0,40.0)
			else:
				weightsTypes[actionType] = clampf(weightsTypes[actionType] + 2.5,0.0,40.0)
		else:
			weightsTypes[actionType] = clampf(weightsTypes[actionType] + 1.0,0.0,40.0)
		#maximumRange = range
		beginReaction(actionCategory == "Heavy")



func beginReaction(reactingToHeavyAttack):
	var reactionTimeNew = reactionTime
	if reactingToHeavyAttack:
		reactionTimeNew += 5 * .0167
	$ReactionTimer.wait_time = reactionTimeNew
	$ReactionTimer.start()
	reacting = true
	
#REACTION
func _on_reaction_timer_timeout() -> void:
	reacting = false
	additionalRange = 0.0
	if currentState.name == "Neutral":
		if rng.randf_range(0,1)<chanceToParryReact:
			chooseAction("Defense","Parry",0,true)
		else:
			punishBasedOnRange()
		
		
func chooseAction(actionType : String, actionCategory : String,searchIndex : int,mustBeCategory): #TODO searchIndex seems useless
	if responding and searchIndex == 0:
		return
	responding = true
	"""
	var chosenAction
	if actionType == "Attack":
		if searchIndex>=attackList.size():
			print_debug("Not a single move was viable for the AI to react with!")
			return
		chosenAction = attackList[searchIndex]
	elif searchIndex>=moveList.size():
		print_debug("AI ran out of possible moves to react with.")
		return
	"""
	if searchIndex>=moveList.size():
		#print("Not a single move was viable for the AI to react with!")
		#print_debug(moveList.keys()[searchIndex-1])
		responding = false
		#clearQueue()
		return
	var chosenAction = moveList.keys()[searchIndex]
	
	var actionRangeHypotenuse = sqrt(pow(moveList[chosenAction][4].x,2)+pow(moveList[chosenAction][4].y,2)) + additionalRange
	
	newChanceToSkipAction = chanceToSkipAction
	if abs(distance) > 15: #more likely to do an attack if closer
		newChanceToSkipAction -= snapped(abs(1/pow(distance,.20)),.01)
	else:
		newChanceToSkipAction = .1
		
	
	#currentState.moveList.has(chosenAction) 
	if currentState.canDoActionName(chosenAction) and actionType == moveList[chosenAction][2] and rng.randf_range(0,1) > newChanceToSkipAction and (rng.randf_range(0,1) > .94 or distance <= actionRangeHypotenuse): #if can do an action, is action in the available moves? is action viable for that state?
		if mustBeCategory and moveList[chosenAction][3] != actionCategory:
			#print_debug("The following move was not viable CATEGORY for the AI to react with: "+chosenAction)
			chooseAction(actionType,actionCategory,searchIndex+1,mustBeCategory) #TODO wtf am i even writing anymore
		else:
			#print("Chose an action!")
			doAction(chosenAction,moveList[chosenAction][1],moveList[chosenAction][2],moveList[chosenAction][3],moveList[chosenAction][4],moveList[chosenAction][5])
			return
			
	#print_debug("The following move was not viable for the AI to react with: "+chosenAction)
	chooseAction(actionType,actionCategory,searchIndex+1,mustBeCategory)


func doAction(action,endState,type,category,range,duration):
	#print("Doing action! "+action)
	$ResponseTimer.start()
	currentAction = action
	currentState.constantAction = ""
	currentState.doAction(action,endState,type,category,range,duration) #state.doAction(name)
	clearQueue()
	#responding = false
	
func queueAction(type,category,mustBeCategory):
	queuing = true
	queuedType = type
	queuedCategory = category
	queuedMustBeCategory = mustBeCategory
	
func clearQueue():
	queuing = false
	queuedType = null
	queuedCategory = null
	queuedMustBeCategory = null

func doConstantAction(action,inputDir):
	if currentState.canDoConstantAction() and not responding: #TODO i dont like this
		currentAction = action
		currentState.doConstantAction(action,inputDir)

"""
func sendConstantAction(action): #intended to save memory, dont want to constantly communicate between scripts 24/7
	if action != prevCurrentAction:
		prevCurrentAction = action
		if not doAction(action):
			prevCurrentAction = ""
		
func doAction(action):
	parent.get_node("StateMachine").doAction(action)
	"""
func resetDictionaries(): #TODO useless
	print("Reset the dictionaries")


func _on_freeze_ai_button_pressed() -> void:
	set_physics_process(!is_physics_processing())
	doConstantAction("Idle",Vector2(0,0))
	#updateOriginalVariables()
	#updateChanceVariables()

func setLabelVisibility():
	var isVisible = label.visible
	label.visible = !isVisible
	label1.visible = !isVisible
	labelWeights.visible = !isVisible
	SliderReaction.visible = !isVisible
	SliderSpam.visible=!isVisible
	SliderAgro.visible=!isVisible
	SliderParry.visible=!isVisible
	SliderCareful.visible=!isVisible
	chara.get_node("SliderInfo").visible=!isVisible


func _on_response_timer_timeout() -> void:
	responding = false

func reactToLandedParry(perfect : bool):
	if rng.randf_range(0,1) > chanceToPunish:
		return
	punishing = true
	#if perfect:
	#	punishTime += (2 * .0167)
	$PunishTimer.wait_time = punishTime
	$PunishTimer.start()
	#queueAction("Attack","Light",true)

func punishBasedOnRange():
	if distance < 18.0: #TODO get rid of this once you add a doActionBasedOnRange() function
		queueAction("Attack","Light",true)
	elif distance < 35.0:
		queueAction("Attack","Light",false)
	else:
		queueAction("Attack","Medium",true)
		
func _on_punish_timer_timeout() -> void:
	punishBasedOnRange()
	#await get_tree().create_timer(2 * .0167).timeout #just so that the AI has time for its punish before physics_process goes back to running the AI
	punishing = false

func enemyHitboxGone(player, _framesLeft):
	return #TODO I'll deal with this later since the AI already reacts
	if player == chara:
		return
	print("HITBOX GONE")
	if rng.randf_range(0,1) < chanceToPunish / 3.0:
		punishing = true
		$PunishTimer.wait_time = punishTime
		$PunishTimer.start()

func canForwardParry():
	if currentState != currentState.neutralState or responding or queuing or punishing:
		return false
	return rng.randf_range(0,1) < chanceToParryReact

func canBlock():
	if blockAll:
		return true
	if currentState != currentState.neutralState or responding or queuing or punishing:
		return false
	return (currentAction=="WalkBackward") or rng.randf_range(0,1) < chanceToBlock

func setLabelText():
	"""AILabelThinking.text = ("Reacting: "+str(reacting)+"
	Responding: "+str(responding)+"
	Queuing "+str(queuing)+"
	Punishing "+str(punishing))
	"""
	if reacting:
		label.text = (
		"Distance: "+str(distance)+"	
		MinDis: "+str(minComfortDistance)+"
		MaxDis: "+str(maxComfortDistance)+"
		Comfortable: "+str(comfortable)+"
		Reaction time: "+str(reactionTime)+"
		Chance to move: "+str(chanceToChangeMovement)+"
		Chance to not act: "+str(newChanceToSkipAction)+"
		Doing: "+currentAction+"
		Reacting!"
		)
	else:
		label.text = (
		"Distance: "+str(distance)+"
		MinDis: "+str(minComfortDistance)+"
		MaxDis: "+str(maxComfortDistance)+"
		Comfortable: "+str(comfortable)+"
		Reaction time: "+str(reactionTime)+"
		Chance to move: "+str(chanceToChangeMovement)+"
		Chance to not act: "+str(newChanceToSkipAction)+"
		Doing: "+currentAction
		)
	label1.text = (
		"Chances:
		RawAttack: "+str(chanceRawAttack)+"
		RawBigAttack: "+str(chanceRawBigAttack)+"
		Parry: "+str(chanceToParryNew)
	)
	labelWeights.text = str(weightsTypes)