feat: add James Boss

-  Add standard boss class
 - Improve projectile
 - Update layer and mask collision
This commit is contained in:
Varylios 2025-09-14 01:31:18 +02:00
parent 0e3a7697ba
commit d352574618
21 changed files with 197 additions and 94 deletions

View file

@ -0,0 +1,50 @@
[gd_scene load_steps=9 format=3 uid="uid://dyl4wiga2goi"]
[ext_resource type="PackedScene" uid="uid://dg8bjkjuddnhg" path="res://Enemies/enemy.tscn" id="1_ohyx6"]
[ext_resource type="Script" uid="uid://dw53e0rr7qc02" path="res://Enemies/boss.gd" id="2_vnmiq"]
[ext_resource type="Texture2D" uid="uid://d38rvmky1mr5f" path="res://Assets/Characters/72 Character Free/Char 5/Character 5.png" id="3_831fr"]
[ext_resource type="Script" uid="uid://ddgbr0n8kic3y" path="res://Towers/Projectiles/ProjectileResource.gd" id="3_vnmiq"]
[ext_resource type="Texture2D" uid="uid://dqyhhvxpwtpsy" path="res://Assets/Emotes/emote_star.png" id="4_831fr"]
[sub_resource type="Resource" id="Resource_831fr"]
script = ExtResource("3_vnmiq")
speed = 2
damage = 3
maxTargets = 1
type = 4
allowedTargets = 2
sprite = ExtResource("4_831fr")
metadata/_custom_type_script = "uid://ddgbr0n8kic3y"
[sub_resource type="ViewportTexture" id="ViewportTexture_mw235"]
viewport_path = NodePath("Enemy/HealthBar3D/SubViewport")
[sub_resource type="ConvexPolygonShape3D" id="ConvexPolygonShape3D_tviqb"]
points = PackedVector3Array(0, 0, 0, -2, 1, 3, 2, 1, 3, 2, 0, 3, -2, 0, 3)
[node name="PathFollow3D" instance=ExtResource("1_ohyx6")]
[node name="Enemy" parent="." index="0"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.32, 0)
script = ExtResource("2_vnmiq")
projectile = SubResource("Resource_831fr")
speed = 0.7
base_attack_damage = 10
max_life = 20
money = 120
[node name="Sprite3D" parent="Enemy" index="1"]
transform = Transform3D(2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0)
texture = ExtResource("3_831fr")
[node name="HealthBar3D" parent="Enemy" index="2"]
transform = Transform3D(0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 0, 0.34, 0)
texture = SubResource("ViewportTexture_mw235")
[node name="TowerAttackRange" type="Area3D" parent="Enemy" index="4"]
collision_layer = 4
collision_mask = 4
[node name="CollisionShape3D" type="CollisionShape3D" parent="Enemy/TowerAttackRange" index="0"]
shape = SubResource("ConvexPolygonShape3D_tviqb")
debug_color = Color(0.419608, 0, 0.584314, 0.419608)

25
Enemies/boss.gd Normal file
View file

@ -0,0 +1,25 @@
extends Enemy
class_name Boss
@export var projectile : ProjectileResource
func _ready() -> void:
super._ready()
$TowerAttackRange.body_entered.connect(onAttackRangeBodyEntered)
$TowerAttackRange.body_exited.connect(onAttackRangeBodyExited)
func attack() -> bool:
# already attacked TheCube
if super.attack():
return true
var towerTargets : Array[Node3D] = targets.filter(func(b): return b is Tower)
if $AttackCooldown.is_stopped() && not towerTargets.is_empty():
projectile.shoot(towerTargets[0], global_position)
targets.erase(towerTargets[0])
$AttackCooldown.start()
return false

1
Enemies/boss.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://dw53e0rr7qc02

View file

