Examples: Boss Patterns
Implementations of different boss types using the Ability System.
Boss 1: Simple Melee Boss
extends CharacterBody3D
class_name SimpleMeleeBoss
@onready var asc = $AbilityComponent
@onready var anim = $AnimationPlayer
var health = 200.0
var target: Player = null
var attack_timer = 0.0
var phase = 1
signal defeated
signal health_changed(new_health)
func _ready():
asc.attribute_set.set_attribute_base_value(&"health", health)
func _physics_process(delta):
if health <= 0:
return
attack_timer += delta
## Phase transition
if health < 100 and phase == 1:
enter_phase_2()
## Attack pattern
if attack_timer >= 1.5:
attack_pattern()
attack_timer = 0.0
## Move to target
if target:
var distance = global_position.distance_to(target.global_position)
if distance > 2.0:
var dir = (target.global_position - global_position).normalized()
velocity = dir * 4.0
move_and_collide(velocity * delta)
func attack_pattern():
if phase == 1:
## Simple: alternating slash and heavy
if randf() < 0.5:
asc.try_activate_ability_by_tag(&"boss.slash")
else:
asc.try_activate_ability_by_tag(&"boss.heavy_attack")
else:
## Phase 2: more aggressive
var attacks = [&"boss.slash", &"boss.heavy_attack", &"boss.roar"]
var choice = attacks[randi() % attacks.size()]
asc.try_activate_ability_by_tag(choice)
## Apply damage to player
if choice in [&"boss.slash", &"boss.heavy_attack"]:
target.asc.apply_effect_by_tag(&"effect.boss_damage", asc)
func enter_phase_2():
phase = 2
anim.play("phase_2_transition")
asc.add_tag(&"boss.phase_2")
print("Boss entered phase 2!")
func take_damage(amount: float):
health -= amount
health_changed.emit(health)
anim.play("hit")
if health <= 0:
die()
func die():
anim.play("death")
await anim.animation_finished
defeated.emit()
queue_free()Boss 2: Caster Boss (AoE + Phases)
extends Node3D
class_name CasterBoss
@onready var asc = $AbilityComponent
@onready var anim = $AnimationPlayer
var health = 150.0
var target: Player = null
var spell_timer = 0.0
var phase = 1 ## 1=Normal, 2=Enraged
var minion_count = 0
var max_minions = 3
signal defeated
signal health_changed(new_health)
func _ready():
asc.attribute_set.set_attribute_base_value(&"health", health)
asc.ability_activated.connect(_on_ability_cast)
func _physics_process(delta):
if health <= 0:
return
spell_timer += delta
## Phase check
if health < 75 and phase == 1:
enter_phase_2()
## Spell rotation
if spell_timer >= 2.0:
cast_spell()
spell_timer = 0.0
func cast_spell():
var spell = null
match phase:
1:
## Normal: fireball or heal
if health < 50:
spell = &"boss.heal"
elif minion_count < max_minions:
spell = &"boss.summon_minion"
else:
spell = &"boss.fireball"
2:
## Enraged: cycling through spells
var spells = [&"boss.fireball", &"boss.ice_storm", &"boss.summon_minion"]
spell = spells[randi() % spells.size()]
if spell:
asc.try_activate_ability_by_tag(spell)
if spell == &"boss.fireball":
## Create projectile
var proj = preload("res://vfx/fireball_projectile.tscn").instantiate()
get_parent().add_child(proj)
proj.global_position = global_position
proj.target = target
proj.damage = 25.0
elif spell == &"boss.heal":
health = min(health + 30, 150)
health_changed.emit(health)
elif spell == &"boss.summon_minion":
summon_minion()
func summon_minion():
if minion_count >= max_minions:
return
var minion = preload("res://enemies/boss_minion.tscn").instantiate()
get_parent().add_child(minion)
minion.global_position = global_position + Vector3(randf_range(-3, 3), 0, randf_range(-3, 3))
minion.target = target
minion_count += 1
## Track minion death
minion.defeated.connect(func():
minion_count -= 1
)
func enter_phase_2():
phase = 2
anim.play("enrage")
asc.add_tag(&"boss.enraged")
print("Boss enraged!")
func take_damage(amount: float):
health -= amount
health_changed.emit(health)
anim.play("hit")
if health <= 0:
die()
func die():
## Kill remaining minions
for child in get_parent().get_children():
if child is BossMinion:
child.queue_free()
anim.play("death")
await anim.animation_finished
defeated.emit()
queue_free()
func _on_ability_cast(spec):
## Sync animation
var ability = spec.get_ability()
if ability.ability_tag == &"boss.fireball":
anim.play("cast_fireball")
elif ability.ability_tag == &"boss.heal":
anim.play("cast_heal")Boss 3: Reactive Boss (Adapts to Player)
extends CharacterBody3D
class_name ReactiveBoss
@onready var asc = $AbilityComponent
@onready var anim = $AnimationPlayer
var health = 250.0
var target: Player = null
var adaptation_timer = 0.0
var player_damage_taken = 0.0
var weakness: StringName = &"" ## Adapt to player's strategy
signal defeated
signal health_changed(new_health)
func _ready():
asc.attribute_set.set_attribute_base_value(&"health", health)
func _physics_process(delta):
if health <= 0:
return
adaptation_timer += delta
## Analyze player every 5 seconds
if adaptation_timer >= 5.0:
analyze_player()
adaptation_timer = 0.0
## Attack based on weakness
attack_intelligently()
func analyze_player():
## Check what player is doing
var player_tags = target.asc.get_all_tags()
## If player keeps using melee, cast ranged
if target.asc.has_tag(&"state.attacking"):
weakness = &"melee"
## If player keeps using magic, build resistance
elif ASTagUtils.event_did_occur(&"event.magic_damage", target.asc, 5.0):
weakness = &"magic"
## If player heals a lot, dispel
elif ASTagUtils.event_did_occur(&"event.healed", target.asc, 5.0):
weakness = &"healer"
print("Boss adapted to weakness: ", weakness)
func attack_intelligently():
match weakness:
&"melee":
## Cast ranged spell
asc.try_activate_ability_by_tag(&"boss.fireball")
&"magic":
## Physical attack + magic resistance buff
asc.try_activate_ability_by_tag(&"boss.heavy_attack")
if not asc.has_tag(&"state.magic_resistance"):
asc.add_tag(&"state.magic_resistance")
&"healer":
## Dispel healing + silence
asc.try_activate_ability_by_tag(&"boss.dispel")
asc.try_activate_ability_by_tag(&"boss.silence")
_:
## Default: rotate attacks
var attacks = [&"boss.slash", &"boss.heavy_attack", &"boss.fireball"]
asc.try_activate_ability_by_tag(attacks[randi() % attacks.size()])
func take_damage(amount: float):
health -= amount
player_damage_taken += amount
health_changed.emit(health)
if health <= 0:
die()
func die():
anim.play("death")
await anim.animation_finished
defeated.emit()
queue_free()Boss 4: Multi-Phase Mega Boss
extends CharacterBody3D
class_name MegaBoss
@onready var asc = $AbilityComponent
@onready var anim = $AnimationPlayer
var health = 500.0
var target: Player = null
var phase = 1 ## 1, 2, 3
signal defeated
signal health_changed(new_health)
signal phase_changed(new_phase)
func _ready():
asc.attribute_set.set_attribute_base_value(&"health", health)
func _physics_process(delta):
if health <= 0:
return
## Continuous phase checks
if health < 375 and phase == 1:
change_phase(2)
elif health < 250 and phase == 2:
change_phase(3)
## Phase-specific behavior
match phase:
1:
phase_1_behavior()
2:
phase_2_behavior()
3:
phase_3_behavior()
func phase_1_behavior():
## Moderate difficulty
var attacks = [&"boss.slash", &"boss.fireball"]
asc.try_activate_ability_by_tag(attacks[randi() % attacks.size()])
func phase_2_behavior():
## Hard difficulty - summon adds
var attacks = [&"boss.heavy_attack", &"boss.fireball", &"boss.summon_minion"]
asc.try_activate_ability_by_tag(attacks[randi() % attacks.size()])
## Apply vulnerability debuff to player
target.asc.apply_effect_by_tag(&"effect.vulnerability", asc)
func phase_3_behavior():
## Extreme - ultimate ability + full power
if health > 100:
var attacks = [&"boss.ultimate_attack", &"boss.meteor_storm", &"boss.summon_minion"]
asc.try_activate_ability_by_tag(attacks[randi() % attacks.size()])
else:
## Final stand - slow but powerful
asc.try_activate_ability_by_tag(&"boss.last_stand")
func change_phase(new_phase: int):
phase = new_phase
phase_changed.emit(new_phase)
anim.play("phase_%d_transition" % new_phase)
asc.add_tag(&"boss.phase_%d" % new_phase)
match new_phase:
2:
print("Boss: Phase 2 - Geting serious!")
health += 50 ## Heal on phase change
3:
print("Boss: Phase 3 - ULTIMATE POWER!")
asc.add_tag(&"state.invulnerable")
await get_tree().create_timer(1.0).timeout
asc.remove_tag(&"state.invulnerable")
health_changed.emit(health)
func take_damage(amount: float):
if asc.has_tag(&"state.invulnerable"):
return
health -= amount
health_changed.emit(health)
anim.play("hit")
if health <= 0:
die()
func die():
## Epic death sequence
anim.play("death")
asc.dispatch_event(&"event.boss_defeated")
## Drop loot
var loot = [&"item.legendary_sword", &"item.boss_ring", &"item.gold_chest"]
for item_tag in loot:
spawn_loot(item_tag)
await anim.animation_finished
defeated.emit()
queue_free()
func spawn_loot(item_tag: StringName):
var loot = preload("res://items/loot.tscn").instantiate()
get_parent().add_child(loot)
loot.global_position = global_position + Vector3(randf_range(-2, 2), 1, randf_range(-2, 2))
loot.item_tag = item_tagBoss 5: Puzzle Boss (Mechanics-Heavy)
extends Node3D
class_name PuzzleBoss
@onready var asc = $AbilityComponent
@onready var anim = $AnimationPlayer
var health = 100.0
var target: Player = null
var puzzle_phase = 0 ## 0=waiting, 1=vulnerable, 2=shield
var shield_active = false
var shield_durability = 30.0
signal defeated
signal health_changed(new_health)
func _ready():
asc.attribute_set.set_attribute_base_value(&"health", health)
func _physics_process(delta):
if health <= 0:
return
## Puzzle mechanics
match puzzle_phase:
0:
waiting_phase()
1:
vulnerable_phase()
2:
shield_phase()
func waiting_phase():
## Boss is dormant, player must trigger
print("Boss waiting for input...")
func vulnerable_phase():
## Boss is vulnerable - 30 seconds window
if not asc.has_tag(&"boss.vulnerable"):
asc.add_tag(&"boss.vulnerable")
anim.play("vulnerable")
## Player should attack now
target.asc.effect_applied.connect(_on_player_damage)
await get_tree().create_timer(30.0).timeout
if puzzle_phase == 1:
shield_phase() ## Close window
func shield_phase():
if shield_active:
return
shield_active = true
asc.add_tag(&"boss.shielded")
puzzle_phase = 2
## Boss creates shield that must be broken
print("Boss shield activated! Durability: ", shield_durability)
## Wait for shield to break
while shield_durability > 0:
await get_tree().create_timer(0.5).timeout
## Shield broken
shield_active = false
asc.remove_tag(&"boss.shielded")
puzzle_phase = 1
vulnerable_phase()
func trigger_boss():
if puzzle_phase != 0:
return
print("Boss awakens!")
anim.play("awaken")
vulnerable_phase()
func break_shield(amount: float):
if not shield_active:
return
shield_durability -= amount
print("Shield durability: ", shield_durability)
if shield_durability <= 0:
shield_durability = 30.0
func take_damage(amount: float):
if shield_active:
break_shield(amount)
return
health -= amount
health_changed.emit(health)
if health <= 0:
die()
func die():
anim.play("death")
await anim.animation_finished
defeated.emit()
queue_free()
func _on_player_damage(effect_spec):
if puzzle_phase == 1:
## Player is damaging in vulnerable window
var damage = -effect_spec.get_magnitude(&"health")
take_damage(damage)Scene Integration
## In BossScene.gd
extends Node3D
@onready var boss = $MegaBoss
@onready var player = $Player
@onready var ui = $UI
func _ready():
boss.target = player
player.target = boss
boss.health_changed.connect(_on_boss_health_changed)
boss.phase_changed.connect(_on_boss_phase_changed)
boss.defeated.connect(_on_boss_defeated)
func _on_boss_health_changed(health):
ui.update_boss_health(health)
func _on_boss_phase_changed(phase):
ui.show_message("Phase %d!" % phase)
func _on_boss_defeated():
ui.show_message("Victory! Boss defeated!")
await get_tree().create_timer(3.0).timeout
get_tree().reload_current_scene()Each pattern can be customized and combined to create epic encounters!