Commit
Theria
feat: give the heroes placeholder 3D models — one animal per kit
modified CHANGELOG.md
@@ -26,6 +26,14 @@ protocol version.
### Changed
- The heroes now wear placeholder 3D models — a distinct low-poly animal per kit (lion,
cheetah, hyena, snake, spider, chameleon) standing in for the species each shapeshifter
takes — in place of the capsule bodies. The models come from mixed sources at different
authored scales, so each is auto-scaled to a common on-field size, stood on the ground,
and washed in a translucent team colour, so squadmates read apart by species while teams
stay legible at a glance. Creeps and structures keep their primitives for now. Bundled
asset licenses are credited in [`CREDITS.md`](CREDITS.md). Presentation only — the
simulation and the netcode protocol are unchanged.
- The match now renders in 2.5D: a pitched, close camera follows your hero across the
field — heroes and creeps stand as shaded capsules and structures as boxes on a lit
ground, replacing the flat top-down dots. HP and resource bars and the human/animal
added CREDITS.md
@@ -0,0 +1,41 @@
<div align="center">
<img src="icon.svg" alt="Theria" width="72">
<h1>Credits</h1>
<p><i>Third-party assets bundled with the game, and their licenses.</i></p>
<p>
<a href="README.md"><b>README</b></a> ·
<a href="CHANGELOG.md"><b>Changelog</b></a> ·
<a href="LICENSE"><b>License</b></a>
</p>
</div>
---
The game's own source is under the Apache License 2.0 (see [`LICENSE`](LICENSE)).
The bundled art assets below are the work of others, used under their own
licenses and credited here as those licenses require.
## Hero models
Placeholder low-poly animal models stand in for the shapeshifter heroes until
original art lands. Each was rescaled to a common on-field size and is drawn
under a translucent team-colour tint; otherwise the meshes and textures are
unchanged. All were obtained through [Poly Pizza](https://poly.pizza).
| Hero | Model | Author | License | Source |
| :--- | :--- | :--- | :--- | :--- |
| Lion | Lion | Poly by Google | [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/) | [poly.pizza](https://poly.pizza/m/3XAJojWxSWz) |
| Cheetah | Cheetah | Poly by Google | [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/) | [poly.pizza](https://poly.pizza/m/5y59KqZXxWf) |
| Hyena | Spotted Hyena | Poly by Google | [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/) | [poly.pizza](https://poly.pizza/m/0yU1LU3Nkpu) |
| Snake | Cobra | Poly by Google | [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/) | [poly.pizza](https://poly.pizza/m/40_5Xq467-U) |
| Spider | Spider | Quaternius | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) | [poly.pizza](https://poly.pizza/m/yRYJiAJyiM) |
| Chameleon | Lizard | madtrollstudio | [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/) | [poly.pizza](https://poly.pizza/m/0z3NJc5zAE) |
---
[Next: Changelog →](CHANGELOG.md)
modified README.md
@@ -78,6 +78,7 @@ delay adapts to the connection's measured jitter rather than being fixed.
| `src/client` | The connect menu, local input sampling, and rendering. |
| `test/unit` | Headless tests of the simulation and the wire protocol. |
| `scenes` | Godot scenes. |
| `assets` | Art assets — the placeholder hero models (see [`CREDITS.md`](CREDITS.md)). |
## Running
@@ -103,8 +104,9 @@ poke range and back off rather than melee — and all cast their own kits, heali
hurt and otherwise firing the reachable ability of their form. Cast its abilities with
**1–4**, aimed at the mouse cursor — the hero shifts between a human and an animal
form (shown by the ring around it, white or amber), each form a different set of
abilities drawing on its own resource (the bar under the health bar). Each hero wears a
distinct shade of its team colour, so your three squadmates read apart at a glance. Abilities are
abilities drawing on its own resource (the bar under the health bar). Each hero appears as
its own animal — a placeholder low-poly model washed in its team colour — so your three
squadmates read apart by species at a glance. Abilities are
cast in a single-machine or hosted match; a joined client moves but does not yet
cast.
@@ -155,7 +157,8 @@ Both run in continuous integration on every push and pull request.
## License
Apache License 2.0 — see [`LICENSE`](LICENSE).
Apache License 2.0 — see [`LICENSE`](LICENSE). Bundled third-party art assets carry
their own licenses, credited in [`CREDITS.md`](CREDITS.md).
---
added assets/models/heroes/chameleon.glb
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e9eab49ee18a382a020ee6e290707bfd069cc4b5be098f5a9ca6282b3c0a35d8
size 134512
added assets/models/heroes/chameleon.glb.import
@@ -0,0 +1,42 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://g6b1e1cncukj"
path="res://.godot/imported/chameleon.glb-e64aa73b7d6df37411b460654f882b52.scn"
[deps]
source_file="res://assets/models/heroes/chameleon.glb"
dest_files=["res://.godot/imported/chameleon.glb-e64aa73b7d6df37411b460654f882b52.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=3
added assets/models/heroes/cheetah.glb
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:070647b5c941d44e9e16cd7428bf5e2a0e9f069dc483b243513e052b6257f341
size 1760164
added assets/models/heroes/cheetah.glb.import
@@ -0,0 +1,42 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://bm3r4lem6vyqm"
path="res://.godot/imported/cheetah.glb-03c04cf6937e984b3e05765b0af730cf.scn"
[deps]
source_file="res://assets/models/heroes/cheetah.glb"
dest_files=["res://.godot/imported/cheetah.glb-03c04cf6937e984b3e05765b0af730cf.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=3
added assets/models/heroes/hyena.glb
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ccfb24283b882c80f050378c706080d89110cae52bfe88180165f633856459a1
size 1340016
added assets/models/heroes/hyena.glb.import
@@ -0,0 +1,42 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://by6v4bpha7nl5"
path="res://.godot/imported/hyena.glb-23b9e89513f2b80c8bba557720e3ff8a.scn"
[deps]
source_file="res://assets/models/heroes/hyena.glb"
dest_files=["res://.godot/imported/hyena.glb-23b9e89513f2b80c8bba557720e3ff8a.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=3
added assets/models/heroes/lion.glb
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e7e64a59768bf2251fd25b641fef1eaf260e857c9e519cd1b08360b3fcfe21f3
size 2015612
added assets/models/heroes/lion.glb.import
@@ -0,0 +1,42 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://dufxrxox0ehp0"
path="res://.godot/imported/lion.glb-513b0625c620c90cd59d1fd41d59d49f.scn"
[deps]
source_file="res://assets/models/heroes/lion.glb"
dest_files=["res://.godot/imported/lion.glb-513b0625c620c90cd59d1fd41d59d49f.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=3
added assets/models/heroes/snake.glb
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:eae5ce11e865724803ce355c67034b41c0f5b0eabd5fa811eea059eb1a4c8d6f
size 830516
added assets/models/heroes/snake.glb.import
@@ -0,0 +1,42 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://cuoh1va851n8c"
path="res://.godot/imported/snake.glb-f8c8561761ec317bbe4720868885f213.scn"
[deps]
source_file="res://assets/models/heroes/snake.glb"
dest_files=["res://.godot/imported/snake.glb-f8c8561761ec317bbe4720868885f213.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=3
added assets/models/heroes/spider.glb
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d759e35f6cf5adc7eb7e3f70135cd2a808787fc36337a523015592ba840ccd6d
size 449008
added assets/models/heroes/spider.glb.import
@@ -0,0 +1,42 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://b41ic83l624fb"
path="res://.godot/imported/spider.glb-3464a0e876e92670f9067673f26f42e1.scn"
[deps]
source_file="res://assets/models/heroes/spider.glb"
dest_files=["res://.godot/imported/spider.glb-3464a0e876e92670f9067673f26f42e1.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=3
added src/client/hero_model_library.gd
@@ -0,0 +1,101 @@
class_name HeroModelLibrary
extends RefCounted
## The placeholder 3D models the heroes wear, and the logic that drops one onto the
## field at a consistent size and team colour. Each hero kit maps to a low-poly animal
## glTF standing in for the species the shapeshifter takes. The models come from mixed
## sources at wildly different authored scales and facings, so this module normalises
## every one to a single on-field size and washes it with its team colour — the asset
## handling kept out of the match presenter, which only asks for a model by kit.
## A hero kit's placeholder model, keyed by `kit_id`. A kit with no entry (an unknown
## kit, or a hero whose `kit_id` did not survive the wire on a pure CLIENT) has no model
## and the presenter falls back to a plain capsule body.
const HERO_MODELS := {
"lion": "res://assets/models/heroes/lion.glb",
"cheetah": "res://assets/models/heroes/cheetah.glb",
"hyena": "res://assets/models/heroes/hyena.glb",
"snake": "res://assets/models/heroes/snake.glb",
"spider": "res://assets/models/heroes/spider.glb",
"chameleon": "res://assets/models/heroes/chameleon.glb",
}
## The world size a model's longest axis is scaled to, so every model reads at one size
## on the field regardless of the units it was authored in. Sized a touch above the
## capsule it replaces so the species is legible from the follow-camera.
const HERO_MODEL_SIZE := 260.0
## The opacity of the team-colour wash overlaid on a model, strong enough to read blue
## or red at a glance while the species texture still shows through underneath.
const TEAM_TINT_ALPHA := 0.34
## Whether `kit_id` has a placeholder model. Gate `add_to` on this — an unmodelled kit
## keeps the capsule body.
static func has_model(kit_id: String) -> bool:
return HERO_MODELS.has(kit_id)
## Instances `kit_id`'s model under `parent`, size-normalised and washed with
## `team_tint`, and returns it. `parent` must already be in the tree so the model's mesh
## transforms resolve for the bounds measurement. Call only when `has_model(kit_id)`.
static func add_to(parent: Node3D, kit_id: String, team_tint: Color) -> Node3D:
var packed := load(HERO_MODELS[kit_id]) as PackedScene
var model := packed.instantiate() as Node3D
parent.add_child(model)
_normalize(model)
_tint(model, team_tint)
return model
## Scales `model` so its longest axis spans HERO_MODEL_SIZE, then offsets it so its
## footprint is centred on the parent origin and its base rests on the ground (y = 0).
## The models arrive at wildly different authored scales, so this is what makes every
## hero read at one size.
static func _normalize(model: Node3D) -> void:
var aabb := _model_aabb(model)
var longest := maxf(aabb.size.x, maxf(aabb.size.y, aabb.size.z))
if longest <= 0.0:
return
var s := HERO_MODEL_SIZE / longest
model.scale = Vector3(s, s, s)
var center := aabb.get_center()
model.position = Vector3(-center.x * s, -aabb.position.y * s, -center.z * s)
## The merged bounds of every mesh under `model`, in the model's own local space so a
## later offset on the model does not skew it. The model must be in the tree for the
## mesh global transforms to resolve.
static func _model_aabb(model: Node3D) -> AABB:
var inv := model.global_transform.affine_inverse()
var out := AABB()
var first := true
for mi in _meshes(model):
var local: AABB = inv * (mi.global_transform * mi.get_aabb())
if first:
out = local
first = false
else:
out = out.merge(local)
return out
## Lays a translucent `color` overlay over every mesh in `model`, tinting it toward its
## team without replacing the model's own material, so the species texture stays visible.
static func _tint(model: Node3D, color: Color) -> void:
var wash := color
wash.a = TEAM_TINT_ALPHA
var overlay := StandardMaterial3D.new()
overlay.albedo_color = wash
overlay.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
overlay.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
for mi in _meshes(model):
mi.material_overlay = overlay
## Every MeshInstance3D carrying a mesh in the subtree under `node`, gathered depth-first.
static func _meshes(node: Node, acc: Array[MeshInstance3D] = []) -> Array[MeshInstance3D]:
if node is MeshInstance3D and (node as MeshInstance3D).mesh != null:
acc.append(node as MeshInstance3D)
for child in node.get_children():
_meshes(child, acc)
return acc
added src/client/hero_model_library.gd.uid
@@ -0,0 +1 @@
uid://by7xm1oa85fms
modified src/client/main.gd
@@ -691,12 +691,7 @@ func _make_view(entity: SimEntity) -> Dictionary:
var root := Node3D.new()
add_child(root)
var view := {"root": root}
var body := MeshInstance3D.new()
body.mesh = _body_mesh(entity)
body.position = Vector3(0.0, _body_half_height(entity), 0.0)
body.material_override = _flat_material(_body_color(entity))
root.add_child(body)
view["body"] = body
view["body"] = _build_body(root, entity)
if entity.is_hero:
var ring := MeshInstance3D.new()
ring.mesh = _ring_mesh()
@@ -708,6 +703,23 @@ func _make_view(entity: SimEntity) -> Dictionary:
return view
## Builds an entity's body under `root`: a size-normalised animal model for a hero whose
## kit has one (handed off to HeroModelLibrary), or a primitive (capsule unit, box
## structure) otherwise. Returned so the view can hold it, though the body is never
## mutated again once built — team and form read off the tint and the ring, not the body.
## A pure CLIENT whose snapshot carried no `kit_id` falls through to the capsule, so an
## unmodelled hero still draws.
func _build_body(root: Node3D, entity: SimEntity) -> Node3D:
if entity.is_hero and HeroModelLibrary.has_model(entity.kit_id):
return HeroModelLibrary.add_to(root, entity.kit_id, _team_color(entity.team))
var body := MeshInstance3D.new()
body.mesh = _body_mesh(entity)
body.position = Vector3(0.0, _body_half_height(entity), 0.0)
body.material_override = _flat_material(_body_color(entity))
root.add_child(body)
return body
## Hangs the floating UI above an entity: an HP bar for anything with health, plus a
## resource bar and a status label for a hero. Creeps get only a lower HP bar.
func _attach_overlay(view: Dictionary, entity: SimEntity) -> void: