ft: nucleotide prey FSM

This commit is contained in:
2026-01-26 23:54:15 +01:00
parent c11afd9ddd
commit adf1438bc8
18 changed files with 121 additions and 45 deletions

View File

@@ -0,0 +1,152 @@
extends AbstractPrey2D
@onready var sprite = get_node("AnimatedSprite2D")
@onready var fsm = $StateMachine
# Mirroed sprites for periodic boundary
var mirrorSprite1: Node2D
var mirrorSprite2: Node2D
var mirrorSprite3: Node2D
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
health = maxHealth
sprite.play("Healthy")
mirrorSprite1 = sprite.duplicate()
mirrorSprite2 = sprite.duplicate()
mirrorSprite3 = sprite.duplicate()
add_child(mirrorSprite1)
add_child(mirrorSprite2)
add_child(mirrorSprite3)
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void:
# Boundary mirroring
_handle_wrapping()
func _physics_process(delta: float) -> void:
#self.move(Vector3(randfn(0, 1), randfn(0, 1), 0))
# TODO: state transition logic (bot controller code)
pass
func move(motion: Vector3) -> void:
move_and_collide(Vector2(motion.x, motion.y)) # Moves along the given vector
# Apply boundary to new position
position = GameManager.get_boundaried_position(position)
func handle_damage(dmg: int, src: Node) -> void:
health = max(0, health-dmg)
if health == 0:
die()
if health < maxHealth:
become_injured()
fsm.transition_to_next_state(fsm.States.FLEEING, {"threat": src})
func die() -> void:
sprite.play("Dying")
super.die()
func become_injured() -> void:
sprite.play("Injured")
mirrorSprite1.play("Injured")
mirrorSprite2.play("Injured")
mirrorSprite3.play("Injured")
# Mirroring table:
# |---|---|---|---|
# | 4 | 3 | 4 | 3 |
# |---|===|===|---|
# | 1 ǁ 2 | 1 ǁ 2 |
# |---ǁ---|---ǁ---|
# | 4 ǁ 3 | 4 ǁ 3 |
# |---|===|===|---|
# | 1 | 2 | 1 | 2 |
# |---|---|---|---|
# If less than viewport size away from an edge, mirror over that edge (for seamless boundary)
# NOTE: For this to look correctly the camera size should be smaller than half the screen port (in
# any one dimension. Ideally, the difference between camera size and half the screen port is
# at least the size of the prey sprite)
func _handle_wrapping():
mirrorSprite1.visible = false
mirrorSprite2.visible = false
mirrorSprite3.visible = false
# TODO: Assume viewport size << screen size and only draw according to GameManager.viewport_size
# Find corresponding section of the screen
if position.x < GameManager.screen_size.x/2 and position.y < GameManager.screen_size.y/2:
# 2
mirrorSprite1.visible = true
mirrorSprite2.visible = true
mirrorSprite3.visible = true
# Right
#mirrorSprite1.position = Vector2(sprite.position.x + GameManager.screen_size.x, sprite.position.y)
mirrorSprite1.position = Vector2(GameManager.screen_size.x, 0)
# Diag
#mirrorSprite2.position = Vector2(sprite.position.x + GameManager.screen_size.x, sprite.position.y + GameManager.screen_size.y)
mirrorSprite3.position = Vector2(GameManager.screen_size.x, GameManager.screen_size.y)
# Bottom
#mirrorSprite3.position = Vector2(sprite.position.x, sprite.position.y + GameManager.screen_size.y)
mirrorSprite2.position = Vector2(0, GameManager.screen_size.y)
elif position.x < GameManager.screen_size.x/2:
# 3
mirrorSprite1.visible = true
mirrorSprite2.visible = true
mirrorSprite3.visible = true
# Top
#mirrorSprite1.position = Vector2(sprite.position.x, sprite.position.y - GameManager.screen_size.y)
mirrorSprite1.position = Vector2(0, - GameManager.screen_size.y)
# Diag
#mirrorSprite2.position = Vector2(sprite.position.x + GameManager.screen_size.x, sprite.position.y - GameManager.screen_size.y)
mirrorSprite2.position = Vector2(GameManager.screen_size.x, - GameManager.screen_size.y)
# Right
#mirrorSprite3.position = Vector2(sprite.position.x + GameManager.screen_size.x, sprite.position.y)
mirrorSprite3.position = Vector2(GameManager.screen_size.x, 0)
elif position.y < GameManager.screen_size.y/2:
# 1
mirrorSprite1.visible = true
mirrorSprite2.visible = true
mirrorSprite3.visible = true
# Left
#mirrorSprite1.position = Vector2(sprite.position.x - GameManager.screen_size.x, sprite.position.y)
mirrorSprite1.position = Vector2(- GameManager.screen_size.x, 0)
# Bottom
#mirrorSprite2.position = Vector2(sprite.position.x, sprite.position.y + GameManager.screen_size.y)
mirrorSprite2.position = Vector2(0, GameManager.screen_size.y)
# Diag
#mirrorSprite3.position = Vector2(sprite.position.x - GameManager.screen_size.x, sprite.position.y + GameManager.screen_size.y)
mirrorSprite3.position = Vector2(- GameManager.screen_size.x, GameManager.screen_size.y)
else:
# 4
mirrorSprite1.visible = true
mirrorSprite2.visible = true
mirrorSprite3.visible = true
# Left
#mirrorSprite1.position = Vector2(sprite.position.x - GameManager.screen_size.x, sprite.position.y)
mirrorSprite1.position = Vector2(- GameManager.screen_size.x, 0)
# Diag
#mirrorSprite2.position = Vector2(sprite.position.x - GameManager.screen_size.x, sprite.position.y - GameManager.screen_size.y)
mirrorSprite2.position = Vector2(- GameManager.screen_size.x, - GameManager.screen_size.y)
# Top
#mirrorSprite3.position = Vector2(sprite.position.x, sprite.position.y - GameManager.screen_size.y)
mirrorSprite3.position = Vector2(0, - GameManager.screen_size.y)
func _on_timer_timeout() -> void:
pass # Replace with function body.

