[agent:claude-cli] feat(mobs): shark pursuit 15u + dégâts -20HP + knockback, fish school fuite boost, drops requin(dent)/méduse(gelée), recettes Amulette T2 + Lampe Portable
This commit is contained in:
@@ -317,6 +317,10 @@ func take_damage(amount: float) -> void:
|
|||||||
player_died.emit()
|
player_died.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func apply_knockback(impulse: Vector3) -> void:
|
||||||
|
velocity += impulse
|
||||||
|
|
||||||
|
|
||||||
func heal(amount: float) -> void:
|
func heal(amount: float) -> void:
|
||||||
health = min(max_health, health + amount)
|
health = min(max_health, health + amount)
|
||||||
_emit_stats()
|
_emit_stats()
|
||||||
|
|||||||
@@ -32,6 +32,16 @@ static var RECIPES: Array = [
|
|||||||
"inputs": [{"item_id": 105, "count": 2}, {"item_id": 4, "count": 1}],
|
"inputs": [{"item_id": 105, "count": 2}, {"item_id": 4, "count": 1}],
|
||||||
"output": {"item_id": 106, "count": 1}
|
"output": {"item_id": 106, "count": 1}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Amulette Tier 2",
|
||||||
|
"inputs": [{"item_id": 107, "count": 2}, {"item_id": 106, "count": 1}],
|
||||||
|
"output": {"item_id": 109, "count": 1}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Lampe Portable",
|
||||||
|
"inputs": [{"item_id": 108, "count": 2}, {"item_id": 100, "count": 1}],
|
||||||
|
"output": {"item_id": 110, "count": 1}
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ const ITEM_NAMES: Dictionary = {
|
|||||||
104: "Armure ecailles",
|
104: "Armure ecailles",
|
||||||
105: "Perle",
|
105: "Perle",
|
||||||
106: "Amulette de soin",
|
106: "Amulette de soin",
|
||||||
|
107: "Dent de Requin",
|
||||||
|
108: "Gelée Bioluminescente",
|
||||||
|
109: "Amulette Tier 2",
|
||||||
|
110: "Lampe Portable",
|
||||||
}
|
}
|
||||||
|
|
||||||
const ITEM_COLORS: Dictionary = {
|
const ITEM_COLORS: Dictionary = {
|
||||||
@@ -38,6 +42,10 @@ const ITEM_COLORS: Dictionary = {
|
|||||||
104: Color(0.1, 0.55, 0.5),
|
104: Color(0.1, 0.55, 0.5),
|
||||||
105: Color(0.95, 0.95, 1.0),
|
105: Color(0.95, 0.95, 1.0),
|
||||||
106: Color(1.0, 0.85, 0.4),
|
106: Color(1.0, 0.85, 0.4),
|
||||||
|
107: Color(0.9, 0.9, 0.85),
|
||||||
|
108: Color(0.2, 0.85, 0.65),
|
||||||
|
109: Color(1.0, 0.75, 0.15),
|
||||||
|
110: Color(0.5, 0.9, 1.0),
|
||||||
}
|
}
|
||||||
|
|
||||||
const PLACEABLE_IDS: Array = [2, 3, 4, 5, 6, 7, 8]
|
const PLACEABLE_IDS: Array = [2, 3, 4, 5, 6, 7, 8]
|
||||||
|
|||||||
@@ -110,13 +110,20 @@ func _update_boids(dt: float) -> void:
|
|||||||
if center_offset.length() > school_radius:
|
if center_offset.length() > school_radius:
|
||||||
vel += -center_offset.normalized() * 1.5 * dt * 4.0
|
vel += -center_offset.normalized() * 1.5 * dt * 4.0
|
||||||
|
|
||||||
# --- Player avoidance ---
|
# --- Player avoidance (boosted player = bigger + faster flee) ---
|
||||||
if is_instance_valid(_player):
|
if is_instance_valid(_player):
|
||||||
var world_fish_pos: Vector3 = global_position + fish.position
|
var world_fish_pos: Vector3 = global_position + fish.position
|
||||||
var dist_to_player: float = world_fish_pos.distance_to(_player.global_position)
|
var dist_to_player: float = world_fish_pos.distance_to(_player.global_position)
|
||||||
if dist_to_player < 3.0:
|
var player_boosting: bool = _player.get("is_boosting") if "is_boosting" in _player else false
|
||||||
|
var flee_radius: float = 6.0 if player_boosting else 3.0
|
||||||
|
var flee_force: float = 9.0 if player_boosting else 4.0
|
||||||
|
if dist_to_player < flee_radius:
|
||||||
var flee: Vector3 = (world_fish_pos - _player.global_position).normalized()
|
var flee: Vector3 = (world_fish_pos - _player.global_position).normalized()
|
||||||
vel += flee * 4.0 * dt * (3.0 - dist_to_player)
|
# Only flee if player is moving toward the school
|
||||||
|
var player_vel: Vector3 = _player.velocity if "velocity" in _player else Vector3.ZERO
|
||||||
|
var toward: bool = player_vel.dot((world_fish_pos - _player.global_position).normalized()) > 0.2
|
||||||
|
if toward or player_boosting:
|
||||||
|
vel += flee * flee_force * dt * (flee_radius - dist_to_player)
|
||||||
|
|
||||||
# Clamp speed
|
# Clamp speed
|
||||||
if vel.length() > swim_speed * 1.5:
|
if vel.length() > swim_speed * 1.5:
|
||||||
|
|||||||
@@ -160,4 +160,21 @@ func _pick_wander_target() -> void:
|
|||||||
func take_damage(dmg: float) -> void:
|
func take_damage(dmg: float) -> void:
|
||||||
health -= dmg
|
health -= dmg
|
||||||
if health <= 0.0:
|
if health <= 0.0:
|
||||||
|
_drop_loot()
|
||||||
queue_free()
|
queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
func _drop_loot() -> void:
|
||||||
|
if not is_instance_valid(_player):
|
||||||
|
return
|
||||||
|
var dist: float = global_position.distance_to(_player.global_position)
|
||||||
|
if dist > 10.0:
|
||||||
|
return
|
||||||
|
var main: Node = get_tree().get_first_node_in_group("main")
|
||||||
|
if main == null or main.get("inventory") == null:
|
||||||
|
return
|
||||||
|
# Drop 1 gelée bioluminescente
|
||||||
|
main.inventory.add_item(108, 1)
|
||||||
|
var pp: Node = get_node_or_null("/root/PlayerProgress")
|
||||||
|
if pp != null:
|
||||||
|
pp.award(8, "méduse", global_position)
|
||||||
|
|||||||
@@ -178,10 +178,11 @@ func _load_and_instantiate(scene_path: String, pos: Vector3) -> Node3D:
|
|||||||
func _connect_mob_signal(mob: Node3D) -> void:
|
func _connect_mob_signal(mob: Node3D) -> void:
|
||||||
if mob.has_signal("attacked_player"):
|
if mob.has_signal("attacked_player"):
|
||||||
if not mob.is_connected("attacked_player", _on_mob_attacked_player):
|
if not mob.is_connected("attacked_player", _on_mob_attacked_player):
|
||||||
|
# Use variadic-safe connect: bind by checking signal params
|
||||||
mob.attacked_player.connect(_on_mob_attacked_player)
|
mob.attacked_player.connect(_on_mob_attacked_player)
|
||||||
|
|
||||||
|
|
||||||
func _on_mob_attacked_player(dmg: float) -> void:
|
func _on_mob_attacked_player(dmg: float, kb_dir: Vector3 = Vector3.ZERO) -> void:
|
||||||
if is_instance_valid(_player_node):
|
if is_instance_valid(_player_node):
|
||||||
if _player_node.has_method("take_damage"):
|
if _player_node.has_method("take_damage"):
|
||||||
_player_node.take_damage(dmg)
|
_player_node.take_damage(dmg)
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
extends CharacterBody3D
|
extends CharacterBody3D
|
||||||
|
|
||||||
signal attacked_player(damage: float)
|
signal attacked_player(damage: float, knockback_dir: Vector3)
|
||||||
|
|
||||||
@export var max_health: float = 50.0
|
@export var max_health: float = 50.0
|
||||||
@export var damage: float = 15.0
|
@export var damage: float = 20.0
|
||||||
@export var detection_radius: float = 12.0
|
@export var detection_radius: float = 15.0 # chase trigger distance (15 units)
|
||||||
@export var move_speed: float = 4.0
|
@export var move_speed: float = 4.0
|
||||||
@export var chase_speed: float = 7.0
|
@export var chase_speed: float = 7.0
|
||||||
|
@export var knockback_force: float = 14.0
|
||||||
|
|
||||||
enum State { PATROL, CHASE, ATTACK }
|
enum State { PATROL, CHASE, ATTACK }
|
||||||
|
|
||||||
@@ -153,7 +154,13 @@ func _physics_process(delta: float) -> void:
|
|||||||
|
|
||||||
State.ATTACK:
|
State.ATTACK:
|
||||||
if is_instance_valid(_player) and _attack_cooldown <= 0.0:
|
if is_instance_valid(_player) and _attack_cooldown <= 0.0:
|
||||||
attacked_player.emit(damage)
|
var kb_dir: Vector3 = (_player.global_position - global_position).normalized()
|
||||||
|
attacked_player.emit(damage, kb_dir)
|
||||||
|
# Apply knockback directly on player
|
||||||
|
if _player.has_method("apply_knockback"):
|
||||||
|
_player.call("apply_knockback", kb_dir * knockback_force)
|
||||||
|
elif "velocity" in _player:
|
||||||
|
_player.velocity += kb_dir * knockback_force
|
||||||
_attack_cooldown = 2.0
|
_attack_cooldown = 2.0
|
||||||
if player_dist > 3.0:
|
if player_dist > 3.0:
|
||||||
_state = State.CHASE
|
_state = State.CHASE
|
||||||
@@ -186,4 +193,26 @@ func _do_patrol(delta: float) -> void:
|
|||||||
func take_damage(dmg: float) -> void:
|
func take_damage(dmg: float) -> void:
|
||||||
health -= dmg
|
health -= dmg
|
||||||
if health <= 0.0:
|
if health <= 0.0:
|
||||||
|
_drop_loot()
|
||||||
queue_free()
|
queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
func _drop_loot() -> void:
|
||||||
|
# Give shark tooth to nearby player inventory
|
||||||
|
if not is_instance_valid(_player):
|
||||||
|
return
|
||||||
|
var dist: float = global_position.distance_to(_player.global_position)
|
||||||
|
if dist > 12.0:
|
||||||
|
return
|
||||||
|
var main: Node = get_tree().get_first_node_in_group("main")
|
||||||
|
if main == null or main.get("inventory") == null:
|
||||||
|
return
|
||||||
|
# Drop 1-2 shark teeth
|
||||||
|
var count: int = randi_range(1, 2)
|
||||||
|
main.inventory.add_item(107, count)
|
||||||
|
# Visual popup
|
||||||
|
if main.has_method("_spawn_xp_popup"):
|
||||||
|
main.call("_spawn_xp_popup", 0, global_position)
|
||||||
|
var pp: Node = get_node_or_null("/root/PlayerProgress")
|
||||||
|
if pp != null:
|
||||||
|
pp.award(15, "requin", global_position)
|
||||||
|
|||||||
Reference in New Issue
Block a user