feat: define reusable animal archetypes
This commit is contained in:
@@ -0,0 +1,93 @@
|
|||||||
|
class_name AnimalCatalog
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
const CORE_CATALOG_PATH := "res://simulation/animals/definitions/core_animals.tres"
|
||||||
|
|
||||||
|
@export var animals: Array[AnimalDefinition] = []
|
||||||
|
|
||||||
|
var _definitions_by_id: Dictionary = {}
|
||||||
|
var _definition_ids: Array[StringName] = []
|
||||||
|
var _errors: Array[String] = []
|
||||||
|
|
||||||
|
|
||||||
|
static func create_core() -> AnimalCatalog:
|
||||||
|
var catalog := load(CORE_CATALOG_PATH) as AnimalCatalog
|
||||||
|
if catalog != null:
|
||||||
|
catalog.rebuild()
|
||||||
|
return catalog
|
||||||
|
|
||||||
|
|
||||||
|
func rebuild() -> Array[String]:
|
||||||
|
_definitions_by_id.clear()
|
||||||
|
_definition_ids.clear()
|
||||||
|
_errors.clear()
|
||||||
|
for definition in animals:
|
||||||
|
if definition == null:
|
||||||
|
_errors.append("Animal catalog contains a null definition")
|
||||||
|
continue
|
||||||
|
for error in definition.validate():
|
||||||
|
_errors.append("Animal '%s': %s" % [definition.animal_definition_id, error])
|
||||||
|
if _definitions_by_id.has(definition.animal_definition_id):
|
||||||
|
_errors.append("Duplicate animal definition '%s'" % definition.animal_definition_id)
|
||||||
|
else:
|
||||||
|
_definitions_by_id[definition.animal_definition_id] = definition
|
||||||
|
_definition_ids.append(definition.animal_definition_id)
|
||||||
|
_definition_ids.sort_custom(_id_less)
|
||||||
|
_errors.sort()
|
||||||
|
return get_errors()
|
||||||
|
|
||||||
|
|
||||||
|
func is_valid() -> bool:
|
||||||
|
return rebuild().is_empty()
|
||||||
|
|
||||||
|
|
||||||
|
func get_errors() -> Array[String]:
|
||||||
|
return _errors.duplicate()
|
||||||
|
|
||||||
|
|
||||||
|
func has_definition(animal_definition_id: StringName) -> bool:
|
||||||
|
_ensure_built()
|
||||||
|
return _definitions_by_id.has(animal_definition_id)
|
||||||
|
|
||||||
|
|
||||||
|
func get_definition(animal_definition_id: StringName) -> AnimalDefinition:
|
||||||
|
_ensure_built()
|
||||||
|
return _definitions_by_id.get(animal_definition_id) as AnimalDefinition
|
||||||
|
|
||||||
|
|
||||||
|
func matches_identity(animal_definition_id: StringName, species_id: StringName) -> bool:
|
||||||
|
var definition := get_definition(animal_definition_id)
|
||||||
|
return definition != null and definition.species_id == species_id
|
||||||
|
|
||||||
|
|
||||||
|
func get_definition_ids() -> Array[StringName]:
|
||||||
|
_ensure_built()
|
||||||
|
return _definition_ids.duplicate()
|
||||||
|
|
||||||
|
|
||||||
|
func get_definitions_for_behavior(behavior_id: StringName) -> Array[AnimalDefinition]:
|
||||||
|
_ensure_built()
|
||||||
|
var result: Array[AnimalDefinition] = []
|
||||||
|
for definition_id in _definition_ids:
|
||||||
|
var definition := _definitions_by_id[definition_id] as AnimalDefinition
|
||||||
|
if definition.behavior_id == behavior_id:
|
||||||
|
result.append(definition)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
func find_definition_for_species(species_id: StringName) -> AnimalDefinition:
|
||||||
|
_ensure_built()
|
||||||
|
for definition_id in _definition_ids:
|
||||||
|
var definition := _definitions_by_id[definition_id] as AnimalDefinition
|
||||||
|
if definition.species_id == species_id:
|
||||||
|
return definition
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
func _ensure_built() -> void:
|
||||||
|
if _definitions_by_id.is_empty() and not animals.is_empty():
|
||||||
|
rebuild()
|
||||||
|
|
||||||
|
|
||||||
|
static func _id_less(left: StringName, right: StringName) -> bool:
|
||||||
|
return String(left) < String(right)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bbd405aahq78w
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
class_name AnimalDefinition
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
const BEHAVIOR_GRAZER_ROUTINE := &"grazer_routine"
|
||||||
|
|
||||||
|
@export var animal_definition_id: StringName
|
||||||
|
@export var species_id: StringName
|
||||||
|
@export var display_name: String
|
||||||
|
@export var behavior_id: StringName = BEHAVIOR_GRAZER_ROUTINE
|
||||||
|
@export var supported_action_ids: Array[StringName] = []
|
||||||
|
@export_file("*.tscn") var presentation_scene_path: String
|
||||||
|
@export_range(0.1, 10.0, 0.1) var movement_speed := 1.6
|
||||||
|
@export var visual_scale := Vector3.ONE
|
||||||
|
@export var body_scale := Vector3.ONE
|
||||||
|
@export var fluff_scale := Vector3.ONE
|
||||||
|
@export var show_horns := true
|
||||||
|
@export var show_beard := true
|
||||||
|
|
||||||
|
|
||||||
|
func validate() -> Array[String]:
|
||||||
|
var errors: Array[String] = []
|
||||||
|
if not is_valid_stable_id(animal_definition_id):
|
||||||
|
errors.append("animal_definition_id is invalid")
|
||||||
|
if not is_valid_stable_id(species_id):
|
||||||
|
errors.append("species_id is invalid for '%s'" % animal_definition_id)
|
||||||
|
if display_name.strip_edges().is_empty():
|
||||||
|
errors.append("display_name is empty for '%s'" % animal_definition_id)
|
||||||
|
if behavior_id != BEHAVIOR_GRAZER_ROUTINE:
|
||||||
|
errors.append(
|
||||||
|
"behavior_id '%s' is unsupported for '%s'" % [behavior_id, animal_definition_id]
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
presentation_scene_path.is_empty()
|
||||||
|
or not presentation_scene_path.begins_with("res://")
|
||||||
|
or presentation_scene_path.get_extension() != "tscn"
|
||||||
|
or not ResourceLoader.exists(presentation_scene_path, "PackedScene")
|
||||||
|
):
|
||||||
|
errors.append("presentation_scene_path is invalid for '%s'" % animal_definition_id)
|
||||||
|
if not is_finite(movement_speed) or movement_speed <= 0.0:
|
||||||
|
errors.append("movement_speed must be positive for '%s'" % animal_definition_id)
|
||||||
|
if (
|
||||||
|
not visual_scale.is_finite()
|
||||||
|
or visual_scale.x <= 0.0
|
||||||
|
or visual_scale.y <= 0.0
|
||||||
|
or visual_scale.z <= 0.0
|
||||||
|
):
|
||||||
|
errors.append("visual_scale must be finite and positive for '%s'" % animal_definition_id)
|
||||||
|
if (
|
||||||
|
not body_scale.is_finite()
|
||||||
|
or body_scale.x <= 0.0
|
||||||
|
or body_scale.y <= 0.0
|
||||||
|
or body_scale.z <= 0.0
|
||||||
|
):
|
||||||
|
errors.append("body_scale must be finite and positive for '%s'" % animal_definition_id)
|
||||||
|
if (
|
||||||
|
not fluff_scale.is_finite()
|
||||||
|
or fluff_scale.x <= 0.0
|
||||||
|
or fluff_scale.y <= 0.0
|
||||||
|
or fluff_scale.z <= 0.0
|
||||||
|
):
|
||||||
|
errors.append("fluff_scale must be finite and positive for '%s'" % animal_definition_id)
|
||||||
|
var seen: Dictionary = {}
|
||||||
|
for action_id in supported_action_ids:
|
||||||
|
if not is_valid_stable_id(action_id):
|
||||||
|
errors.append("supported action ID is invalid for '%s'" % animal_definition_id)
|
||||||
|
elif seen.has(action_id):
|
||||||
|
errors.append(
|
||||||
|
"duplicate supported action '%s' for '%s'" % [action_id, animal_definition_id]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
seen[action_id] = true
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
func supports_action(action_id: StringName) -> bool:
|
||||||
|
return action_id in supported_action_ids
|
||||||
|
|
||||||
|
|
||||||
|
static func is_valid_stable_id(value: Variant) -> bool:
|
||||||
|
if value is not String and value is not StringName:
|
||||||
|
return false
|
||||||
|
var text := String(value)
|
||||||
|
if text.is_empty() or text.length() > 96:
|
||||||
|
return false
|
||||||
|
for character in text:
|
||||||
|
var code := character.unicode_at(0)
|
||||||
|
if not (
|
||||||
|
(code >= 48 and code <= 57)
|
||||||
|
or (code >= 65 and code <= 90)
|
||||||
|
or (code >= 97 and code <= 122)
|
||||||
|
or character in ["_", "-", ".", ":"]
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
return true
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cg5ou2nnnvcr2
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
class_name AnimalFactory
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
const CONFIGURATION_KEYS := [
|
||||||
|
"initial_hunger",
|
||||||
|
"initial_enabled",
|
||||||
|
"can_npcs_feed",
|
||||||
|
"can_player_feed",
|
||||||
|
"debug_label_enabled",
|
||||||
|
"initial_routine_site_id",
|
||||||
|
"initial_next_routine_tick",
|
||||||
|
]
|
||||||
|
|
||||||
|
var last_error := ""
|
||||||
|
|
||||||
|
var _catalog: AnimalCatalog
|
||||||
|
|
||||||
|
|
||||||
|
func _init(catalog: AnimalCatalog = null) -> void:
|
||||||
|
_catalog = catalog if catalog != null else AnimalCatalog.create_core()
|
||||||
|
if _catalog == null:
|
||||||
|
last_error = "Animal catalog could not be loaded"
|
||||||
|
elif not _catalog.rebuild().is_empty():
|
||||||
|
last_error = "; ".join(_catalog.get_errors())
|
||||||
|
|
||||||
|
|
||||||
|
func get_catalog() -> AnimalCatalog:
|
||||||
|
return _catalog
|
||||||
|
|
||||||
|
|
||||||
|
func create_presentation(
|
||||||
|
animal_definition_id: StringName,
|
||||||
|
animal_id: StringName,
|
||||||
|
display_name: String,
|
||||||
|
configuration: Dictionary = {}
|
||||||
|
) -> AnimalNode:
|
||||||
|
last_error = ""
|
||||||
|
if _catalog == null or not _catalog.has_definition(animal_definition_id):
|
||||||
|
return _fail("Unknown animal definition '%s'" % animal_definition_id)
|
||||||
|
if not AnimalDefinition.is_valid_stable_id(animal_id):
|
||||||
|
return _fail("animal_id is invalid")
|
||||||
|
if display_name.strip_edges().is_empty():
|
||||||
|
return _fail("display_name is empty")
|
||||||
|
if not _configuration_is_valid(configuration):
|
||||||
|
return null
|
||||||
|
var definition := _catalog.get_definition(animal_definition_id)
|
||||||
|
var presentation_scene := load(definition.presentation_scene_path) as PackedScene
|
||||||
|
var node := (
|
||||||
|
presentation_scene.instantiate() as AnimalNode if presentation_scene != null else null
|
||||||
|
)
|
||||||
|
if node == null:
|
||||||
|
return _fail(
|
||||||
|
"Presentation scene for '%s' must instantiate AnimalNode" % animal_definition_id
|
||||||
|
)
|
||||||
|
if not node.apply_animal_definition(definition):
|
||||||
|
node.free()
|
||||||
|
return _fail("Presentation rejected definition '%s'" % animal_definition_id)
|
||||||
|
node.animal_id = animal_id
|
||||||
|
node.display_name = display_name
|
||||||
|
_apply_configuration(node, configuration)
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
func _configuration_is_valid(configuration: Dictionary) -> bool:
|
||||||
|
for key in configuration:
|
||||||
|
if (key is not String and key is not StringName) or String(key) not in CONFIGURATION_KEYS:
|
||||||
|
return _fail_bool("Unknown animal presentation configuration '%s'" % key)
|
||||||
|
if configuration.has("initial_hunger"):
|
||||||
|
if (
|
||||||
|
configuration["initial_hunger"] is not int
|
||||||
|
and configuration["initial_hunger"] is not float
|
||||||
|
):
|
||||||
|
return _fail_bool("initial_hunger must be numeric")
|
||||||
|
var hunger := float(configuration["initial_hunger"])
|
||||||
|
if not is_finite(hunger) or hunger < 0.0 or hunger > 100.0:
|
||||||
|
return _fail_bool("initial_hunger must be between 0 and 100")
|
||||||
|
if (
|
||||||
|
configuration.has("initial_routine_site_id")
|
||||||
|
and not AnimalDefinition.is_valid_stable_id(configuration["initial_routine_site_id"])
|
||||||
|
):
|
||||||
|
return _fail_bool("initial_routine_site_id is invalid")
|
||||||
|
if configuration.has("initial_next_routine_tick"):
|
||||||
|
if configuration["initial_next_routine_tick"] is not int:
|
||||||
|
return _fail_bool("initial_next_routine_tick must be an int")
|
||||||
|
if int(configuration["initial_next_routine_tick"]) < 0:
|
||||||
|
return _fail_bool("initial_next_routine_tick must not be negative")
|
||||||
|
for key in ["initial_enabled", "can_npcs_feed", "can_player_feed", "debug_label_enabled"]:
|
||||||
|
if configuration.has(key) and configuration[key] is not bool:
|
||||||
|
return _fail_bool("%s must be a bool" % key)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_configuration(node: AnimalNode, configuration: Dictionary) -> void:
|
||||||
|
if configuration.has("initial_hunger"):
|
||||||
|
node.initial_hunger = float(configuration["initial_hunger"])
|
||||||
|
if configuration.has("initial_enabled"):
|
||||||
|
node.initial_enabled = bool(configuration["initial_enabled"])
|
||||||
|
if configuration.has("can_npcs_feed"):
|
||||||
|
node.can_npcs_feed = bool(configuration["can_npcs_feed"])
|
||||||
|
if configuration.has("can_player_feed"):
|
||||||
|
node.can_player_feed = bool(configuration["can_player_feed"])
|
||||||
|
if configuration.has("debug_label_enabled"):
|
||||||
|
node.debug_label_enabled = bool(configuration["debug_label_enabled"])
|
||||||
|
if configuration.has("initial_routine_site_id"):
|
||||||
|
node.initial_routine_site_id = StringName(configuration["initial_routine_site_id"])
|
||||||
|
if configuration.has("initial_next_routine_tick"):
|
||||||
|
node.initial_next_routine_tick = int(configuration["initial_next_routine_tick"])
|
||||||
|
|
||||||
|
|
||||||
|
func _fail(message: String) -> AnimalNode:
|
||||||
|
last_error = message
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
func _fail_bool(message: String) -> bool:
|
||||||
|
last_error = message
|
||||||
|
return false
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cgpayfff6eeug
|
||||||
@@ -29,6 +29,7 @@ var states: Dictionary = {}
|
|||||||
var economy: RefCounted
|
var economy: RefCounted
|
||||||
var active_world_adapter: Node
|
var active_world_adapter: Node
|
||||||
var current_tick := 0
|
var current_tick := 0
|
||||||
|
var _animal_catalog: AnimalCatalog
|
||||||
|
|
||||||
|
|
||||||
func configure(economy_service: RefCounted, world_adapter: Node) -> void:
|
func configure(economy_service: RefCounted, world_adapter: Node) -> void:
|
||||||
@@ -36,6 +37,19 @@ func configure(economy_service: RefCounted, world_adapter: Node) -> void:
|
|||||||
active_world_adapter = world_adapter
|
active_world_adapter = world_adapter
|
||||||
|
|
||||||
|
|
||||||
|
func configure_animal_catalog(catalog: AnimalCatalog) -> bool:
|
||||||
|
if catalog == null or not catalog.rebuild().is_empty():
|
||||||
|
return false
|
||||||
|
_animal_catalog = catalog
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func get_animal_catalog() -> AnimalCatalog:
|
||||||
|
if _animal_catalog == null:
|
||||||
|
_animal_catalog = AnimalCatalog.create_core()
|
||||||
|
return _animal_catalog
|
||||||
|
|
||||||
|
|
||||||
func advance(simulation_tick: int) -> void:
|
func advance(simulation_tick: int) -> void:
|
||||||
current_tick = simulation_tick
|
current_tick = simulation_tick
|
||||||
var animal_ids: Array = states.keys()
|
var animal_ids: Array = states.keys()
|
||||||
@@ -67,15 +81,23 @@ func register_node(node: AnimalNode) -> bool:
|
|||||||
push_error("AnimalCareSystem: Animal ID '%s' collides with a world target" % node.animal_id)
|
push_error("AnimalCareSystem: Animal ID '%s' collides with a world target" % node.animal_id)
|
||||||
return false
|
return false
|
||||||
var action_definition := SimulationDefinitions.get_action(SimulationIds.ACTION_FEED_ANIMAL)
|
var action_definition := SimulationDefinitions.get_action(SimulationIds.ACTION_FEED_ANIMAL)
|
||||||
|
var animal_catalog := get_animal_catalog()
|
||||||
|
var animal_definition := (
|
||||||
|
animal_catalog.get_definition(node.animal_definition_id) if animal_catalog != null else null
|
||||||
|
)
|
||||||
if (
|
if (
|
||||||
action_definition == null
|
action_definition == null
|
||||||
or action_definition.target_type != SimulationIds.TARGET_ANIMAL
|
or action_definition.target_type != SimulationIds.TARGET_ANIMAL
|
||||||
or node.species_id != SimulationIds.SPECIES_GOAT
|
or animal_definition == null
|
||||||
|
or animal_definition.behavior_id != AnimalDefinition.BEHAVIOR_GRAZER_ROUTINE
|
||||||
|
or animal_definition.species_id != node.species_id
|
||||||
|
or not animal_definition.supports_action(SimulationIds.ACTION_FEED_ANIMAL)
|
||||||
|
or not node.supports_action(SimulationIds.ACTION_FEED_ANIMAL)
|
||||||
):
|
):
|
||||||
push_error(
|
push_error(
|
||||||
(
|
(
|
||||||
"AnimalCareSystem: AnimalNode '%s' has an invalid feed action or species '%s'"
|
"AnimalCareSystem: AnimalNode '%s' has invalid definition '%s' or species '%s'"
|
||||||
% [node.animal_id, node.species_id]
|
% [node.animal_id, node.animal_definition_id, node.species_id]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[gd_resource type="Resource" script_class="AnimalCatalog" load_steps=4 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://simulation/animals/AnimalCatalog.gd" id="1_catalog"]
|
||||||
|
[ext_resource type="Resource" path="res://simulation/animals/definitions/domestic_goat.tres" id="2_goat"]
|
||||||
|
[ext_resource type="Resource" path="res://simulation/animals/definitions/domestic_sheep.tres" id="3_sheep"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_catalog")
|
||||||
|
animals = Array[ExtResource("2_goat")]([ExtResource("2_goat"), ExtResource("3_sheep")])
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[gd_resource type="Resource" script_class="AnimalDefinition" load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://simulation/animals/AnimalDefinition.gd" id="1_definition"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_definition")
|
||||||
|
animal_definition_id = &"domestic_goat"
|
||||||
|
species_id = &"goat"
|
||||||
|
display_name = "Domestic goat"
|
||||||
|
behavior_id = &"grazer_routine"
|
||||||
|
supported_action_ids = Array[StringName]([&"feed_animal"])
|
||||||
|
presentation_scene_path = "res://world/animals/grazer/cozy_grazer.tscn"
|
||||||
|
movement_speed = 1.6
|
||||||
|
visual_scale = Vector3(1, 1, 1)
|
||||||
|
body_scale = Vector3(1, 1, 1)
|
||||||
|
fluff_scale = Vector3(1, 1, 1)
|
||||||
|
show_horns = true
|
||||||
|
show_beard = true
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[gd_resource type="Resource" script_class="AnimalDefinition" load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://simulation/animals/AnimalDefinition.gd" id="1_definition"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_definition")
|
||||||
|
animal_definition_id = &"domestic_sheep"
|
||||||
|
species_id = &"sheep"
|
||||||
|
display_name = "Domestic sheep"
|
||||||
|
behavior_id = &"grazer_routine"
|
||||||
|
supported_action_ids = Array[StringName]([&"feed_animal"])
|
||||||
|
presentation_scene_path = "res://world/animals/grazer/cozy_grazer.tscn"
|
||||||
|
movement_speed = 1.4
|
||||||
|
visual_scale = Vector3(1.08, 1.04, 1.08)
|
||||||
|
body_scale = Vector3(1.12, 1.08, 1.12)
|
||||||
|
fluff_scale = Vector3(1.16, 1.14, 1.16)
|
||||||
|
show_horns = false
|
||||||
|
show_beard = false
|
||||||
@@ -4,12 +4,14 @@ extends RefCounted
|
|||||||
signal changed(state: AnimalStateRecord)
|
signal changed(state: AnimalStateRecord)
|
||||||
signal fed(state: AnimalStateRecord, actor_id: int)
|
signal fed(state: AnimalStateRecord, actor_id: int)
|
||||||
|
|
||||||
const SCHEMA_VERSION := 2
|
const SCHEMA_VERSION := 3
|
||||||
|
const ROUTINE_SCHEMA_VERSION := 2
|
||||||
const LEGACY_SCHEMA_VERSION := 1
|
const LEGACY_SCHEMA_VERSION := 1
|
||||||
const HUNGER_PER_TICK := 0.125
|
const HUNGER_PER_TICK := 0.125
|
||||||
const FEED_THRESHOLD := 65.0
|
const FEED_THRESHOLD := 65.0
|
||||||
const HUNGER_RELIEF := 55.0
|
const HUNGER_RELIEF := 55.0
|
||||||
const NEVER_FED_TICK := -1
|
const NEVER_FED_TICK := -1
|
||||||
|
const LEGACY_DEFINITION_BY_SPECIES := {"goat": "domestic_goat"}
|
||||||
|
|
||||||
var data: Dictionary
|
var data: Dictionary
|
||||||
|
|
||||||
@@ -25,6 +27,7 @@ static func create_from_node(node: AnimalNode) -> AnimalStateRecord:
|
|||||||
{
|
{
|
||||||
"schema_version": SCHEMA_VERSION,
|
"schema_version": SCHEMA_VERSION,
|
||||||
"animal_id": String(node.animal_id),
|
"animal_id": String(node.animal_id),
|
||||||
|
"animal_definition_id": String(node.animal_definition_id),
|
||||||
"display_name": node.display_name,
|
"display_name": node.display_name,
|
||||||
"species_id": String(node.species_id),
|
"species_id": String(node.species_id),
|
||||||
"position":
|
"position":
|
||||||
@@ -49,6 +52,8 @@ static func from_dictionary(record_data: Dictionary) -> AnimalStateRecord:
|
|||||||
var version := int(record_data.get("schema_version", -1))
|
var version := int(record_data.get("schema_version", -1))
|
||||||
if version == LEGACY_SCHEMA_VERSION:
|
if version == LEGACY_SCHEMA_VERSION:
|
||||||
record_data = _migrate_legacy(record_data)
|
record_data = _migrate_legacy(record_data)
|
||||||
|
elif version == ROUTINE_SCHEMA_VERSION:
|
||||||
|
record_data = _migrate_definition_identity(record_data)
|
||||||
elif version != SCHEMA_VERSION:
|
elif version != SCHEMA_VERSION:
|
||||||
return null
|
return null
|
||||||
if not (
|
if not (
|
||||||
@@ -56,6 +61,7 @@ static func from_dictionary(record_data: Dictionary) -> AnimalStateRecord:
|
|||||||
. has_all(
|
. has_all(
|
||||||
[
|
[
|
||||||
"animal_id",
|
"animal_id",
|
||||||
|
"animal_definition_id",
|
||||||
"display_name",
|
"display_name",
|
||||||
"species_id",
|
"species_id",
|
||||||
"position",
|
"position",
|
||||||
@@ -75,6 +81,7 @@ static func from_dictionary(record_data: Dictionary) -> AnimalStateRecord:
|
|||||||
):
|
):
|
||||||
return null
|
return null
|
||||||
var animal_id := String(record_data["animal_id"])
|
var animal_id := String(record_data["animal_id"])
|
||||||
|
var animal_definition_id := String(record_data["animal_definition_id"])
|
||||||
var display_name := String(record_data["display_name"])
|
var display_name := String(record_data["display_name"])
|
||||||
var species_id := String(record_data["species_id"])
|
var species_id := String(record_data["species_id"])
|
||||||
var saved_position = record_data["position"]
|
var saved_position = record_data["position"]
|
||||||
@@ -87,9 +94,12 @@ static func from_dictionary(record_data: Dictionary) -> AnimalStateRecord:
|
|||||||
var has_travel_target := bool(record_data["has_travel_target"])
|
var has_travel_target := bool(record_data["has_travel_target"])
|
||||||
var next_routine_tick := int(record_data["next_routine_tick"])
|
var next_routine_tick := int(record_data["next_routine_tick"])
|
||||||
var enabled := bool(record_data["enabled"])
|
var enabled := bool(record_data["enabled"])
|
||||||
if animal_id.is_empty() or display_name.is_empty() or species_id.is_empty():
|
if (
|
||||||
return null
|
not AnimalDefinition.is_valid_stable_id(animal_id)
|
||||||
if StringName(species_id) != SimulationIds.SPECIES_GOAT:
|
or not AnimalDefinition.is_valid_stable_id(animal_definition_id)
|
||||||
|
or display_name.is_empty()
|
||||||
|
or not AnimalDefinition.is_valid_stable_id(species_id)
|
||||||
|
):
|
||||||
return null
|
return null
|
||||||
if not saved_position is Array or saved_position.size() != 3:
|
if not saved_position is Array or saved_position.size() != 3:
|
||||||
return null
|
return null
|
||||||
@@ -120,6 +130,7 @@ static func from_dictionary(record_data: Dictionary) -> AnimalStateRecord:
|
|||||||
var normalized := record_data.duplicate(true)
|
var normalized := record_data.duplicate(true)
|
||||||
normalized["schema_version"] = SCHEMA_VERSION
|
normalized["schema_version"] = SCHEMA_VERSION
|
||||||
normalized["animal_id"] = animal_id
|
normalized["animal_id"] = animal_id
|
||||||
|
normalized["animal_definition_id"] = animal_definition_id
|
||||||
normalized["display_name"] = display_name
|
normalized["display_name"] = display_name
|
||||||
normalized["species_id"] = species_id
|
normalized["species_id"] = species_id
|
||||||
normalized["position"] = [
|
normalized["position"] = [
|
||||||
@@ -145,7 +156,8 @@ func apply_definition(node: AnimalNode) -> bool:
|
|||||||
if node == null or node.animal_id != get_animal_id():
|
if node == null or node.animal_id != get_animal_id():
|
||||||
return false
|
return false
|
||||||
return (
|
return (
|
||||||
node.display_name == get_display_name()
|
node.animal_definition_id == get_animal_definition_id()
|
||||||
|
and node.display_name == get_display_name()
|
||||||
and node.species_id == get_species_id()
|
and node.species_id == get_species_id()
|
||||||
and node.can_npcs_feed == can_npc_feed()
|
and node.can_npcs_feed == can_npc_feed()
|
||||||
and node.can_player_feed == can_player_feed_animal()
|
and node.can_player_feed == can_player_feed_animal()
|
||||||
@@ -156,6 +168,10 @@ func get_animal_id() -> StringName:
|
|||||||
return StringName(data["animal_id"])
|
return StringName(data["animal_id"])
|
||||||
|
|
||||||
|
|
||||||
|
func get_animal_definition_id() -> StringName:
|
||||||
|
return StringName(data["animal_definition_id"])
|
||||||
|
|
||||||
|
|
||||||
func get_display_name() -> String:
|
func get_display_name() -> String:
|
||||||
return String(data["display_name"])
|
return String(data["display_name"])
|
||||||
|
|
||||||
@@ -350,10 +366,18 @@ func to_dictionary() -> Dictionary:
|
|||||||
|
|
||||||
static func _migrate_legacy(record_data: Dictionary) -> Dictionary:
|
static func _migrate_legacy(record_data: Dictionary) -> Dictionary:
|
||||||
var migrated := record_data.duplicate(true)
|
var migrated := record_data.duplicate(true)
|
||||||
migrated["schema_version"] = SCHEMA_VERSION
|
migrated["schema_version"] = ROUTINE_SCHEMA_VERSION
|
||||||
migrated["routine_site_id"] = ""
|
migrated["routine_site_id"] = ""
|
||||||
migrated["travel_target_site_id"] = ""
|
migrated["travel_target_site_id"] = ""
|
||||||
migrated["travel_target_position"] = [0.0, 0.0, 0.0]
|
migrated["travel_target_position"] = [0.0, 0.0, 0.0]
|
||||||
migrated["has_travel_target"] = false
|
migrated["has_travel_target"] = false
|
||||||
migrated["next_routine_tick"] = 0
|
migrated["next_routine_tick"] = 0
|
||||||
|
return _migrate_definition_identity(migrated)
|
||||||
|
|
||||||
|
|
||||||
|
static func _migrate_definition_identity(record_data: Dictionary) -> Dictionary:
|
||||||
|
var migrated := record_data.duplicate(true)
|
||||||
|
migrated["schema_version"] = SCHEMA_VERSION
|
||||||
|
var species_id := String(migrated.get("species_id", ""))
|
||||||
|
migrated["animal_definition_id"] = String(LEGACY_DEFINITION_BY_SPECIES.get(species_id, ""))
|
||||||
return migrated
|
return migrated
|
||||||
|
|||||||
@@ -0,0 +1,234 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
|
||||||
|
class FakeRoutineWorld:
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
func get_animal_routine_sites(
|
||||||
|
species_id: StringName, animal_id: StringName
|
||||||
|
) -> Array[Dictionary]:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"site_id": &"%s_%s_meadow" % [species_id, animal_id],
|
||||||
|
"position": Vector3(3.0 if species_id == &"goat" else -3.0, 0.0, 2.0),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
var failures: Array[String] = []
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
var catalog := AnimalCatalog.create_core()
|
||||||
|
_check(catalog != null and catalog.rebuild().is_empty(), "Core animal content should validate")
|
||||||
|
if catalog == null:
|
||||||
|
_finish()
|
||||||
|
return
|
||||||
|
var definition_ids := catalog.get_definition_ids()
|
||||||
|
_check(
|
||||||
|
definition_ids == [&"domestic_goat", &"domestic_sheep"],
|
||||||
|
"Animal definitions should iterate in deterministic stable-ID order"
|
||||||
|
)
|
||||||
|
var goat_definition := catalog.get_definition(&"domestic_goat")
|
||||||
|
var sheep_definition := catalog.get_definition(&"domestic_sheep")
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
goat_definition != null
|
||||||
|
and sheep_definition != null
|
||||||
|
and goat_definition.species_id == &"goat"
|
||||||
|
and sheep_definition.species_id == &"sheep"
|
||||||
|
),
|
||||||
|
"Goat and sheep should be distinct data definitions"
|
||||||
|
)
|
||||||
|
if goat_definition == null or sheep_definition == null:
|
||||||
|
_finish()
|
||||||
|
return
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
goat_definition.behavior_id == AnimalDefinition.BEHAVIOR_GRAZER_ROUTINE
|
||||||
|
and sheep_definition.behavior_id == AnimalDefinition.BEHAVIOR_GRAZER_ROUTINE
|
||||||
|
and goat_definition.presentation_scene_path == sheep_definition.presentation_scene_path
|
||||||
|
),
|
||||||
|
"Both animals should reuse one grazer behavior and presentation contract"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
goat_definition.supports_action(&"feed_animal")
|
||||||
|
and sheep_definition.supports_action(&"feed_animal")
|
||||||
|
),
|
||||||
|
"Both data definitions should advertise the existing care action"
|
||||||
|
)
|
||||||
|
|
||||||
|
var generic_scene := load(goat_definition.presentation_scene_path) as PackedScene
|
||||||
|
var generic := generic_scene.instantiate() as AnimalNode
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
generic != null
|
||||||
|
and generic.animal_id.is_empty()
|
||||||
|
and generic.animal_definition_id.is_empty()
|
||||||
|
and generic.species_id.is_empty()
|
||||||
|
and generic.display_name == "Animal"
|
||||||
|
and (generic.get_node("DebugLabel") as Label3D).text == "Animal"
|
||||||
|
),
|
||||||
|
"The reusable grazer scene should contain no Dunja-specific identity"
|
||||||
|
)
|
||||||
|
if generic != null:
|
||||||
|
generic.free()
|
||||||
|
|
||||||
|
var factory := AnimalFactory.new(catalog)
|
||||||
|
var goat := (
|
||||||
|
factory
|
||||||
|
. create_presentation(
|
||||||
|
&"domestic_goat",
|
||||||
|
&"test_goat",
|
||||||
|
"Dunja Test",
|
||||||
|
{
|
||||||
|
"initial_hunger": 10.0,
|
||||||
|
"initial_routine_site_id": &"goat_barn",
|
||||||
|
"initial_next_routine_tick": 0,
|
||||||
|
"debug_label_enabled": false,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
var sheep := (
|
||||||
|
factory
|
||||||
|
. create_presentation(
|
||||||
|
&"domestic_sheep",
|
||||||
|
&"test_sheep",
|
||||||
|
"Mila Test",
|
||||||
|
{
|
||||||
|
"initial_hunger": 10.0,
|
||||||
|
"initial_routine_site_id": &"sheep_fold",
|
||||||
|
"initial_next_routine_tick": 0,
|
||||||
|
"debug_label_enabled": false,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_check(goat != null and sheep != null, "The shared factory should create both definitions")
|
||||||
|
if goat == null or sheep == null:
|
||||||
|
if goat != null:
|
||||||
|
goat.free()
|
||||||
|
if sheep != null:
|
||||||
|
sheep.free()
|
||||||
|
_finish()
|
||||||
|
return
|
||||||
|
root.add_child(goat)
|
||||||
|
root.add_child(sheep)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
goat.get_script() == sheep.get_script()
|
||||||
|
and goat.get_node("Visual").get_script() == sheep.get_node("Visual").get_script()
|
||||||
|
),
|
||||||
|
"A second animal should add no movement or visual-controller system"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
goat.get_node("Visual/HeadRoot/HornLeft").visible
|
||||||
|
and not sheep.get_node("Visual/HeadRoot/HornLeft").visible
|
||||||
|
and not sheep.get_node("Visual/HeadRoot/Beard").visible
|
||||||
|
),
|
||||||
|
"Definition data should configure bounded presentation differences"
|
||||||
|
)
|
||||||
|
|
||||||
|
var routine_world := FakeRoutineWorld.new()
|
||||||
|
root.add_child(routine_world)
|
||||||
|
var care := AnimalCareSystem.new()
|
||||||
|
care.configure(null, routine_world)
|
||||||
|
_check(care.configure_animal_catalog(catalog), "Animal care should accept the isolated catalog")
|
||||||
|
var goat_registered := care.register_node(goat)
|
||||||
|
var sheep_registered := care.register_node(sheep)
|
||||||
|
_check(
|
||||||
|
goat_registered and sheep_registered,
|
||||||
|
"Existing AnimalCareSystem should bind both shared-contract nodes"
|
||||||
|
)
|
||||||
|
var goat_state := care.get_state(&"test_goat")
|
||||||
|
var sheep_state := care.get_state(&"test_sheep")
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
goat_state != null
|
||||||
|
and sheep_state != null
|
||||||
|
and goat_state.get_animal_definition_id() == &"domestic_goat"
|
||||||
|
and sheep_state.get_animal_definition_id() == &"domestic_sheep"
|
||||||
|
and goat_state.get_script() == sheep_state.get_script()
|
||||||
|
),
|
||||||
|
"Authoritative records should retain stable animal definition IDs"
|
||||||
|
)
|
||||||
|
if goat_state != null and sheep_state != null:
|
||||||
|
var restored_sheep := AnimalStateRecord.from_dictionary(sheep_state.to_dictionary())
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
restored_sheep != null
|
||||||
|
and restored_sheep.get_animal_definition_id() == &"domestic_sheep"
|
||||||
|
and restored_sheep.get_species_id() == &"sheep"
|
||||||
|
),
|
||||||
|
"The second animal should round-trip through the existing animal record system"
|
||||||
|
)
|
||||||
|
var started: Array[StringName] = []
|
||||||
|
care.routine_started.connect(
|
||||||
|
func(animal_id: StringName, _site_id: StringName, _position: Vector3) -> void:
|
||||||
|
started.append(animal_id)
|
||||||
|
)
|
||||||
|
care.advance(0)
|
||||||
|
started.sort_custom(
|
||||||
|
func(left: StringName, right: StringName) -> bool: return String(left) < String(right)
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
started == [&"test_goat", &"test_sheep"]
|
||||||
|
and goat_state.has_active_travel_target()
|
||||||
|
and sheep_state.has_active_travel_target()
|
||||||
|
),
|
||||||
|
"Goat and sheep should execute the same deterministic routine scheduler"
|
||||||
|
)
|
||||||
|
_test_nested_record_migration(goat_state)
|
||||||
|
|
||||||
|
goat.free()
|
||||||
|
sheep.free()
|
||||||
|
routine_world.free()
|
||||||
|
_finish()
|
||||||
|
|
||||||
|
|
||||||
|
func _test_nested_record_migration(goat_state: AnimalStateRecord) -> void:
|
||||||
|
var previous_record := goat_state.to_dictionary()
|
||||||
|
previous_record["schema_version"] = AnimalStateRecord.ROUTINE_SCHEMA_VERSION
|
||||||
|
previous_record.erase("animal_definition_id")
|
||||||
|
var migrated := AnimalStateRecord.from_dictionary(previous_record)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
migrated != null
|
||||||
|
and migrated.get_animal_definition_id() == &"domestic_goat"
|
||||||
|
and migrated.to_dictionary()["schema_version"] == AnimalStateRecord.SCHEMA_VERSION
|
||||||
|
),
|
||||||
|
"Nested v2 animal records should migrate to stable goat definition identity"
|
||||||
|
)
|
||||||
|
var mismatched := goat_state.to_dictionary()
|
||||||
|
mismatched["animal_definition_id"] = "domestic_sheep"
|
||||||
|
var mismatched_record := AnimalStateRecord.from_dictionary(mismatched)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
mismatched_record != null
|
||||||
|
and not AnimalCatalog.create_core().matches_identity(
|
||||||
|
mismatched_record.get_animal_definition_id(), mismatched_record.get_species_id()
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"Data-only restore should remain extensible while the catalog detects identity mismatches"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _check(condition: bool, message: String) -> void:
|
||||||
|
if not condition:
|
||||||
|
failures.append(message)
|
||||||
|
|
||||||
|
|
||||||
|
func _finish() -> void:
|
||||||
|
if failures.is_empty():
|
||||||
|
print("[TEST] Shared animal definition contract passed")
|
||||||
|
quit(0)
|
||||||
|
return
|
||||||
|
for failure in failures:
|
||||||
|
push_error("[TEST] " + failure)
|
||||||
|
quit(1)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dad5kvxbeg72u
|
||||||
@@ -9,10 +9,13 @@ signal routine_arrived(animal_id: StringName, site_id: StringName, arrival_posit
|
|||||||
signal navigation_failed(animal_id: StringName, site_id: StringName)
|
signal navigation_failed(animal_id: StringName, site_id: StringName)
|
||||||
|
|
||||||
static var _all: Array[AnimalNode] = []
|
static var _all: Array[AnimalNode] = []
|
||||||
|
static var _core_animal_catalog: AnimalCatalog
|
||||||
|
|
||||||
@export var animal_id: StringName
|
@export var animal_id: StringName
|
||||||
@export var display_name := "Animal"
|
@export var display_name := "Animal"
|
||||||
@export var species_id: StringName = SimulationIds.SPECIES_GOAT
|
@export var animal_definition_id: StringName
|
||||||
|
@export var species_id: StringName
|
||||||
|
@export var supported_action_ids: Array[StringName] = []
|
||||||
@export_range(0.0, 100.0, 0.5) var initial_hunger := 70.0
|
@export_range(0.0, 100.0, 0.5) var initial_hunger := 70.0
|
||||||
@export var initial_enabled := true
|
@export var initial_enabled := true
|
||||||
@export var can_npcs_feed := true
|
@export var can_npcs_feed := true
|
||||||
@@ -35,9 +38,20 @@ var navigation_target := Vector3.INF
|
|||||||
var navigation_request_id := 0
|
var navigation_request_id := 0
|
||||||
var path_pending := false
|
var path_pending := false
|
||||||
var has_reported_navigation_result := true
|
var has_reported_navigation_result := true
|
||||||
|
var _animal_definition: AnimalDefinition
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
if not _ensure_animal_definition():
|
||||||
|
push_error(
|
||||||
|
(
|
||||||
|
"AnimalNode at %s has invalid animal_definition_id '%s'"
|
||||||
|
% [get_path(), animal_definition_id]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
initial_enabled = false
|
||||||
|
_update_presentation()
|
||||||
|
return
|
||||||
if animal_id.is_empty():
|
if animal_id.is_empty():
|
||||||
push_error("AnimalNode at %s has empty animal_id" % get_path())
|
push_error("AnimalNode at %s has empty animal_id" % get_path())
|
||||||
initial_enabled = false
|
initial_enabled = false
|
||||||
@@ -69,7 +83,11 @@ func _exit_tree() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func bind_state(animal_state: AnimalStateRecord) -> bool:
|
func bind_state(animal_state: AnimalStateRecord) -> bool:
|
||||||
if animal_state == null or animal_state.get_animal_id() != animal_id:
|
if (
|
||||||
|
animal_state == null
|
||||||
|
or animal_state.get_animal_id() != animal_id
|
||||||
|
or animal_state.get_animal_definition_id() != animal_definition_id
|
||||||
|
):
|
||||||
return false
|
return false
|
||||||
_disconnect_state()
|
_disconnect_state()
|
||||||
state = animal_state
|
state = animal_state
|
||||||
@@ -111,7 +129,42 @@ func get_interaction_position() -> Vector3:
|
|||||||
|
|
||||||
|
|
||||||
func supports_action(action_id: StringName) -> bool:
|
func supports_action(action_id: StringName) -> bool:
|
||||||
return action_id == SimulationIds.ACTION_FEED_ANIMAL
|
return action_id in supported_action_ids
|
||||||
|
|
||||||
|
|
||||||
|
func apply_animal_definition(definition: AnimalDefinition) -> bool:
|
||||||
|
if definition == null or not definition.validate().is_empty():
|
||||||
|
return false
|
||||||
|
if state != null and state.get_animal_definition_id() != definition.animal_definition_id:
|
||||||
|
return false
|
||||||
|
_animal_definition = definition
|
||||||
|
animal_definition_id = definition.animal_definition_id
|
||||||
|
species_id = definition.species_id
|
||||||
|
supported_action_ids = definition.supported_action_ids.duplicate()
|
||||||
|
move_speed = definition.movement_speed
|
||||||
|
var visual := get_node_or_null("Visual")
|
||||||
|
if visual != null and visual.has_method("apply_animal_definition"):
|
||||||
|
return bool(visual.apply_animal_definition(definition))
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func get_animal_definition() -> AnimalDefinition:
|
||||||
|
return _animal_definition
|
||||||
|
|
||||||
|
|
||||||
|
func _ensure_animal_definition() -> bool:
|
||||||
|
if (
|
||||||
|
_animal_definition != null
|
||||||
|
and _animal_definition.animal_definition_id == animal_definition_id
|
||||||
|
):
|
||||||
|
return true
|
||||||
|
if animal_definition_id.is_empty():
|
||||||
|
return false
|
||||||
|
if _core_animal_catalog == null:
|
||||||
|
_core_animal_catalog = AnimalCatalog.create_core()
|
||||||
|
if _core_animal_catalog == null:
|
||||||
|
return false
|
||||||
|
return apply_animal_definition(_core_animal_catalog.get_definition(animal_definition_id))
|
||||||
|
|
||||||
|
|
||||||
func get_hunger() -> float:
|
func get_hunger() -> float:
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ static var _all: Array[AnimalRoutineSite] = []
|
|||||||
|
|
||||||
@export var site_id: StringName
|
@export var site_id: StringName
|
||||||
@export var display_name := "Animal site"
|
@export var display_name := "Animal site"
|
||||||
@export var species_id: StringName = SimulationIds.SPECIES_GOAT
|
@export var species_id: StringName
|
||||||
@export var resident_animal_id: StringName
|
@export var resident_animal_id: StringName
|
||||||
@export var debug_label_enabled := false
|
@export var debug_label_enabled := false
|
||||||
|
|
||||||
|
|||||||
@@ -1,316 +1,12 @@
|
|||||||
[gd_scene load_steps=24 format=3]
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://world/animals/animal_node.gd" id="1_animal"]
|
[ext_resource type="PackedScene" path="res://world/animals/grazer/cozy_grazer.tscn" id="1_grazer"]
|
||||||
[ext_resource type="Script" path="res://world/animals/goat/cozy_goat_visual.gd" id="2_visual"]
|
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="Material_wool"]
|
[node name="CozyGoat" instance=ExtResource("1_grazer")]
|
||||||
albedo_color = Color(0.88, 0.82, 0.67, 1)
|
animal_definition_id = &"domestic_goat"
|
||||||
roughness = 0.96
|
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="Material_face"]
|
|
||||||
albedo_color = Color(0.58, 0.43, 0.29, 1)
|
|
||||||
roughness = 0.92
|
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="Material_dark"]
|
|
||||||
albedo_color = Color(0.13, 0.09, 0.065, 1)
|
|
||||||
roughness = 0.9
|
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="Material_horn"]
|
|
||||||
albedo_color = Color(0.78, 0.68, 0.5, 1)
|
|
||||||
roughness = 0.86
|
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="Material_collar"]
|
|
||||||
albedo_color = Color(0.12, 0.5, 0.5, 1)
|
|
||||||
roughness = 0.72
|
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="Material_hunger"]
|
|
||||||
transparency = 1
|
|
||||||
shading_mode = 0
|
|
||||||
albedo_color = Color(0.98, 0.62, 0.22, 0.9)
|
|
||||||
emission_enabled = true
|
|
||||||
emission = Color(0.92, 0.36, 0.08, 1)
|
|
||||||
emission_energy_multiplier = 0.32
|
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="Material_fed"]
|
|
||||||
transparency = 1
|
|
||||||
shading_mode = 0
|
|
||||||
albedo_color = Color(0.72, 0.92, 0.5, 0.92)
|
|
||||||
emission_enabled = true
|
|
||||||
emission = Color(0.45, 0.78, 0.28, 1)
|
|
||||||
emission_energy_multiplier = 0.38
|
|
||||||
|
|
||||||
[sub_resource type="CapsuleMesh" id="Mesh_body"]
|
|
||||||
material = SubResource("Material_wool")
|
|
||||||
radius = 0.48
|
|
||||||
height = 1.55
|
|
||||||
radial_segments = 12
|
|
||||||
rings = 4
|
|
||||||
|
|
||||||
[sub_resource type="SphereMesh" id="Mesh_fluff"]
|
|
||||||
material = SubResource("Material_wool")
|
|
||||||
radius = 0.42
|
|
||||||
height = 0.72
|
|
||||||
radial_segments = 12
|
|
||||||
rings = 6
|
|
||||||
|
|
||||||
[sub_resource type="CapsuleMesh" id="Mesh_leg"]
|
|
||||||
material = SubResource("Material_face")
|
|
||||||
radius = 0.105
|
|
||||||
height = 0.62
|
|
||||||
radial_segments = 8
|
|
||||||
rings = 3
|
|
||||||
|
|
||||||
[sub_resource type="BoxMesh" id="Mesh_hoof"]
|
|
||||||
material = SubResource("Material_dark")
|
|
||||||
size = Vector3(0.2, 0.13, 0.28)
|
|
||||||
|
|
||||||
[sub_resource type="SphereMesh" id="Mesh_head"]
|
|
||||||
material = SubResource("Material_face")
|
|
||||||
radius = 0.34
|
|
||||||
height = 0.58
|
|
||||||
radial_segments = 12
|
|
||||||
rings = 6
|
|
||||||
|
|
||||||
[sub_resource type="SphereMesh" id="Mesh_snout"]
|
|
||||||
material = SubResource("Material_wool")
|
|
||||||
radius = 0.21
|
|
||||||
height = 0.32
|
|
||||||
radial_segments = 10
|
|
||||||
rings = 5
|
|
||||||
|
|
||||||
[sub_resource type="SphereMesh" id="Mesh_ear"]
|
|
||||||
material = SubResource("Material_face")
|
|
||||||
radius = 0.24
|
|
||||||
height = 0.42
|
|
||||||
radial_segments = 10
|
|
||||||
rings = 5
|
|
||||||
|
|
||||||
[sub_resource type="SphereMesh" id="Mesh_eye"]
|
|
||||||
material = SubResource("Material_dark")
|
|
||||||
radius = 0.045
|
|
||||||
height = 0.075
|
|
||||||
radial_segments = 8
|
|
||||||
rings = 4
|
|
||||||
|
|
||||||
[sub_resource type="CylinderMesh" id="Mesh_horn"]
|
|
||||||
material = SubResource("Material_horn")
|
|
||||||
top_radius = 0.035
|
|
||||||
bottom_radius = 0.09
|
|
||||||
height = 0.42
|
|
||||||
radial_segments = 8
|
|
||||||
|
|
||||||
[sub_resource type="CylinderMesh" id="Mesh_beard"]
|
|
||||||
material = SubResource("Material_wool")
|
|
||||||
top_radius = 0.015
|
|
||||||
bottom_radius = 0.11
|
|
||||||
height = 0.34
|
|
||||||
radial_segments = 8
|
|
||||||
|
|
||||||
[sub_resource type="CylinderMesh" id="Mesh_tail"]
|
|
||||||
material = SubResource("Material_wool")
|
|
||||||
top_radius = 0.07
|
|
||||||
bottom_radius = 0.12
|
|
||||||
height = 0.42
|
|
||||||
radial_segments = 8
|
|
||||||
|
|
||||||
[sub_resource type="TorusMesh" id="Mesh_collar"]
|
|
||||||
material = SubResource("Material_collar")
|
|
||||||
inner_radius = 0.27
|
|
||||||
outer_radius = 0.32
|
|
||||||
rings = 14
|
|
||||||
ring_segments = 8
|
|
||||||
|
|
||||||
[sub_resource type="TorusMesh" id="Mesh_hunger_bowl"]
|
|
||||||
material = SubResource("Material_hunger")
|
|
||||||
inner_radius = 0.18
|
|
||||||
outer_radius = 0.27
|
|
||||||
rings = 14
|
|
||||||
ring_segments = 8
|
|
||||||
|
|
||||||
[sub_resource type="SphereMesh" id="Mesh_hunger_food"]
|
|
||||||
material = SubResource("Material_hunger")
|
|
||||||
radius = 0.09
|
|
||||||
height = 0.14
|
|
||||||
radial_segments = 8
|
|
||||||
rings = 4
|
|
||||||
|
|
||||||
[sub_resource type="TorusMesh" id="Mesh_fed_ring"]
|
|
||||||
material = SubResource("Material_fed")
|
|
||||||
inner_radius = 0.32
|
|
||||||
outer_radius = 0.42
|
|
||||||
rings = 16
|
|
||||||
ring_segments = 8
|
|
||||||
|
|
||||||
[sub_resource type="SphereMesh" id="Mesh_fed_mote"]
|
|
||||||
material = SubResource("Material_fed")
|
|
||||||
radius = 0.075
|
|
||||||
height = 0.13
|
|
||||||
radial_segments = 8
|
|
||||||
rings = 4
|
|
||||||
|
|
||||||
[node name="CozyGoat" type="Node3D"]
|
|
||||||
script = ExtResource("1_animal")
|
|
||||||
animal_id = &"goat_dunja"
|
animal_id = &"goat_dunja"
|
||||||
display_name = "Dunja"
|
display_name = "Dunja"
|
||||||
species_id = &"goat"
|
species_id = &"goat"
|
||||||
initial_hunger = 40.0
|
initial_hunger = 40.0
|
||||||
initial_routine_site_id = &"dunja_shelter"
|
initial_routine_site_id = &"dunja_shelter"
|
||||||
initial_next_routine_tick = 8
|
initial_next_routine_tick = 8
|
||||||
|
|
||||||
[node name="Visual" type="Node3D" parent="."]
|
|
||||||
script = ExtResource("2_visual")
|
|
||||||
|
|
||||||
[node name="Body" type="MeshInstance3D" parent="Visual"]
|
|
||||||
position = Vector3(0, 0.95, 0)
|
|
||||||
rotation_degrees = Vector3(90, 0, 0)
|
|
||||||
mesh = SubResource("Mesh_body")
|
|
||||||
|
|
||||||
[node name="FluffFront" type="MeshInstance3D" parent="Visual"]
|
|
||||||
position = Vector3(0, 1.02, 0.42)
|
|
||||||
scale = Vector3(1.08, 1.05, 1)
|
|
||||||
mesh = SubResource("Mesh_fluff")
|
|
||||||
|
|
||||||
[node name="FluffBack" type="MeshInstance3D" parent="Visual"]
|
|
||||||
position = Vector3(0, 1.02, -0.42)
|
|
||||||
scale = Vector3(1.08, 1.05, 1)
|
|
||||||
mesh = SubResource("Mesh_fluff")
|
|
||||||
|
|
||||||
[node name="LegFrontLeft" type="MeshInstance3D" parent="Visual"]
|
|
||||||
position = Vector3(-0.3, 0.43, 0.48)
|
|
||||||
mesh = SubResource("Mesh_leg")
|
|
||||||
|
|
||||||
[node name="Hoof" type="MeshInstance3D" parent="Visual/LegFrontLeft"]
|
|
||||||
position = Vector3(0, -0.29, 0.035)
|
|
||||||
mesh = SubResource("Mesh_hoof")
|
|
||||||
|
|
||||||
[node name="LegFrontRight" type="MeshInstance3D" parent="Visual"]
|
|
||||||
position = Vector3(0.3, 0.43, 0.48)
|
|
||||||
mesh = SubResource("Mesh_leg")
|
|
||||||
|
|
||||||
[node name="Hoof" type="MeshInstance3D" parent="Visual/LegFrontRight"]
|
|
||||||
position = Vector3(0, -0.29, 0.035)
|
|
||||||
mesh = SubResource("Mesh_hoof")
|
|
||||||
|
|
||||||
[node name="LegBackLeft" type="MeshInstance3D" parent="Visual"]
|
|
||||||
position = Vector3(-0.3, 0.43, -0.48)
|
|
||||||
mesh = SubResource("Mesh_leg")
|
|
||||||
|
|
||||||
[node name="Hoof" type="MeshInstance3D" parent="Visual/LegBackLeft"]
|
|
||||||
position = Vector3(0, -0.29, 0.035)
|
|
||||||
mesh = SubResource("Mesh_hoof")
|
|
||||||
|
|
||||||
[node name="LegBackRight" type="MeshInstance3D" parent="Visual"]
|
|
||||||
position = Vector3(0.3, 0.43, -0.48)
|
|
||||||
mesh = SubResource("Mesh_leg")
|
|
||||||
|
|
||||||
[node name="Hoof" type="MeshInstance3D" parent="Visual/LegBackRight"]
|
|
||||||
position = Vector3(0, -0.29, 0.035)
|
|
||||||
mesh = SubResource("Mesh_hoof")
|
|
||||||
|
|
||||||
[node name="HeadRoot" type="Node3D" parent="Visual"]
|
|
||||||
position = Vector3(0, 1.28, 0.88)
|
|
||||||
rotation_degrees = Vector3(-3, 0, 0)
|
|
||||||
|
|
||||||
[node name="Head" type="MeshInstance3D" parent="Visual/HeadRoot"]
|
|
||||||
mesh = SubResource("Mesh_head")
|
|
||||||
|
|
||||||
[node name="Snout" type="MeshInstance3D" parent="Visual/HeadRoot"]
|
|
||||||
position = Vector3(0, -0.08, 0.31)
|
|
||||||
scale = Vector3(1.05, 0.72, 0.9)
|
|
||||||
mesh = SubResource("Mesh_snout")
|
|
||||||
|
|
||||||
[node name="EyeLeft" type="MeshInstance3D" parent="Visual/HeadRoot"]
|
|
||||||
position = Vector3(-0.18, 0.08, 0.285)
|
|
||||||
mesh = SubResource("Mesh_eye")
|
|
||||||
|
|
||||||
[node name="EyeRight" type="MeshInstance3D" parent="Visual/HeadRoot"]
|
|
||||||
position = Vector3(0.18, 0.08, 0.285)
|
|
||||||
mesh = SubResource("Mesh_eye")
|
|
||||||
|
|
||||||
[node name="EarLeft" type="MeshInstance3D" parent="Visual/HeadRoot"]
|
|
||||||
position = Vector3(-0.35, 0.13, 0)
|
|
||||||
rotation_degrees = Vector3(8, 0, 67)
|
|
||||||
scale = Vector3(0.82, 0.28, 1)
|
|
||||||
mesh = SubResource("Mesh_ear")
|
|
||||||
|
|
||||||
[node name="EarRight" type="MeshInstance3D" parent="Visual/HeadRoot"]
|
|
||||||
position = Vector3(0.35, 0.13, 0)
|
|
||||||
rotation_degrees = Vector3(8, 0, -67)
|
|
||||||
scale = Vector3(0.82, 0.28, 1)
|
|
||||||
mesh = SubResource("Mesh_ear")
|
|
||||||
|
|
||||||
[node name="HornLeft" type="MeshInstance3D" parent="Visual/HeadRoot"]
|
|
||||||
position = Vector3(-0.17, 0.34, -0.06)
|
|
||||||
rotation_degrees = Vector3(-24, 0, -20)
|
|
||||||
mesh = SubResource("Mesh_horn")
|
|
||||||
|
|
||||||
[node name="HornRight" type="MeshInstance3D" parent="Visual/HeadRoot"]
|
|
||||||
position = Vector3(0.17, 0.34, -0.06)
|
|
||||||
rotation_degrees = Vector3(-24, 0, 20)
|
|
||||||
mesh = SubResource("Mesh_horn")
|
|
||||||
|
|
||||||
[node name="Beard" type="MeshInstance3D" parent="Visual/HeadRoot"]
|
|
||||||
position = Vector3(0, -0.35, 0.08)
|
|
||||||
mesh = SubResource("Mesh_beard")
|
|
||||||
|
|
||||||
[node name="Collar" type="MeshInstance3D" parent="Visual/HeadRoot"]
|
|
||||||
position = Vector3(0, -0.31, -0.16)
|
|
||||||
rotation_degrees = Vector3(90, 0, 0)
|
|
||||||
scale = Vector3(0.85, 0.85, 0.85)
|
|
||||||
mesh = SubResource("Mesh_collar")
|
|
||||||
|
|
||||||
[node name="TailRoot" type="Node3D" parent="Visual"]
|
|
||||||
position = Vector3(0, 1.16, -0.92)
|
|
||||||
rotation_degrees = Vector3(60, 0, 0)
|
|
||||||
|
|
||||||
[node name="Tail" type="MeshInstance3D" parent="Visual/TailRoot"]
|
|
||||||
position = Vector3(0, 0.18, 0)
|
|
||||||
mesh = SubResource("Mesh_tail")
|
|
||||||
|
|
||||||
[node name="HungerCueRoot" type="Node3D" parent="Visual"]
|
|
||||||
position = Vector3(0, 2.05, 0.18)
|
|
||||||
|
|
||||||
[node name="EmptyBowl" type="MeshInstance3D" parent="Visual/HungerCueRoot"]
|
|
||||||
rotation_degrees = Vector3(72, 0, 0)
|
|
||||||
scale = Vector3(1.1, 0.72, 1.1)
|
|
||||||
mesh = SubResource("Mesh_hunger_bowl")
|
|
||||||
|
|
||||||
[node name="FoodMoteLeft" type="MeshInstance3D" parent="Visual/HungerCueRoot"]
|
|
||||||
position = Vector3(-0.13, 0.22, 0)
|
|
||||||
mesh = SubResource("Mesh_hunger_food")
|
|
||||||
|
|
||||||
[node name="FoodMoteRight" type="MeshInstance3D" parent="Visual/HungerCueRoot"]
|
|
||||||
position = Vector3(0.13, 0.29, 0)
|
|
||||||
scale = Vector3(0.82, 0.82, 0.82)
|
|
||||||
mesh = SubResource("Mesh_hunger_food")
|
|
||||||
|
|
||||||
[node name="FedResponseRoot" type="Node3D" parent="Visual"]
|
|
||||||
visible = false
|
|
||||||
position = Vector3(0, 1.8, 0.12)
|
|
||||||
scale = Vector3(0.2, 0.2, 0.2)
|
|
||||||
|
|
||||||
[node name="Halo" type="MeshInstance3D" parent="Visual/FedResponseRoot"]
|
|
||||||
rotation_degrees = Vector3(74, 0, 0)
|
|
||||||
mesh = SubResource("Mesh_fed_ring")
|
|
||||||
|
|
||||||
[node name="MoteLeft" type="MeshInstance3D" parent="Visual/FedResponseRoot"]
|
|
||||||
position = Vector3(-0.34, 0.18, 0)
|
|
||||||
mesh = SubResource("Mesh_fed_mote")
|
|
||||||
|
|
||||||
[node name="MoteRight" type="MeshInstance3D" parent="Visual/FedResponseRoot"]
|
|
||||||
position = Vector3(0.34, 0.28, 0)
|
|
||||||
scale = Vector3(0.82, 0.82, 0.82)
|
|
||||||
mesh = SubResource("Mesh_fed_mote")
|
|
||||||
|
|
||||||
[node name="InteractionPoint" type="Marker3D" parent="."]
|
|
||||||
position = Vector3(0, 0, 2.05)
|
|
||||||
|
|
||||||
[node name="DebugLabel" type="Label3D" parent="."]
|
|
||||||
position = Vector3(0, 2.55, 0)
|
|
||||||
billboard = 1
|
|
||||||
no_depth_test = true
|
|
||||||
font_size = 22
|
|
||||||
outline_size = 5
|
|
||||||
text = "Dunja"
|
|
||||||
modulate = Color(1, 0.94, 0.78, 1)
|
|
||||||
pixel_size = 0.006
|
|
||||||
|
|||||||
@@ -1,90 +1 @@
|
|||||||
extends Node3D
|
extends "res://world/animals/grazer/cozy_grazer_visual.gd"
|
||||||
|
|
||||||
const HUNGER_CUE_BASE_Y := 2.05
|
|
||||||
const FED_RESPONSE_BASE_Y := 1.8
|
|
||||||
|
|
||||||
@onready var head_root: Node3D = $HeadRoot
|
|
||||||
@onready var tail_root: Node3D = $TailRoot
|
|
||||||
@onready var hunger_cue_root: Node3D = $HungerCueRoot
|
|
||||||
@onready var fed_response_root: Node3D = $FedResponseRoot
|
|
||||||
@onready var leg_front_left: Node3D = $LegFrontLeft
|
|
||||||
@onready var leg_front_right: Node3D = $LegFrontRight
|
|
||||||
@onready var leg_back_left: Node3D = $LegBackLeft
|
|
||||||
@onready var leg_back_right: Node3D = $LegBackRight
|
|
||||||
|
|
||||||
var idle_phase := 0.0
|
|
||||||
var is_hungry := false
|
|
||||||
var is_moving := false
|
|
||||||
var fed_tween: Tween
|
|
||||||
var head_base_position: Vector3
|
|
||||||
var visual_base_position: Vector3
|
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
|
||||||
head_base_position = head_root.position
|
|
||||||
visual_base_position = position
|
|
||||||
reset_transient_feedback()
|
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
|
||||||
idle_phase = fmod(idle_phase + delta, TAU)
|
|
||||||
var breath := sin(idle_phase * 1.7) * 0.025
|
|
||||||
var hungry_drop := -0.12 if is_hungry else 0.0
|
|
||||||
var walking_bob := absf(sin(idle_phase * 5.4)) * 0.045 if is_moving else 0.0
|
|
||||||
position.y = visual_base_position.y + walking_bob
|
|
||||||
head_root.position.y = head_base_position.y + breath + hungry_drop
|
|
||||||
head_root.rotation_degrees.x = (
|
|
||||||
8.0 + sin(idle_phase * 1.2) * 2.0 if is_hungry else -3.0 + sin(idle_phase * 1.4) * 2.5
|
|
||||||
)
|
|
||||||
tail_root.rotation_degrees.z = sin(idle_phase * 3.1) * (4.0 if is_hungry else 13.0)
|
|
||||||
var stride := sin(idle_phase * 5.4) * 14.0 if is_moving else 0.0
|
|
||||||
leg_front_left.rotation_degrees.x = stride
|
|
||||||
leg_back_right.rotation_degrees.x = stride
|
|
||||||
leg_front_right.rotation_degrees.x = -stride
|
|
||||||
leg_back_left.rotation_degrees.x = -stride
|
|
||||||
if hunger_cue_root.visible:
|
|
||||||
hunger_cue_root.position.y = HUNGER_CUE_BASE_Y + sin(idle_phase * 2.2) * 0.07
|
|
||||||
|
|
||||||
|
|
||||||
func set_hungry(value: bool) -> void:
|
|
||||||
is_hungry = value
|
|
||||||
hunger_cue_root.visible = value
|
|
||||||
|
|
||||||
|
|
||||||
func set_moving(value: bool) -> void:
|
|
||||||
is_moving = value
|
|
||||||
if not value:
|
|
||||||
position = visual_base_position
|
|
||||||
leg_front_left.rotation_degrees.x = 0.0
|
|
||||||
leg_front_right.rotation_degrees.x = 0.0
|
|
||||||
leg_back_left.rotation_degrees.x = 0.0
|
|
||||||
leg_back_right.rotation_degrees.x = 0.0
|
|
||||||
|
|
||||||
|
|
||||||
func reset_transient_feedback() -> void:
|
|
||||||
if fed_tween != null:
|
|
||||||
fed_tween.kill()
|
|
||||||
fed_tween = null
|
|
||||||
fed_response_root.visible = false
|
|
||||||
fed_response_root.position.y = FED_RESPONSE_BASE_Y
|
|
||||||
fed_response_root.scale = Vector3(0.2, 0.2, 0.2)
|
|
||||||
|
|
||||||
|
|
||||||
func play_fed_response() -> void:
|
|
||||||
reset_transient_feedback()
|
|
||||||
fed_response_root.visible = true
|
|
||||||
fed_tween = create_tween()
|
|
||||||
fed_tween.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
|
|
||||||
fed_tween.tween_property(fed_response_root, "scale", Vector3.ONE, 0.34)
|
|
||||||
fed_tween.parallel().tween_property(
|
|
||||||
fed_response_root, "position:y", FED_RESPONSE_BASE_Y + 0.22, 0.34
|
|
||||||
)
|
|
||||||
fed_tween.set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN)
|
|
||||||
fed_tween.tween_interval(0.7)
|
|
||||||
fed_tween.tween_property(fed_response_root, "scale", Vector3(0.2, 0.2, 0.2), 0.42)
|
|
||||||
fed_tween.finished.connect(_on_fed_response_finished)
|
|
||||||
|
|
||||||
|
|
||||||
func _on_fed_response_finished() -> void:
|
|
||||||
fed_response_root.visible = false
|
|
||||||
fed_tween = null
|
|
||||||
|
|||||||
@@ -0,0 +1,311 @@
|
|||||||
|
[gd_scene load_steps=24 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://world/animals/animal_node.gd" id="1_animal"]
|
||||||
|
[ext_resource type="Script" path="res://world/animals/grazer/cozy_grazer_visual.gd" id="2_visual"]
|
||||||
|
|
||||||
|
[sub_resource type="StandardMaterial3D" id="Material_wool"]
|
||||||
|
albedo_color = Color(0.88, 0.82, 0.67, 1)
|
||||||
|
roughness = 0.96
|
||||||
|
|
||||||
|
[sub_resource type="StandardMaterial3D" id="Material_face"]
|
||||||
|
albedo_color = Color(0.58, 0.43, 0.29, 1)
|
||||||
|
roughness = 0.92
|
||||||
|
|
||||||
|
[sub_resource type="StandardMaterial3D" id="Material_dark"]
|
||||||
|
albedo_color = Color(0.13, 0.09, 0.065, 1)
|
||||||
|
roughness = 0.9
|
||||||
|
|
||||||
|
[sub_resource type="StandardMaterial3D" id="Material_horn"]
|
||||||
|
albedo_color = Color(0.78, 0.68, 0.5, 1)
|
||||||
|
roughness = 0.86
|
||||||
|
|
||||||
|
[sub_resource type="StandardMaterial3D" id="Material_collar"]
|
||||||
|
albedo_color = Color(0.12, 0.5, 0.5, 1)
|
||||||
|
roughness = 0.72
|
||||||
|
|
||||||
|
[sub_resource type="StandardMaterial3D" id="Material_hunger"]
|
||||||
|
transparency = 1
|
||||||
|
shading_mode = 0
|
||||||
|
albedo_color = Color(0.98, 0.62, 0.22, 0.9)
|
||||||
|
emission_enabled = true
|
||||||
|
emission = Color(0.92, 0.36, 0.08, 1)
|
||||||
|
emission_energy_multiplier = 0.32
|
||||||
|
|
||||||
|
[sub_resource type="StandardMaterial3D" id="Material_fed"]
|
||||||
|
transparency = 1
|
||||||
|
shading_mode = 0
|
||||||
|
albedo_color = Color(0.72, 0.92, 0.5, 0.92)
|
||||||
|
emission_enabled = true
|
||||||
|
emission = Color(0.45, 0.78, 0.28, 1)
|
||||||
|
emission_energy_multiplier = 0.38
|
||||||
|
|
||||||
|
[sub_resource type="CapsuleMesh" id="Mesh_body"]
|
||||||
|
material = SubResource("Material_wool")
|
||||||
|
radius = 0.48
|
||||||
|
height = 1.55
|
||||||
|
radial_segments = 12
|
||||||
|
rings = 4
|
||||||
|
|
||||||
|
[sub_resource type="SphereMesh" id="Mesh_fluff"]
|
||||||
|
material = SubResource("Material_wool")
|
||||||
|
radius = 0.42
|
||||||
|
height = 0.72
|
||||||
|
radial_segments = 12
|
||||||
|
rings = 6
|
||||||
|
|
||||||
|
[sub_resource type="CapsuleMesh" id="Mesh_leg"]
|
||||||
|
material = SubResource("Material_face")
|
||||||
|
radius = 0.105
|
||||||
|
height = 0.62
|
||||||
|
radial_segments = 8
|
||||||
|
rings = 3
|
||||||
|
|
||||||
|
[sub_resource type="BoxMesh" id="Mesh_hoof"]
|
||||||
|
material = SubResource("Material_dark")
|
||||||
|
size = Vector3(0.2, 0.13, 0.28)
|
||||||
|
|
||||||
|
[sub_resource type="SphereMesh" id="Mesh_head"]
|
||||||
|
material = SubResource("Material_face")
|
||||||
|
radius = 0.34
|
||||||
|
height = 0.58
|
||||||
|
radial_segments = 12
|
||||||
|
rings = 6
|
||||||
|
|
||||||
|
[sub_resource type="SphereMesh" id="Mesh_snout"]
|
||||||
|
material = SubResource("Material_wool")
|
||||||
|
radius = 0.21
|
||||||
|
height = 0.32
|
||||||
|
radial_segments = 10
|
||||||
|
rings = 5
|
||||||
|
|
||||||
|
[sub_resource type="SphereMesh" id="Mesh_ear"]
|
||||||
|
material = SubResource("Material_face")
|
||||||
|
radius = 0.24
|
||||||
|
height = 0.42
|
||||||
|
radial_segments = 10
|
||||||
|
rings = 5
|
||||||
|
|
||||||
|
[sub_resource type="SphereMesh" id="Mesh_eye"]
|
||||||
|
material = SubResource("Material_dark")
|
||||||
|
radius = 0.045
|
||||||
|
height = 0.075
|
||||||
|
radial_segments = 8
|
||||||
|
rings = 4
|
||||||
|
|
||||||
|
[sub_resource type="CylinderMesh" id="Mesh_horn"]
|
||||||
|
material = SubResource("Material_horn")
|
||||||
|
top_radius = 0.035
|
||||||
|
bottom_radius = 0.09
|
||||||
|
height = 0.42
|
||||||
|
radial_segments = 8
|
||||||
|
|
||||||
|
[sub_resource type="CylinderMesh" id="Mesh_beard"]
|
||||||
|
material = SubResource("Material_wool")
|
||||||
|
top_radius = 0.015
|
||||||
|
bottom_radius = 0.11
|
||||||
|
height = 0.34
|
||||||
|
radial_segments = 8
|
||||||
|
|
||||||
|
[sub_resource type="CylinderMesh" id="Mesh_tail"]
|
||||||
|
material = SubResource("Material_wool")
|
||||||
|
top_radius = 0.07
|
||||||
|
bottom_radius = 0.12
|
||||||
|
height = 0.42
|
||||||
|
radial_segments = 8
|
||||||
|
|
||||||
|
[sub_resource type="TorusMesh" id="Mesh_collar"]
|
||||||
|
material = SubResource("Material_collar")
|
||||||
|
inner_radius = 0.27
|
||||||
|
outer_radius = 0.32
|
||||||
|
rings = 14
|
||||||
|
ring_segments = 8
|
||||||
|
|
||||||
|
[sub_resource type="TorusMesh" id="Mesh_hunger_bowl"]
|
||||||
|
material = SubResource("Material_hunger")
|
||||||
|
inner_radius = 0.18
|
||||||
|
outer_radius = 0.27
|
||||||
|
rings = 14
|
||||||
|
ring_segments = 8
|
||||||
|
|
||||||
|
[sub_resource type="SphereMesh" id="Mesh_hunger_food"]
|
||||||
|
material = SubResource("Material_hunger")
|
||||||
|
radius = 0.09
|
||||||
|
height = 0.14
|
||||||
|
radial_segments = 8
|
||||||
|
rings = 4
|
||||||
|
|
||||||
|
[sub_resource type="TorusMesh" id="Mesh_fed_ring"]
|
||||||
|
material = SubResource("Material_fed")
|
||||||
|
inner_radius = 0.32
|
||||||
|
outer_radius = 0.42
|
||||||
|
rings = 16
|
||||||
|
ring_segments = 8
|
||||||
|
|
||||||
|
[sub_resource type="SphereMesh" id="Mesh_fed_mote"]
|
||||||
|
material = SubResource("Material_fed")
|
||||||
|
radius = 0.075
|
||||||
|
height = 0.13
|
||||||
|
radial_segments = 8
|
||||||
|
rings = 4
|
||||||
|
|
||||||
|
[node name="CozyGrazer" type="Node3D"]
|
||||||
|
script = ExtResource("1_animal")
|
||||||
|
display_name = "Animal"
|
||||||
|
|
||||||
|
[node name="Visual" type="Node3D" parent="."]
|
||||||
|
script = ExtResource("2_visual")
|
||||||
|
|
||||||
|
[node name="Body" type="MeshInstance3D" parent="Visual"]
|
||||||
|
position = Vector3(0, 0.95, 0)
|
||||||
|
rotation_degrees = Vector3(90, 0, 0)
|
||||||
|
mesh = SubResource("Mesh_body")
|
||||||
|
|
||||||
|
[node name="FluffFront" type="MeshInstance3D" parent="Visual"]
|
||||||
|
position = Vector3(0, 1.02, 0.42)
|
||||||
|
scale = Vector3(1.08, 1.05, 1)
|
||||||
|
mesh = SubResource("Mesh_fluff")
|
||||||
|
|
||||||
|
[node name="FluffBack" type="MeshInstance3D" parent="Visual"]
|
||||||
|
position = Vector3(0, 1.02, -0.42)
|
||||||
|
scale = Vector3(1.08, 1.05, 1)
|
||||||
|
mesh = SubResource("Mesh_fluff")
|
||||||
|
|
||||||
|
[node name="LegFrontLeft" type="MeshInstance3D" parent="Visual"]
|
||||||
|
position = Vector3(-0.3, 0.43, 0.48)
|
||||||
|
mesh = SubResource("Mesh_leg")
|
||||||
|
|
||||||
|
[node name="Hoof" type="MeshInstance3D" parent="Visual/LegFrontLeft"]
|
||||||
|
position = Vector3(0, -0.29, 0.035)
|
||||||
|
mesh = SubResource("Mesh_hoof")
|
||||||
|
|
||||||
|
[node name="LegFrontRight" type="MeshInstance3D" parent="Visual"]
|
||||||
|
position = Vector3(0.3, 0.43, 0.48)
|
||||||
|
mesh = SubResource("Mesh_leg")
|
||||||
|
|
||||||
|
[node name="Hoof" type="MeshInstance3D" parent="Visual/LegFrontRight"]
|
||||||
|
position = Vector3(0, -0.29, 0.035)
|
||||||
|
mesh = SubResource("Mesh_hoof")
|
||||||
|
|
||||||
|
[node name="LegBackLeft" type="MeshInstance3D" parent="Visual"]
|
||||||
|
position = Vector3(-0.3, 0.43, -0.48)
|
||||||
|
mesh = SubResource("Mesh_leg")
|
||||||
|
|
||||||
|
[node name="Hoof" type="MeshInstance3D" parent="Visual/LegBackLeft"]
|
||||||
|
position = Vector3(0, -0.29, 0.035)
|
||||||
|
mesh = SubResource("Mesh_hoof")
|
||||||
|
|
||||||
|
[node name="LegBackRight" type="MeshInstance3D" parent="Visual"]
|
||||||
|
position = Vector3(0.3, 0.43, -0.48)
|
||||||
|
mesh = SubResource("Mesh_leg")
|
||||||
|
|
||||||
|
[node name="Hoof" type="MeshInstance3D" parent="Visual/LegBackRight"]
|
||||||
|
position = Vector3(0, -0.29, 0.035)
|
||||||
|
mesh = SubResource("Mesh_hoof")
|
||||||
|
|
||||||
|
[node name="HeadRoot" type="Node3D" parent="Visual"]
|
||||||
|
position = Vector3(0, 1.28, 0.88)
|
||||||
|
rotation_degrees = Vector3(-3, 0, 0)
|
||||||
|
|
||||||
|
[node name="Head" type="MeshInstance3D" parent="Visual/HeadRoot"]
|
||||||
|
mesh = SubResource("Mesh_head")
|
||||||
|
|
||||||
|
[node name="Snout" type="MeshInstance3D" parent="Visual/HeadRoot"]
|
||||||
|
position = Vector3(0, -0.08, 0.31)
|
||||||
|
scale = Vector3(1.05, 0.72, 0.9)
|
||||||
|
mesh = SubResource("Mesh_snout")
|
||||||
|
|
||||||
|
[node name="EyeLeft" type="MeshInstance3D" parent="Visual/HeadRoot"]
|
||||||
|
position = Vector3(-0.18, 0.08, 0.285)
|
||||||
|
mesh = SubResource("Mesh_eye")
|
||||||
|
|
||||||
|
[node name="EyeRight" type="MeshInstance3D" parent="Visual/HeadRoot"]
|
||||||
|
position = Vector3(0.18, 0.08, 0.285)
|
||||||
|
mesh = SubResource("Mesh_eye")
|
||||||
|
|
||||||
|
[node name="EarLeft" type="MeshInstance3D" parent="Visual/HeadRoot"]
|
||||||
|
position = Vector3(-0.35, 0.13, 0)
|
||||||
|
rotation_degrees = Vector3(8, 0, 67)
|
||||||
|
scale = Vector3(0.82, 0.28, 1)
|
||||||
|
mesh = SubResource("Mesh_ear")
|
||||||
|
|
||||||
|
[node name="EarRight" type="MeshInstance3D" parent="Visual/HeadRoot"]
|
||||||
|
position = Vector3(0.35, 0.13, 0)
|
||||||
|
rotation_degrees = Vector3(8, 0, -67)
|
||||||
|
scale = Vector3(0.82, 0.28, 1)
|
||||||
|
mesh = SubResource("Mesh_ear")
|
||||||
|
|
||||||
|
[node name="HornLeft" type="MeshInstance3D" parent="Visual/HeadRoot"]
|
||||||
|
position = Vector3(-0.17, 0.34, -0.06)
|
||||||
|
rotation_degrees = Vector3(-24, 0, -20)
|
||||||
|
mesh = SubResource("Mesh_horn")
|
||||||
|
|
||||||
|
[node name="HornRight" type="MeshInstance3D" parent="Visual/HeadRoot"]
|
||||||
|
position = Vector3(0.17, 0.34, -0.06)
|
||||||
|
rotation_degrees = Vector3(-24, 0, 20)
|
||||||
|
mesh = SubResource("Mesh_horn")
|
||||||
|
|
||||||
|
[node name="Beard" type="MeshInstance3D" parent="Visual/HeadRoot"]
|
||||||
|
position = Vector3(0, -0.35, 0.08)
|
||||||
|
mesh = SubResource("Mesh_beard")
|
||||||
|
|
||||||
|
[node name="Collar" type="MeshInstance3D" parent="Visual/HeadRoot"]
|
||||||
|
position = Vector3(0, -0.31, -0.16)
|
||||||
|
rotation_degrees = Vector3(90, 0, 0)
|
||||||
|
scale = Vector3(0.85, 0.85, 0.85)
|
||||||
|
mesh = SubResource("Mesh_collar")
|
||||||
|
|
||||||
|
[node name="TailRoot" type="Node3D" parent="Visual"]
|
||||||
|
position = Vector3(0, 1.16, -0.92)
|
||||||
|
rotation_degrees = Vector3(60, 0, 0)
|
||||||
|
|
||||||
|
[node name="Tail" type="MeshInstance3D" parent="Visual/TailRoot"]
|
||||||
|
position = Vector3(0, 0.18, 0)
|
||||||
|
mesh = SubResource("Mesh_tail")
|
||||||
|
|
||||||
|
[node name="HungerCueRoot" type="Node3D" parent="Visual"]
|
||||||
|
position = Vector3(0, 2.05, 0.18)
|
||||||
|
|
||||||
|
[node name="EmptyBowl" type="MeshInstance3D" parent="Visual/HungerCueRoot"]
|
||||||
|
rotation_degrees = Vector3(72, 0, 0)
|
||||||
|
scale = Vector3(1.1, 0.72, 1.1)
|
||||||
|
mesh = SubResource("Mesh_hunger_bowl")
|
||||||
|
|
||||||
|
[node name="FoodMoteLeft" type="MeshInstance3D" parent="Visual/HungerCueRoot"]
|
||||||
|
position = Vector3(-0.13, 0.22, 0)
|
||||||
|
mesh = SubResource("Mesh_hunger_food")
|
||||||
|
|
||||||
|
[node name="FoodMoteRight" type="MeshInstance3D" parent="Visual/HungerCueRoot"]
|
||||||
|
position = Vector3(0.13, 0.29, 0)
|
||||||
|
scale = Vector3(0.82, 0.82, 0.82)
|
||||||
|
mesh = SubResource("Mesh_hunger_food")
|
||||||
|
|
||||||
|
[node name="FedResponseRoot" type="Node3D" parent="Visual"]
|
||||||
|
visible = false
|
||||||
|
position = Vector3(0, 1.8, 0.12)
|
||||||
|
scale = Vector3(0.2, 0.2, 0.2)
|
||||||
|
|
||||||
|
[node name="Halo" type="MeshInstance3D" parent="Visual/FedResponseRoot"]
|
||||||
|
rotation_degrees = Vector3(74, 0, 0)
|
||||||
|
mesh = SubResource("Mesh_fed_ring")
|
||||||
|
|
||||||
|
[node name="MoteLeft" type="MeshInstance3D" parent="Visual/FedResponseRoot"]
|
||||||
|
position = Vector3(-0.34, 0.18, 0)
|
||||||
|
mesh = SubResource("Mesh_fed_mote")
|
||||||
|
|
||||||
|
[node name="MoteRight" type="MeshInstance3D" parent="Visual/FedResponseRoot"]
|
||||||
|
position = Vector3(0.34, 0.28, 0)
|
||||||
|
scale = Vector3(0.82, 0.82, 0.82)
|
||||||
|
mesh = SubResource("Mesh_fed_mote")
|
||||||
|
|
||||||
|
[node name="InteractionPoint" type="Marker3D" parent="."]
|
||||||
|
position = Vector3(0, 0, 2.05)
|
||||||
|
|
||||||
|
[node name="DebugLabel" type="Label3D" parent="."]
|
||||||
|
position = Vector3(0, 2.55, 0)
|
||||||
|
billboard = 1
|
||||||
|
no_depth_test = true
|
||||||
|
font_size = 22
|
||||||
|
outline_size = 5
|
||||||
|
text = "Animal"
|
||||||
|
modulate = Color(1, 0.94, 0.78, 1)
|
||||||
|
pixel_size = 0.006
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
extends Node3D
|
||||||
|
|
||||||
|
const HUNGER_CUE_BASE_Y := 2.05
|
||||||
|
const FED_RESPONSE_BASE_Y := 1.8
|
||||||
|
|
||||||
|
@onready var head_root: Node3D = $HeadRoot
|
||||||
|
@onready var tail_root: Node3D = $TailRoot
|
||||||
|
@onready var hunger_cue_root: Node3D = $HungerCueRoot
|
||||||
|
@onready var fed_response_root: Node3D = $FedResponseRoot
|
||||||
|
@onready var leg_front_left: Node3D = $LegFrontLeft
|
||||||
|
@onready var leg_front_right: Node3D = $LegFrontRight
|
||||||
|
@onready var leg_back_left: Node3D = $LegBackLeft
|
||||||
|
@onready var leg_back_right: Node3D = $LegBackRight
|
||||||
|
@onready var body: Node3D = $Body
|
||||||
|
@onready var fluff_front: Node3D = $FluffFront
|
||||||
|
@onready var fluff_back: Node3D = $FluffBack
|
||||||
|
@onready var horn_left: Node3D = $HeadRoot/HornLeft
|
||||||
|
@onready var horn_right: Node3D = $HeadRoot/HornRight
|
||||||
|
@onready var beard: Node3D = $HeadRoot/Beard
|
||||||
|
|
||||||
|
var idle_phase := 0.0
|
||||||
|
var is_hungry := false
|
||||||
|
var is_moving := false
|
||||||
|
var fed_tween: Tween
|
||||||
|
var head_base_position: Vector3
|
||||||
|
var visual_base_position: Vector3
|
||||||
|
var body_base_scale: Vector3
|
||||||
|
var fluff_front_base_scale: Vector3
|
||||||
|
var fluff_back_base_scale: Vector3
|
||||||
|
var _animal_definition: AnimalDefinition
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
head_base_position = head_root.position
|
||||||
|
visual_base_position = position
|
||||||
|
body_base_scale = body.scale
|
||||||
|
fluff_front_base_scale = fluff_front.scale
|
||||||
|
fluff_back_base_scale = fluff_back.scale
|
||||||
|
_apply_definition_presentation()
|
||||||
|
reset_transient_feedback()
|
||||||
|
|
||||||
|
|
||||||
|
func apply_animal_definition(definition: AnimalDefinition) -> bool:
|
||||||
|
if definition == null or not definition.validate().is_empty():
|
||||||
|
return false
|
||||||
|
_animal_definition = definition
|
||||||
|
if is_node_ready():
|
||||||
|
_apply_definition_presentation()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_definition_presentation() -> void:
|
||||||
|
if _animal_definition == null:
|
||||||
|
return
|
||||||
|
scale = _animal_definition.visual_scale
|
||||||
|
body.scale = body_base_scale * _animal_definition.body_scale
|
||||||
|
fluff_front.scale = fluff_front_base_scale * _animal_definition.fluff_scale
|
||||||
|
fluff_back.scale = fluff_back_base_scale * _animal_definition.fluff_scale
|
||||||
|
horn_left.visible = _animal_definition.show_horns
|
||||||
|
horn_right.visible = _animal_definition.show_horns
|
||||||
|
beard.visible = _animal_definition.show_beard
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
idle_phase = fmod(idle_phase + delta, TAU)
|
||||||
|
var breath := sin(idle_phase * 1.7) * 0.025
|
||||||
|
var hungry_drop := -0.12 if is_hungry else 0.0
|
||||||
|
var walking_bob := absf(sin(idle_phase * 5.4)) * 0.045 if is_moving else 0.0
|
||||||
|
position.y = visual_base_position.y + walking_bob
|
||||||
|
head_root.position.y = head_base_position.y + breath + hungry_drop
|
||||||
|
head_root.rotation_degrees.x = (
|
||||||
|
8.0 + sin(idle_phase * 1.2) * 2.0 if is_hungry else -3.0 + sin(idle_phase * 1.4) * 2.5
|
||||||
|
)
|
||||||
|
tail_root.rotation_degrees.z = sin(idle_phase * 3.1) * (4.0 if is_hungry else 13.0)
|
||||||
|
var stride := sin(idle_phase * 5.4) * 14.0 if is_moving else 0.0
|
||||||
|
leg_front_left.rotation_degrees.x = stride
|
||||||
|
leg_back_right.rotation_degrees.x = stride
|
||||||
|
leg_front_right.rotation_degrees.x = -stride
|
||||||
|
leg_back_left.rotation_degrees.x = -stride
|
||||||
|
if hunger_cue_root.visible:
|
||||||
|
hunger_cue_root.position.y = HUNGER_CUE_BASE_Y + sin(idle_phase * 2.2) * 0.07
|
||||||
|
|
||||||
|
|
||||||
|
func set_hungry(value: bool) -> void:
|
||||||
|
is_hungry = value
|
||||||
|
hunger_cue_root.visible = value
|
||||||
|
|
||||||
|
|
||||||
|
func set_moving(value: bool) -> void:
|
||||||
|
is_moving = value
|
||||||
|
if not value:
|
||||||
|
position = visual_base_position
|
||||||
|
leg_front_left.rotation_degrees.x = 0.0
|
||||||
|
leg_front_right.rotation_degrees.x = 0.0
|
||||||
|
leg_back_left.rotation_degrees.x = 0.0
|
||||||
|
leg_back_right.rotation_degrees.x = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func reset_transient_feedback() -> void:
|
||||||
|
if fed_tween != null:
|
||||||
|
fed_tween.kill()
|
||||||
|
fed_tween = null
|
||||||
|
fed_response_root.visible = false
|
||||||
|
fed_response_root.position.y = FED_RESPONSE_BASE_Y
|
||||||
|
fed_response_root.scale = Vector3(0.2, 0.2, 0.2)
|
||||||
|
|
||||||
|
|
||||||
|
func play_fed_response() -> void:
|
||||||
|
reset_transient_feedback()
|
||||||
|
fed_response_root.visible = true
|
||||||
|
fed_tween = create_tween()
|
||||||
|
fed_tween.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
|
||||||
|
fed_tween.tween_property(fed_response_root, "scale", Vector3.ONE, 0.34)
|
||||||
|
fed_tween.parallel().tween_property(
|
||||||
|
fed_response_root, "position:y", FED_RESPONSE_BASE_Y + 0.22, 0.34
|
||||||
|
)
|
||||||
|
fed_tween.set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN)
|
||||||
|
fed_tween.tween_interval(0.7)
|
||||||
|
fed_tween.tween_property(fed_response_root, "scale", Vector3(0.2, 0.2, 0.2), 0.42)
|
||||||
|
fed_tween.finished.connect(_on_fed_response_finished)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_fed_response_finished() -> void:
|
||||||
|
fed_response_root.visible = false
|
||||||
|
fed_tween = null
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://ckytaadvxm4sh
|
||||||
Reference in New Issue
Block a user