@ -1,8 +1,10 @@
extends CharacterBody3D
class_name Enemy
signal died
@onready var death_vfx_packed : PackedScene = preload("res://VFX/death_particles.tscn")
@onready var sad_vfx_packed : PackedScene = preload("res://VFX/sad_particles.tscn")
@ -20,37 +22,41 @@ var health : int = 0 :
@onready var path : PathFollow3D = get_parent()
@onready var healthBar : ProgressBar = $HealthBar3D/SubViewport/HealthBar2D
var attack_target : Node3D
var targets : Array[Node3D]
var is_alive : bool = true
func _ready() -> void:
healthBar.value = health
healthBar.max_value = max_life
$CubeAttackRange.body_entered.connect(onAttackRangeBodyEntered)
$CubeAttackRange.body_exited.connect(onAttackRangeBodyExited)
func _physics_process(delta: float) -> void:
if !is_alive:
return
if attack_target:
attack()
else:
if not attack():
path.progress += speed * delta
func take_damage(damage : int) -> void:
func take_damage(damage: int) -> void:
health += damage
healthBar.visible = true
healthBar.value = health
func attack():
if $AttackCooldown.is_stopped():
func attack() -> bool:
if targets.is_empty():
return false
var cubeTarget : Array[Node3D] = targets.filter(func(b): return b is TheCube)
if $AttackCooldown.is_stopped() && not cubeTarget.is_empty():
cubeTarget[0].take_damage(base_attack_damage)
$AttackCooldown.start()
attack_target.take_damage(base_attack_damage)
attack_target = null
path.progress = 0
return true
return false
func death() -> void:
@ -61,15 +67,13 @@ func death() -> void:
var death_vfx : CPUParticles3D = death_vfx_packed.instantiate()
get_tree().current_scene.add_child(death_vfx)
death_vfx.global_position = global_position
path.queue_free()
func _on_attack_range_body_entered(body: Node3D) -> void:
if body is TheCube:
attack_target = body
func onAttackRangeBodyEntered(body: Node3D) -> void:
if (body is TheCube || body is Tower) && not targets.has(body):
targets.append(body)
func _on_attack_range_body_exited(body: Node3D) -> void:
if body is TheCube:
attack_target = null
func onAttackRangeBodyExited(body: Node3D) -> void:
targets.erase(body)

View file