View File

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

View File

@@ -0,0 +1,85 @@
[gd_scene load_steps=14 format=3 uid="uid://c3iw2v3x6ngrb"]
[ext_resource type="PackedScene" uid="uid://bvsdg1v3ksixy" path="res://shared/npc/prey2D.tscn" id="1_qvulj"]
[ext_resource type="Script" uid="uid://bgossk6xo31gi" path="res://molecular/prey/nucleotide_prey.gd" id="2_0227s"]
[ext_resource type="Texture2D" uid="uid://bhcb5g7g7um8" path="res://molecular/assets/prey/prey-dying-frame0.png" id="2_lkj7f"]
[ext_resource type="Texture2D" uid="uid://bxn11avw7dykl" path="res://molecular/assets/prey/prey-dying-frame1.png" id="3_svqyr"]
[ext_resource type="Texture2D" uid="uid://ctkehsavw6ghx" path="res://molecular/assets/prey/prey-healthy-frame0.png" id="4_ee1gb"]
[ext_resource type="Texture2D" uid="uid://uy28y3mkk6nt" path="res://molecular/assets/prey/prey-healthy-frame1.png" id="5_ae5nf"]
[ext_resource type="Texture2D" uid="uid://btnyajci8ptb2" path="res://molecular/assets/prey/prey-injured-frame0.png" id="6_0f87h"]
[ext_resource type="Texture2D" uid="uid://bqll8ge4cr2uf" path="res://molecular/assets/prey/prey-injured-frame1.png" id="7_w7inl"]
[ext_resource type="Script" uid="uid://0vwv2nt16gpv" path="res://molecular/prey/nucleotide_prey_state_machine.gd" id="9_xxtgy"]
[ext_resource type="Script" uid="uid://ubcu8fdfxxj1" path="res://molecular/prey/nucleotide_prey_random_movement.gd" id="10_rgguv"]
[ext_resource type="Script" uid="uid://xbiqj7ubmj7d" path="res://molecular/prey/nucleotide_prey_idle.gd" id="12_ubfhk"]
[ext_resource type="Script" uid="uid://dlw7inlh6asvu" path="res://molecular/prey/nucleotide_prey_fleeing.gd" id="12_xxtgy"]
[sub_resource type="SpriteFrames" id="SpriteFrames_66x8p"]
animations = [{
"frames": [{
"duration": 1.0,
"texture": ExtResource("2_lkj7f")
}, {
"duration": 20.0,
"texture": ExtResource("3_svqyr")
}],
"loop": true,
"name": &"Dying",
"speed": 1.0
}, {
"frames": [{
"duration": 1.0,
"texture": ExtResource("4_ee1gb")
}, {
"duration": 20.0,
"texture": ExtResource("5_ae5nf")
}],
"loop": true,
"name": &"Healthy",
"speed": 1.0
}, {
"frames": [{
"duration": 1.0,
"texture": ExtResource("6_0f87h")
}, {
"duration": 20.0,
"texture": ExtResource("7_w7inl")
}],
"loop": true,
"name": &"Injured",
"speed": 1.0
}]
[node name="NucleotidePrey" groups=["prey"] instance=ExtResource("1_qvulj")]
collision_layer = 2
motion_mode = 1
script = ExtResource("2_0227s")
maxHealth = 20
[node name="AnimatedSprite2D" type="AnimatedSprite2D" parent="." index="0"]
scale = Vector2(0.1, 0.1)
sprite_frames = SubResource("SpriteFrames_66x8p")
animation = &"Injured"
[node name="StateMachine" type="Node" parent="." index="2" node_paths=PackedStringArray("initial_state")]
script = ExtResource("9_xxtgy")
initial_state = NodePath("Idle")
metadata/_custom_type_script = "uid://ck7k8ht54snsy"
[node name="RandomMovement" type="Node" parent="StateMachine" index="0"]
script = ExtResource("10_rgguv")
[node name="Timer" type="Timer" parent="StateMachine/RandomMovement" index="0"]
one_shot = true
[node name="Fleeing" type="Node" parent="StateMachine" index="1"]
script = ExtResource("12_xxtgy")
[node name="Idle" type="Node" parent="StateMachine" index="2"]
script = ExtResource("12_ubfhk")
metadata/_custom_type_script = "uid://co2xp7gauamql"
[node name="Timer" type="Timer" parent="StateMachine/Idle" index="0"]
one_shot = true
[connection signal="timeout" from="StateMachine/RandomMovement/Timer" to="StateMachine/RandomMovement" method="_on_timer_timeout"]
[connection signal="timeout" from="StateMachine/Idle/Timer" to="StateMachine/Idle" method="_on_timer_timeout"]

