extends AIScriptModule # Declare member variables here. Examples: # var a = 2 # var b = "text" # Called when the node enters the scene tree for the first time. var skel var hair_skel var name var garments_female_lingerie = ["female-panties1", "female-bra1"] var garments_head_female = [] var garments_male_lingerie = ["male-panties1"] var garments_head_male = [] var garments_female_main = ["female-shirt_skirt1"] var garments_male_main = ["male-pants1", "male-shoes1"] var basedir = "res://scenes/clothes/" var material_female = preload("res://scenes/clothes/nun-clothes.material") var material_male = preload("res://scenes/clothes/clothes-male.material") var state = 0 var rnd: RandomNumberGenerator const attack_distance = 2.0 const guard_distance = 2.0 const engage_distance = 10.0 const flee_distance = 5.0 var init_blackboard = { "stamina": 100.0, "health": 100.0, "melee_weapon_equipped": false, "attack_cooldown": 0.0, "guard_cooldown": 0.0, "flee_cooldown": 0.0, "guard": false, "melee_damage": false, "dot": 0.0 } func look_at(tick, ch): var root = get_character(tick) var current = root.global_transform var at = ch.global_transform.origin at.y = current.origin.y root.global_transform = current.looking_at(at, Vector3.UP) func combat_event(ev, data, blackboard): var root = blackboard.self var cam = root.get_viewport().get_camera() var player if cam: player = cam.get_meta("player") assert(player) match ev: "about_to_attack": var who = data[0] if who != root: var where = who.global_transform.origin var curpos = root.global_transform.origin var dst = where.distance_squared_to(curpos) if dst < guard_distance * guard_distance: if who == player: blackboard["guard"] = true # blackboard_set(tick, "melee_damage", true) "damage": var who = data[0] var weapon = data[1] if who != root: var where = who.global_transform.origin var curpos = root.global_transform.origin var dst = where.distance_squared_to(curpos) if dst < attack_distance * attack_distance: if who == player: blackboard["melee_damage"] = true # blackboard_set(tick, "melee_damage", true) func init(tick): assert(tick) assert(!get_memory(tick).has("initialized")) name = "bandit_ai" var root = get_character(tick) for e in init_blackboard.keys(): blackboard_set(tick, e, init_blackboard[e]) blackboard_set(tick, "self", root) assert(root.has_meta("skeleton")) rnd = RandomNumberGenerator.new() rnd.randomize() root.add_to_group("bandits") root.add_to_group("bandit") combat.connect("event", self, "combat_event", [blackboard_get_dict(tick)]) var character_data = root.get_meta("character_data") if character_data.sex == "female": var g = garments_female_lingerie var h = [] g += garments_female_main h += garments_head_female characters.call_deferred("setup_garments", root, g, h, material_female) else: var g = garments_male_lingerie var h = [] g += garments_male_main h += garments_head_male characters.call_deferred("setup_garments", root, g, h, material_male) state = 0 for e in conf_behaviors.keys(): if !run_behaviors.has(e): run_behaviors[e] = conf_behaviors[e].new() get_memory(tick).initialized = true var cooldown = 0.0 class base_bhv: var active = false var finished = false func run(delta: float, bb: Dictionary): pass func stop(): pass func activate(): pass class walkto extends base_bhv: signal arrived(where) var state = 0 var where: Vector3 var speed: = 0.0 var gap: = 0.0 var root var timeout: = 0.0 var pdist = INF var cost: = 0.0 func look_at(root): var current = root.global_transform var at = where at.y = current.origin.y root.global_transform = current.looking_at(at, Vector3.UP) root.set_meta("orientation", Transform(root.global_transform.basis, Vector3())) func run(delta: float, bb: Dictionary): assert(active) assert(root) var gpos = root.global_transform.origin var dst = gpos.distance_squared_to(where) match state: 0: look_at(root) characters.animation_node_travel(root, "locomotion") characters.set_walk_speed(root, speed, 0) state = 1 1: bb.stamina -= cost * delta timeout -= delta state = 2 2: if dst < gap * gap: emit_signal("arrived", gpos) state = 10 elif timeout < 0.0: root.global_transform.origin = where emit_signal("arrived", gpos) state = 20 elif dst > pdist && dst > gap * gap: root.global_transform.origin = where emit_signal("arrived", gpos) state = 30 printerr("missed the target: ", sqrt(dst), gap) elif dst < 3.0 * gap * gap && timeout > 0.0: characters.set_walk_speed(root, 0.5 * speed, 0) 10: finished = true 20: finished = true 30: finished = true 200: timeout -= delta if timeout < 0.0: root.global_transform.origin = where characters.set_walk_speed(root, 0, 0) finished = true state = 300 300: printerr("should not be here: ", sqrt(dst), gap) breakpoint # print("walkto state: ", state) pdist = dst func start(root: Node, where: Vector3, speed: float, gap: float, cost: float, timeout: float): assert(root) assert(!active) state = 0 self.root = root self.where = where self.gap = gap self.timeout = timeout self.speed = speed self.cost = cost active = true func stop(): assert(active) assert(root) characters.set_walk_speed(root, 0, 0) active = false finished = false state = 0 # print("walkto stopped") class attack extends base_bhv: var root var anim var event var state = 0 func start(root: Node, anim: String, event: String): self.root = root self.anim = anim self.event = event active = true func look_at(root, bb): var current = root.global_transform var at = bb.enemy_pos at.y = current.origin.y root.global_transform = current.looking_at(at, Vector3.UP) root.set_meta("orientation", Transform(root.global_transform.basis, Vector3())) func run(delta: float, bb: Dictionary): assert(active) assert(root) # print("attack state: ", state, " ", active, " ", finished) if finished: return # print("attack running") bb.attack_cooldown -= delta if bb.attack_cooldown > 0.0: return match state: 0: combat.emit_signal("event", event, [root]) look_at(root, bb) state = 1 1: bb.stamina -= 0.5 * delta bb.stamina -= 3500 * delta characters.animation_node_travel(root, anim) # print("attack: ", anim) state = 2 2: bb.stamina -= 5 * delta bb.attack_cooldown = 1.8 finished = true # print("attack ", anim, " ", event, " done") func stop(): assert(active) assert(root) active = false finished = false state = 0 class take_weapon extends base_bhv: var weapon var root func start(root: Node, weapon): self.root = root self.weapon = weapon active = true func activate(): pass func run(delta: float, bb: Dictionary): assert(active) assert(root) if finished: return if bb.melee_weapon_equipped: return var skel = root.get_meta("skeleton") var wslot = skel.get_node("wrist_r/weapon_right") wslot.set_meta("owner", root) inventory.equip(wslot, weapon) bb.melee_weapon_equipped = true finished = true func stop(): # can't re-run this one assert(active) assert(root) finished = false active = false finished = false class guard extends base_bhv: var root var anim1 var anim2 var state = 0 func start(root: Node, anim1, anim2: String): self.root = root self.anim1 = anim1 self.anim2 = anim2 active = true func run(delta: float, bb: Dictionary): assert(active) assert(root) # print("guard state: ", state, " ", finished) if finished: return match state: 0: # guard var anim if bb.dot < 0: anim = anim1 else: anim = anim2 characters.animation_node_travel(root, anim) # print("guard playing animation ", anim) state = 1 1: state = 2 2: bb.guard_cooldown = 1.5 finished = true class get_damage extends base_bhv: var root var anim1 var anim2 var state = 0 var cost = 0.0 func start(root: Node, anim1, anim2: String, cost: float): self.root = root self.anim1 = anim1 self.anim2 = anim2 self.cost = cost active = true func run(delta: float, bb: Dictionary): assert(active) assert(root) if finished: return match state: 0: # guard var anim if bb.dot < 0: anim = anim1 else: anim = anim2 characters.animation_node_travel(root, anim) if bb.stamina > 50.0: bb.health -= cost else: bb.health -= 5.0 * cost bb.stamina = 5.0 # print("guard playing animation ", anim) state = 1 1: state = 2 2: bb.guard_cooldown = 1.5 finished = true func stop(): assert(active) assert(root) active = false finished = false state = 0 # print("guard stopped") class rest extends base_bhv: var root func start(root: Node): self.root = root active = true func run(delta: float, bb: Dictionary): assert(active) assert(root) if finished: return if bb.stamina < 100.0: # print("rest: stamina: ", bb.stamina) bb.stamina += 15500.0 * delta else: finished = true func stop(): assert(active) assert(root) active = false finished = false class unconcious extends base_bhv: var root var anim1 var anim2 var state = 0 func start(root: Node, anim1: String, anim2: String): self.root = root self.anim1 = anim1 self.anim2 = anim2 active = true state = 0 func run(delta: float, bb: Dictionary): assert(active) assert(root) assert(anim1) assert(anim2) match state: 0: characters.animation_node_travel(root, anim1) state = 1 # if finished: # return # if bb.stamina < 100.0: # print("rest: stamina: ", bb.stamina) # bb.stamina += 15500.0 * delta # else: # finished = true func stop(): assert(active) assert(root) active = false finished = false #var walk_to: walkto #var flee_to: walkto #var melee_attack: attack #var take_melee_weapon: take_weapon #var guard_melee: guard #var do_rest: rest var conf_behaviors: = { "take_melee_weapon": take_weapon, "approach": walkto, "flee": walkto, "melee_attack": attack, "guard": guard, "rest": rest, "get_melee_damage": get_damage, "unconcious": unconcious } var pdst = INF class health_consideration extends AIUtilityScriptedConsideration: func evalute(tick, delta): if blackboard_get(tick, "health") <= 0.0: return 0.0 var ret = blackboard_get(tick, "health") / 100.0 return clamp(ret, 0.0, 1.0) class no_melee_weapon_equipped_consideration extends AIUtilityScriptedConsideration: func evalute(tick, delta): if !blackboard_get(tick, "melee_weapon_equipped"): return 1.0 return 0.0 class threat_consideration extends AIUtilityScriptedConsideration: const engage_distance = 20.0 func evalute(tick, delta): if blackboard_get(tick, "enemy_distance") < engage_distance * engage_distance: return 1.0 return 0.0 class enemy_far_consideration extends AIUtilityScriptedConsideration: func evalute(tick, delta): if blackboard_get(tick, "enemy_distance") < 1.6 * 1.6: return 0.0 return 1.0 class half_or_more_stamina_consideration extends AIUtilityScriptedConsideration: func evalute(tick, delta): if blackboard_get(tick, "stamina") > 50.0: return 1.0 return 0.0 class has_melee_weapon_equipped_consideration extends AIUtilityScriptedConsideration: func evalute(tick, delta): if blackboard_get(tick, "melee_weapon_equipped"): return 1.0 return 0.0 class melee_attack_ready_consideration extends AIUtilityScriptedConsideration: func evalute(tick, delta): if blackboard_get(tick, "attack_cooldown") <= 0.0: return 1.0 return 0.0 class twenty_or_more_stamina_consideration extends AIUtilityScriptedConsideration: func evalute(tick, delta): if blackboard_get(tick, "stamina") > 20.0: return 1.0 return 0.0 class melee_attack_distance_consideration extends AIUtilityScriptedConsideration: const attack_distance = 2.0 func evalute(tick, delta): if blackboard_get(tick, "enemy_distance") < attack_distance * attack_distance: return 1.0 return 0.0 class need_healing_consideration extends AIUtilityScriptedConsideration: func evalute(tick, delta): if blackboard_get(tick, "health") >= 100.0: return 0.0 var ret = 1.0 - blackboard_get(tick, "health") / 100.0 return clamp(ret, 0.0, 1.0) class need_rest_consideration extends AIUtilityScriptedConsideration: func evalute(tick, delta): if blackboard_get(tick, "stamina") >= 100.0: return 0.0 return clamp(1.0 - blackboard_get(tick, "stamina") / 100.0, 0.0, 1.0) class critical_health_consideration extends AIUtilityScriptedConsideration: func evalute(tick, delta): if blackboard_get(tick, "health") < 10.0: return 1.0 return 0.0 class critical_stamina_consideration extends AIUtilityScriptedConsideration: func evalute(tick, delta): if blackboard_get(tick, "stamina") < 10.0: return 1.0 return 0.0 class flee_or_more_distance_consideration extends AIUtilityScriptedConsideration: const flee_distance = 5.0 func evalute(tick, delta): if blackboard_get(tick, "enemy_distance") >= flee_distance * flee_distance: return 1.0 return 0.0 class dangerous_distance_consideration extends AIUtilityScriptedConsideration: const flee_distance = 5.0 func evalute(tick, delta): if blackboard_get(tick, "enemy_distance") < flee_distance * flee_distance: return 1.0 return 0.0 class ready_to_flee_consideration extends AIUtilityScriptedConsideration: func evalute(tick, delta): if blackboard_get(tick, "flee_cooldown") < 0.0: return 1.0 return 0.0 class has_energy_consideration extends AIUtilityScriptedConsideration: func evalute(tick, delta): if blackboard_get(tick, "stamina") >= 10.0: return 1.0 return 0.0 class ready_to_guard_consideration extends AIUtilityScriptedConsideration: func evalute(tick, delta): if blackboard_get(tick, "guard_cooldown") < 0.0: return 1.0 return 0.0 class guarding_consideration extends AIUtilityScriptedConsideration: func evalute(tick, delta): if blackboard_get(tick, "guard"): return 1.0 return 0.0 class melee_damage_consideration extends AIUtilityScriptedConsideration: func evalute(tick, delta): if blackboard_get(tick, "melee_damage"): return 1.0 return 0.0 var considerations_data = { "take_melee_weapon": { "considerations": [ {"c": health_consideration.new(), "score": 1.0}, {"c": no_melee_weapon_equipped_consideration.new(), "score": 1.0}, {"c": threat_consideration.new(), "score": 1.0} ], }, "approach": { "considerations": [ {"c": health_consideration.new(), "score": 1.0}, {"c": enemy_far_consideration.new(), "score": 1.0}, {"c": half_or_more_stamina_consideration.new(), "score": 1.0}, {"c": threat_consideration.new(), "score": 1.0} ], }, "melee_attack": { "considerations": [ {"c": health_consideration.new(), "score": 1.0}, {"c": has_melee_weapon_equipped_consideration.new(), "score": 1.0}, {"c": melee_attack_ready_consideration.new(), "score": 1.0}, {"c": twenty_or_more_stamina_consideration.new(), "score": 1.0}, {"c": melee_attack_distance_consideration.new(), "score": 1.0} ], }, "flee": { "considerations": [ {"c": health_consideration.new(), "score": 1.0}, {"c": ready_to_flee_consideration.new(), "score": 1.0}, {"c": has_energy_consideration.new(), "score": 1.0}, {"c": dangerous_distance_consideration.new(), "score": 1.0}, ], }, "guard": { "considerations": [ {"c": health_consideration.new(), "score": 1.0}, {"c": ready_to_guard_consideration.new(), "score": 1.0}, {"c": guarding_consideration.new(), "score": 1.0}, ], }, "unconcious": { "considerations": [ {"c": critical_health_consideration.new(), "score": 1.0} ], }, "rest": { "considerations": [ {"c": critical_stamina_consideration.new(), "score": 1.0}, {"c": flee_or_more_distance_consideration.new(), "score": 1.0} ], }, "get_melee_damage": { "considerations": [ {"c": health_consideration.new(), "score": 1.0}, {"c": melee_damage_consideration.new(), "score": 1.0}, ] } } func calc_utility(tick: AITick, utility: String, delta: float): var v = 0.0 if considerations_data.has(utility): v = 1.0 for b in considerations_data[utility].considerations: assert(b.c) v *= b.c._evalute(tick, delta) * b.score if v <= 0.0: break match utility: "take_melee_weapon": v *= 110.0 # if blackboard_get(tick, "health") <= 0.0: # return 0.0 # if blackboard_get(tick, "melee_weapon_equipped"): # return 0.0 # else: # if blackboard_get(tick, "enemy_distance") < engage_distance * engage_distance: # return 110.0 "approach": v *= 100.0 # if blackboard_get(tick, "health") <= 0.0: # return 0.0 # if blackboard_get(tick, "enemy_distance") < 1.6 * 1.6: # return 0.0 # if blackboard_get(tick, "stamina") > 50.0 && blackboard_get(tick, "enemy_distance") < engage_distance * engage_distance: # return 10.0 + (blackboard_get(tick, "enemy_distance") - 10.0) / 6.0 "melee_attack": v *= 50.0 # if blackboard_get(tick, "health") <= 0.0: # return 0.0 # if blackboard_get(tick, "melee_weapon_equipped"): # if blackboard_get(tick, "attack_cooldown") <= 0.0: # var d = attack_distance * attack_distance # if blackboard_get(tick, "stamina") > 20.0 && blackboard_get(tick, "enemy_distance") <= d: # return 50.0 "flee": v *= 300.0 # if blackboard_get(tick, "health") <= 0.0: # return 0.0 # if blackboard_get(tick, "flee_cooldown") > 0.0: # return 0.0 # if blackboard_get(tick, "stamina") <= 50.0 && blackboard_get(tick, "stamina") > 10.0 && blackboard_get(tick, "enemy_distance") < flee_distance * flee_distance: # return 100.0 + clamp((100.0 - blackboard_get(tick, "stamina")), 0, 90.0) * 2.0 # if blackboard_get(tick, "stamina") <= 10.0 && blackboard_get(tick, "enemy_distance") <= flee_distance * flee_distance * 0.5: # return 250.0 # if blackboard_get(tick, "stamina") <= 50.0 && blackboard_get(tick, "enemy_distance") < engage_distance * engage_distance: # if blackboard_get(tick, "randf") < 0.3: # return 100.0 # if blackboard_get(tick, "enemy_distance") < 1.4 * 1.4: # return 150.0 # if blackboard_get(tick, "health") < 50 && blackboard_get(tick, "health") > 15: # return 160 "guard": if blackboard_get(tick, "guard"): blackboard_set(tick, "guard", false) v *= 900.0 # if blackboard_get(tick, "health") <= 0.0: # return 0.0 # if blackboard_get(tick, "guard_cooldown") > 0.0: # return 0.0 # elif blackboard_get(tick, "guard"): # blackboard_set(tick, "guard", false) # return 400.0 # elif blackboard_get(tick, "enemy_distance") < guard_distance * guard_distance * 0.3 && blackboard_get(tick, "stamina") > 10.0: # return 80 # elif blackboard_get(tick, "enemy_distance") < attack_distance * attack_distance * 0.3: # return 10.0 "unconcious": v *= 2000.0 # if blackboard_get(tick, "health") < 10: # return 2000 "rest": v *= 150.0 # if blackboard_get(tick, "stamina") >= 100.0: # return 0.0 # if blackboard_get(tick, "stamina") <= 10.0 && blackboard_get(tick, "enemy_distance") >= flee_distance * flee_distance: # return 100.0 + blackboard_get(tick, "enemy_distance") / 10.0 "get_melee_damage": v *= 3000.0 if blackboard_get(tick, "melee_damage"): blackboard_set(tick, "melee_damage", false) # if blackboard_get(tick, "health") <= 0.0: # return 0.0 # if blackboard_get(tick, "melee_damage"): # blackboard_set(tick, "melee_damage", false) # print("DAMAGE2 ", blackboard_get(tick, "health")) # return 1000 _: assert(false) return v var last_behavior func select_behavior(tick, delta): var best_behavior var best_utility best_behavior = "rest" best_utility = 0.0 for e in conf_behaviors.keys(): var utility = calc_utility(tick, e, delta) if e == last_behavior: utility *= 2.0 if best_utility < utility: best_utility = utility best_behavior = e last_behavior = best_behavior return best_behavior var last_bhv var last_running = [] var current_running = [] var run_behaviors = {} func update_physics(tick, delta): assert(get_memory(tick).initialized) var root = get_character(tick) assert(root) var cam = root.get_viewport().get_camera() if !cam: return if !cam.has_meta("player"): return blackboard_set(tick, "self", get_character(tick)) var space: PhysicsDirectSpaceState = root.get_world().get_direct_space_state() var o = root.global_transform.origin var p = cam.get_meta("player").global_transform pdst = o.distance_squared_to(p.origin) blackboard_set(tick, "enemy_distance", pdst) blackboard_set(tick, "randf", rnd.randf()) blackboard_set(tick, "enemy_pos", p.origin) blackboard_set(tick, "space", space) # var adest = o + (p.origin - o).normalized() * min(2.0, sqrt(pdst)) var adest = p.origin var away = (o - p.origin).normalized() var attack_xform = Transform(root.global_transform.basis, o + away * 1.5) var fdest = o + away * 4.0 var enemy_dir: Vector3 = Transform(p.basis, Vector3()).xform(Vector3(0, 0, -1)) var root_dir: Vector3 = Transform(root.global_transform.basis, Vector3()).xform(Vector3(0, 0, -1)) var dot = root_dir.dot(enemy_dir) blackboard_set(tick, "dot", dot) var prev_state = state match state: 0: last_running = current_running.duplicate() current_running = [] var bhv = select_behavior(tick, delta) if last_bhv != bhv: # print("behavior: ", bhv) last_bhv = bhv if !run_behaviors[bhv].active: match bhv: "take_melee_weapon": run_behaviors[bhv].start(root, "s_dagger") "approach": var dest = adest if pdst > 25.0: run_behaviors[bhv].start(root, dest, 0.5, 3.0, 4.0, 6.0) else: run_behaviors[bhv].start(root, dest, 0.2, 1.5, 2.0, 6.0) "melee_attack": run_behaviors[bhv].start(root, "attack-melee1", "about_to_attack") "flee": # walk away from enemy (player) var dest = fdest run_behaviors[bhv].start(root, dest, 0.3, 1.5, 20.0, 6.0) "rest": run_behaviors[bhv].start(root) "guard": run_behaviors[bhv].start(root, "guard-melee1", "guard-front-melee1") "get_melee_damage": run_behaviors[bhv].start(root, "guard-melee1", "guard-front-melee1", 10.0) "unconcious": run_behaviors[bhv].start(root, "fall-front", "fall-back") _: assert(false) # print("adding behavior: ", bhv) current_running.push_back(run_behaviors[bhv]) for e in current_running: if !e in last_running: if !e.active: e.activate() e.run(delta, blackboard_get_dict(tick)) #FIXME: mess :( if e == run_behaviors["flee"]: blackboard_set(tick, "flee_cooldown", 2.0 + rnd.randf() * 3.0) if e.finished && e.active: # print("in current run but finished") e.stop() for e in last_running: if !e in current_running: if e.active: # print("not in current run but was in last") e.stop() 1: state = 2 2: state = 3 3: state = 0 if blackboard_get(tick, "stamina") < 100.0: blackboard_set(tick, "stamina", blackboard_get(tick, "stamina") + delta) if blackboard_get(tick, "attack_cooldown") > 0.0: blackboard_set(tick, "attack_cooldown", blackboard_get(tick, "attack_cooldown") - delta) if blackboard_get(tick, "guard_cooldown") > 0.0: blackboard_set(tick, "guard_cooldown", blackboard_get(tick, "guard_cooldown") - delta) if blackboard_get(tick, "flee_cooldown") > 0.0: blackboard_set(tick, "flee_cooldown", blackboard_get(tick, "flee_cooldown") - delta) blackboard_set(tick, "stamina", clamp(blackboard_get(tick, "stamina"), 0, 100.0)) blackboard_set(tick, "health", clamp(blackboard_get(tick, "health"), 0, 100.0)) return ERR_BUSY func update(tick, delta): return ERR_BUSY ## # if prev_state != state: # print("new state: ", state) # print("running ", current_running.size(), " behaviors")