@ -22,14 +22,14 @@ color = 0
metadata/_custom_type_script = "uid://blnmjxmusrsa7"
[sub_resource type="SphereShape3D" id="SphereShape3D_cmo2f"]
radius = 1.0
radius = 0.6
[node name="PathFollow3D" type="PathFollow3D"]
[node name="Enemy" type="CharacterBody3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.25, 0)
collision_layer = 4
collision_mask = 4
collision_layer = 2
collision_mask = 2
script = ExtResource("1_orwns")
[node name="CollisionShape3D" type="CollisionShape3D" parent="Enemy"]
@ -68,16 +68,14 @@ theme_override_styles/fill = SubResource("StyleBoxFlat_d6lpy")
show_percentage = false
metadata/_custom_type_script = "uid://blnmjxmusrsa7"
[node name="AttackRange" type="Area3D" parent="Enemy"]
collision_layer = 8
collision_mask = 8
[node name="CubeAttackRange" type="Area3D" parent="Enemy"]
collision_layer = 4
collision_mask = 4
[node name="CollisionShape3D" type="CollisionShape3D" parent="Enemy/AttackRange"]
[node name="CollisionShape3D" type="CollisionShape3D" parent="Enemy/CubeAttackRange"]
shape = SubResource("SphereShape3D_cmo2f")
debug_color = Color(0.994297, 0, 0.224345, 0.42)
[node name="AttackCooldown" type="Timer" parent="Enemy"]
wait_time = 4.0
one_shot = true
[connection signal="body_entered" from="Enemy/AttackRange" to="Enemy" method="_on_attack_range_body_entered"]
[connection signal="body_exited" from="Enemy/AttackRange" to="Enemy" method="_on_attack_range_body_exited"]

View file

@ -81,17 +81,21 @@ static func showConfirmPopup(
confirmPopup.confirmed.connect(confirmCallback)
confirmPopup.canceled.connect(cancelCallback)
static func getTopOfHitBox(body : CollisionObject3D) -> float:
enum POSITION { TOP, CENTER, DOWN }
static func getHitBoxLocation(body : CollisionObject3D, position : POSITION) -> float:
if body is GameTile:
return .2
if body.has_node("CollisionShape3D"):
var shape : Shape3D = body.shape_owner_get_shape(0, 0)
var transform : Transform3D = body.shape_owner_get_transform(0)
if shape is CapsuleShape3D:
return shape.height + shape.radius + transform.origin.y
if shape is SphereShape3D:
return shape.radius + transform.origin.y
if position == POSITION.CENTER:
return transform.origin.y
else:
var factor : int = 1 if POSITION.TOP == position else -1
if shape is CapsuleShape3D:
return factor * (shape.height + shape.radius) + transform.origin.y
if shape is SphereShape3D:
return factor * shape.radius + transform.origin.y
return 0

View file

@ -1,7 +1,7 @@
extends Resource
class_name Troop
@export_file("*.tscn") var enemy : String = "res://enemies/"
@export_file("*.tscn") var enemy : String = "res://enemies/Scenes"
@export var number_to_spawn : int = 1
@export var lane_to_spawn : int = 0
@export var spawn_delay : float = 1

View file

@ -7,8 +7,8 @@
size = Vector3(1.2, 1, 1.2)
[node name="TheCube" type="StaticBody3D"]
collision_layer = 8
collision_mask = 8
collision_layer = 4
collision_mask = 4
script = ExtResource("1_wrmut")
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]

View file

@ -18,6 +18,9 @@ enum TYPE { ## Types of projectiles
## Bouncing over enemies[br]work with [member ProjectileResource.maxTarets] and [member ProjectileResource.damageArea]
BOUNCING,
## Disable ally tower for [param damage] duration [br]Usable on [Boss] projectiles
DISABLING,
}
var type : TYPE = TYPE.BASIC
@ -27,63 +30,86 @@ var target : PhysicsBody3D
var vectorTarget : Vector3
var maxTargets : int = 1
var damage : int = 1
var enemiesInRange : Array[Enemy]
var bodiesInRange : Array[Enemy]
var affectedTarget : Array[Enemy]
var allyInRange : Array[Tower]
func _physics_process(_delta: float) -> void:
if !is_instance_valid(target) && type != TYPE.PIERCING || vectorTarget.distance_squared_to(global_position) < .4:
if shouldQueueFree():
queue_free()
return
var globalPos : Vector3 = vectorTarget if vectorTarget else target.global_position
if target is Tower:
globalPos.y += Helper.getHitBoxLocation(target, Helper.POSITION.CENTER)
velocity = global_position.direction_to(globalPos) * speed
look_at(globalPos)
move_and_slide()
func shouldQueueFree() -> bool:
match type:
TYPE.PIERCING: return vectorTarget.distance_squared_to(global_position) < .4
_ when target is Tower: return not target.visible
_ when !is_instance_valid(target): return true
return false
func onBodyEnteredDamageArea(body: Node3D) -> void:
if type != TYPE.BASIC && targetable(body):
if type == TYPE.PIERCING:
resolveDamages(body)
else:
addTarget(body)
resolveContact(body)
elif not bodiesInRange.has(body):
bodiesInRange.append(body)
func onBodyCollideWithProjectile(body: Node3D) -> void:
if (body == target || type == TYPE.PIERCING && targetable(body)):
resolveDamages(body)
resolveContact(body)
func targetable(body: Node3D) -> bool:
if body is Enemy:
return TARGET_ENEMY & allowedTargets && not affectedTarget.has(body)
if body is Tower:
if body is Tower || body is TheCube:
return TARGET_ALLY & allowedTargets
return false
func resolveDamages(body: Node3D) -> void:
damageEnemy(body)
func resolveContact(body: Node3D) -> void:
if body is Enemy:
resolveEnemyDamages(body)
if body is Tower || body is TheCube:
resolveAllyEffects(body)
func resolveEnemyDamages(enemy: Enemy) -> void:
damageEnemy(enemy)
if type == TYPE.AOE:
for enemy in enemiesInRange:
if is_instance_valid(enemy):
damageEnemy(enemy)
for body in bodiesInRange:
if is_instance_valid(body):
damageEnemy(body)
if maxTargets < 1 || type == TYPE.AOE:
return queue_free()
if type == TYPE.BOUNCING:
enemiesInRange.erase(body)
if enemiesInRange.size():
target = enemiesInRange.pop_front()
bodiesInRange.erase(enemy)
if bodiesInRange.size():
target = bodiesInRange.pop_front()
else:
queue_free()
func resolveAllyEffects(ally: Node3D) -> void:
if ally is Tower && type == TYPE.DISABLING:
ally.disable(damage)
queue_free()
func damageEnemy(enemy: Enemy) -> void:
if not affectedTarget.has(enemy):
maxTargets -= 1
@ -91,7 +117,6 @@ func damageEnemy(enemy: Enemy) -> void:
affectedTarget.append(enemy)
func loadProjectile(resource: ProjectileResource, _target: PhysicsBody3D) -> void:
target = _target
type = resource.type
@ -104,19 +129,9 @@ func loadProjectile(resource: ProjectileResource, _target: PhysicsBody3D) -> voi
damage = resource.damage
allowedTargets = resource.allowedTargets
$Sprite3D.texture = resource.sprite
if type != TYPE.BASIC && resource.damageArea:
if [TYPE.AOE, TYPE.PIERCING, TYPE.BOUNCING].has(type) && resource.damageArea:
$DamageArea/ProjectileArea.shape = resource.damageArea
func addTarget(body: Node3D) -> void:
if body is Enemy && not enemiesInRange.has(body):
enemiesInRange.append(body)
if body is Tower && not allyInRange.has(body):
allyInRange.append(body)
func removeTarget(body: Node3D) -> void:
if body is Enemy:
enemiesInRange.erase(body)
if body is Tower:
allyInRange.erase(body)
bodiesInRange.erase(body)

View file

@ -1,6 +1,7 @@
extends Resource
class_name ProjectileResource
const projectileScene : PackedScene = preload("res://Towers/Projectiles/projectile.tscn")
@export var speed : int = 20
@export var damage : int = 1
@ -11,3 +12,8 @@ class_name ProjectileResource
## When [enum Projectile.TYPE] is [param BOUNCING] the shape is for target new entity in range
@export var damageArea : Shape3D
@export var sprite : Texture2D = load("res://Assets/Emotes/emote_star.png")
func shoot(target: Node3D, gobalPos: Vector3) -> void:
var projectile : Projectile = projectileScene.instantiate()
projectile.loadProjectile(self, target)
EventBus.projectile_shooted.emit(projectile, gobalPos)

View file

@ -21,16 +21,16 @@ billboard = 2
texture = ExtResource("2_08w86")
[node name="HitBox" type="Area3D" parent="."]
collision_layer = 4
collision_mask = 4
collision_layer = 6
collision_mask = 6
[node name="ProjectileSize" type="CollisionShape3D" parent="HitBox"]
shape = SubResource("SphereShape3D_dsts2")
debug_color = Color(0.926858, 0.237749, 0.335021, 0.42)
[node name="DamageArea" type="Area3D" parent="."]
collision_layer = 4
collision_mask = 4
collision_layer = 6
collision_mask = 6
[node name="ProjectileArea" type="CollisionShape3D" parent="DamageArea"]
debug_color = Color(0.926858, 0.237749, 0.335021, 0.42)

View file

@ -32,7 +32,7 @@ type = 2
icone = ExtResource("3_6h033")
bio = ""
price = 150
projectileRessource = SubResource("Resource_lhd8w")
projectile = SubResource("Resource_lhd8w")
towerRange = SubResource("SphereShape3D_6bcjo")
action_cooldown = 2.5
max_energy = 100.0

View file

@ -22,6 +22,6 @@ tower_name = "Evan"
type = 5
icone = ExtResource("2_5uh04")
bio = ""
projectileRessource = SubResource("Resource_mf185")
projectile = SubResource("Resource_mf185")
towerRange = SubResource("SphereShape3D_y05yr")
tower_shop = Array[ExtResource("4_v32j5")]([])

View file

@ -29,7 +29,7 @@ type = 3
icone = ExtResource("2_sciv6")
bio = ""
price = 200
projectileRessource = SubResource("Resource_sciv6")
projectile = SubResource("Resource_sciv6")
towerRange = SubResource("SphereShape3D_pw4mj")
action_cooldown = 3.0
max_energy = 50.0

View file

@ -86,7 +86,7 @@ tower_name = "Pierre"
type = 1
icone = ExtResource("2_lcjqw")
price = 100
projectileRessource = SubResource("Resource_r52mr")
projectile = SubResource("Resource_r52mr")
towerRange = SubResource("SphereShape3D_c55ds")
action_cooldown = 1.0
max_energy = 25.0

View file

@ -3,7 +3,6 @@
[ext_resource type="PackedScene" uid="uid://trg7ag3dqr2l" path="res://Towers/tower.tscn" id="1_ki73m"]
[ext_resource type="Texture2D" uid="uid://m6jwakrus50v" path="res://Assets/Icones/sea-star.svg" id="2_n34tq"]
[ext_resource type="Script" uid="uid://ddgbr0n8kic3y" path="res://Towers/Projectiles/ProjectileResource.gd" id="3_sf52i"]
[ext_resource type="Script" uid="uid://bg0x4egeu17qb" path="res://Upgrades/TowerUpgrade.gd" id="4_uso5g"]
[ext_resource type="Texture2D" uid="uid://315k07rsgf6t" path="res://Assets/Characters/Female1.png" id="5_2rqsg"]
[sub_resource type="SphereShape3D" id="SphereShape3D_rbuca"]
@ -22,17 +21,22 @@ metadata/_custom_type_script = "uid://ddgbr0n8kic3y"
[sub_resource type="SphereShape3D" id="SphereShape3D_sf52i"]
radius = 4.0
[sub_resource type="ViewportTexture" id="ViewportTexture_n34tq"]
viewport_path = NodePath("EnergyBar3D/SubViewport")
[node name="Victoria" instance=ExtResource("1_ki73m")]
tower_name = "Victoria"
type = 4
icone = ExtResource("2_n34tq")
bio = ""
projectileRessource = SubResource("Resource_c1yif")
projectile = SubResource("Resource_c1yif")
towerRange = SubResource("SphereShape3D_sf52i")
energy_regen = 25.0
energy_cost = 20.0
tower_shop = Array[ExtResource("4_uso5g")]([])
[node name="Sprite3D" parent="." index="5"]
texture = ExtResource("5_2rqsg")
frame = 59
[node name="EnergyBar3D" parent="." index="6"]
texture = SubResource("ViewportTexture_n34tq")

View file

@ -14,8 +14,6 @@ signal changed
# DANGER "NONE" Should always be first
enum TYPE { NONE, PIERRE, ALINE, MAXENCE, VICTORIA, EVAN, ALEX, GERALDINE }
var projectileScene : PackedScene = preload("res://Towers/Projectiles/projectile.tscn")
@export var tower_name : String = "None"
@export var type : TYPE
@export_group("Base data")
@ -24,7 +22,7 @@ var projectileScene : PackedScene = preload("res://Towers/Projectiles/projectile
@export var price : int
@export_group("Attack")
@export var projectileRessource : ProjectileResource
@export var projectile : ProjectileResource
@export var towerRange : Shape3D
@export var action_cooldown : float = 0.3 :
set(value):
@ -59,8 +57,6 @@ var energy : float :
energy = clampf(value, 0, max_energy)
energyBar.value = energy
energy_changed.emit()
if not energy && state != STATE.BLUEPRINT:
state = STATE.EXHAUSTED
var availableTargets : Array[Enemy]
var selectable : bool :
@ -96,23 +92,23 @@ func changeState(newState : STATE) -> void:
match newState:
STATE.ACTION when state == STATE.REST: in_action()
STATE.ACTION when not energy: newState = STATE.EXHAUSTED
STATE.ACTION: pass
STATE.REST: resting()
_: return # NOTE Prevent changing of state
_: return # NOTE Prevent change of state
state = newState
func shoot() -> void:
if energy < energy_cost:
state = STATE.EXHAUSTED
return
var target : Enemy = choose_target()
if not target:
return
energy -= energy_cost
var projectile : Projectile = projectileScene.instantiate()
projectile.loadProjectile(projectileRessource, target)
EventBus.projectile_shooted.emit(projectile, $Aim.global_position)
projectile.shoot(target, $Aim.global_position)
$AttackCooldown.start(action_cooldown)

View file

@ -87,7 +87,7 @@ func handle_player_controls() -> CollisionObject3D:
visible = true
selection_icon.visible = true
global_position = collider.global_position
global_position.y += Helper.getTopOfHitBox(collider) + .01
global_position.y += Helper.getHitBoxLocation(collider, Helper.POSITION.TOP) + .01
if selected_tower && selected_tower.state == Tower.STATE.BLUEPRINT:
selected_tower.sprite.modulate = "ff4545c8" # If the tower can't be placed he is red

View file

@ -37,8 +37,8 @@ shape = SubResource("CapsuleShape3D_ynmsb")
[node name="Range" type="Area3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.4, 0)
collision_layer = 4
collision_mask = 4
collision_layer = 2
collision_mask = 2
[node name="Range" type="CollisionShape3D" parent="Range"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.475708, 0)

View file

@ -43,7 +43,7 @@ func onTowerChange() -> void:
onEnergyChange()
%TowerIcon.texture = tower.icone
%TowerName.text = tower.tower_name
%TowerDamage.text = "Dmg : %d" % tower.projectileRessource.damage
%TowerDamage.text = "Dmg : %d" % tower.projectile.damage
%TowerCooldown.text = "cooldown : %.1f" % tower.action_cooldown
# TODO Check for better UI to display it
#%TowerBio.text = tower.bio

View file

@ -23,8 +23,8 @@ func upgrade(tower: Tower = null) -> bool:
TYPE.MAX_ENERGY: upgradeProperty(tower, "max_energy")
TYPE.ENERGY_REGEN: upgradeProperty(tower, "energy_regen")
TYPE.ENERGY_COST: upgradeProperty(tower, "energy_cost")
TYPE.DAMAGE: upgradeProperty(tower.projectileRessource, "damage")
TYPE.AOE_INCREASE: upgradeProperty(tower.projectileRessource.damageArea, "radius")
TYPE.DAMAGE: upgradeProperty(tower.projectile, "damage")
TYPE.AOE_INCREASE: upgradeProperty(tower.projectile.damageArea, "radius")
TYPE.COOLDOWN: upgradeProperty(tower, "action_cooldown")
upgradeUpgrade()