View File

@@ -0,0 +1,22 @@
extends State
var threat: Node2D
var threshold: float = 100
func enter(previous_state_path: String, data := {}) -> void:
if data.has("threat"):
threat = data["threat"]
else:
# default behaviour; do nothing
threat = owner
func physics_update(_delta: float) -> void:
if owner.position.distance_to(threat.position) > threshold:
finished.emit(owner.fsm.States.IDLE, {})
return
owner.move(flee_from(threat.position))
func flee_from(pos: Vector2) -> Vector3:
var diff = threat.position - owner.position
diff = diff.normalized() * -1
return Vector3(diff.x, diff.y ,0)

View File

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

View File

@@ -0,0 +1,19 @@
extends State
@onready var timer = $Timer
func enter(previous_state_path: String, data := {}) -> void:
timer.start((float)(randi() % 5)/5)
func physics_update(_delta: float) -> void:
owner.move(Vector3(randfn(0, 1), randfn(0, 1), 0))
func _on_timer_timeout() -> void:
if (randi() % 4 != 0):
finished.emit(owner.fsm.States.RANDOMMOVEMENT, {})
else:
finished.emit(owner.fsm.States.IDLE, {})
func exit() -> void:
timer.stop()

View File

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

View File

@@ -0,0 +1,20 @@
extends State
@onready var timer = $Timer
var dir: Vector3 = Vector3(0,0,0);
func enter(previous_state_path: String, data := {}) -> void:
timer.start((float)(randi() % 10)/20)
dir = calc_dir(randi() % 360)
func physics_update(_delta: float) -> void:
owner.move(dir)
func calc_dir(angle: float) -> Vector3:
return Vector3(cos(angle), sin(angle), 0)
func _on_timer_timeout() -> void:
finished.emit(owner.fsm.States.IDLE, {})
func exit() -> void:
timer.stop()

View File

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

View File

@@ -0,0 +1,17 @@
extends StateMachine
enum States {IDLE, RANDOMMOVEMENT, FEEDING, FLEEING}
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
super()
await owner.ready
func transition_to_next_state(target: int, data: Dictionary = {}) -> void:
match target:
States.IDLE: _transition_to_next_state("Idle", data)
States.RANDOMMOVEMENT: _transition_to_next_state("RandomMovement", data)
States.FEEDING: _transition_to_next_state("Feeding", data)
States.FLEEING: _transition_to_next_state("Fleeing", data)
_: push_error("Trying to transition to unknown state {target}")

View File

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