Compare commits
38 Commits
a1c77fed5e
...
0.5.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 91d88cd5c7 | |||
| 6dff45c659 | |||
| 2fd41f4f69 | |||
| a147527e91 | |||
| 09d4a60f43 | |||
| c0a022fed4 | |||
| 9458085abb | |||
| 565c132848 | |||
| 400ae24f92 | |||
| 79ea5bb2be | |||
| 981ebf1910 | |||
| 01d6ecf475 | |||
| 931ddf0eeb | |||
| 50073b2574 | |||
| 07f8a71f2c | |||
| 34dc5b026e | |||
| 5c1e881a53 | |||
| 4efc6763f0 | |||
| 76d4fee33c | |||
| 271be0b26b | |||
| 98c6f1285a | |||
| 4d66df8c7e | |||
| 41f18cb5d6 | |||
| 6b6a926715 | |||
| dae7ec06da | |||
| 1eb06876e9 | |||
| 1021a07b04 | |||
| c365f76d80 | |||
| 32ac46f840 | |||
| bd10d1789b | |||
| 170d8ff3f1 | |||
| 550bf2c5ad | |||
| 02f3e283de | |||
| 9123574947 | |||
| aadf59d2d0 | |||
| d57c3dc2a2 | |||
| 09ae6bfbf4 | |||
| 40c1b8e680 |
@@ -2,4 +2,4 @@ disable:
|
||||
- class-definitions-order
|
||||
- no-else-return
|
||||
max-line-length: 120
|
||||
max-file-lines: 1200
|
||||
max-file-lines: 3000
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
name: CI
|
||||
|
||||
# Gitea Actions is GitHub-Actions-compatible: drop this in .gitea/workflows/, enable Actions on the
|
||||
# repo ([actions] ENABLED=true in app.ini, Gitea >= 1.19) and register an act_runner — it then runs
|
||||
# on every push / PR. Godot and gdtoolkit are installed with plain shell steps (not the chickensoft
|
||||
# setup-godot action), because a self-hosted runner usually can't reach the GitHub action registry
|
||||
# (unless DEFAULT_ACTIONS_URL is set to proxy github.com). Only actions/checkout and upload-artifact
|
||||
# remain as external actions — both ship with Gitea's default proxy; if yours is offline, mirror them
|
||||
# into your instance.
|
||||
#
|
||||
# It also runs once a day (schedule) as a safety net over main's tip. The `guard` job skips that daily
|
||||
# run when the commit already passed — so an idle main isn't re-tested, but a commit whose push-time
|
||||
# run was missed/skipped still gets verified. schedule needs Gitea >= 1.21; workflow_dispatch (the
|
||||
# manual "Run workflow" button) needs >= 1.23; from 1.23 the cron is evaluated in UTC.
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 4 * * *" # 04:00 UTC daily
|
||||
|
||||
# statuses:write lets the test job stamp a `ci/headless` commit status the guard reads back.
|
||||
permissions:
|
||||
contents: read
|
||||
statuses: write
|
||||
|
||||
env:
|
||||
# The Godot build the team runs (godot --version -> 4.7.beta.custom_build.417cd33bf). Pin this to a
|
||||
# PUBLISHED build from https://github.com/godotengine/godot-builds/releases. If you run a custom
|
||||
# build with no public binary, host the linux headless zip where the runner can reach it and set
|
||||
# GODOT_URL to that (it overrides the constructed URL below).
|
||||
GODOT_VERSION: "4.7-beta1"
|
||||
GODOT_URL: ""
|
||||
|
||||
jobs:
|
||||
# On push/PR this passes straight through. On the daily schedule it asks the API whether main's tip
|
||||
# already carries a successful `ci/headless` status (stamped by an earlier run) and, if so, tells the
|
||||
# test job to skip — that's the "don't re-test a commit that already ran" rule.
|
||||
guard:
|
||||
name: Decide whether to run
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
run: ${{ steps.decide.outputs.run }}
|
||||
steps:
|
||||
- name: Check for an existing pass on this commit
|
||||
id: decide
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
set -u
|
||||
if [ "${{ github.event_name }}" != "schedule" ]; then
|
||||
echo "event=${{ github.event_name }} -> run"
|
||||
echo "run=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
api="${{ github.api_url }}"; [ -n "$api" ] || api="${{ github.server_url }}/api/v1"
|
||||
curl -sfL -H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"$api/repos/${{ github.repository }}/commits/${{ github.sha }}/statuses?limit=100" \
|
||||
-o /tmp/statuses.json || echo '[]' > /tmp/statuses.json
|
||||
python3 - <<'PY'
|
||||
import json, os
|
||||
try:
|
||||
data = json.load(open("/tmp/statuses.json"))
|
||||
except Exception:
|
||||
data = []
|
||||
items = data.get("statuses", data) if isinstance(data, dict) else data
|
||||
done = any(
|
||||
s.get("context") == "ci/headless" and (s.get("state") or s.get("status")) == "success"
|
||||
for s in (items or [])
|
||||
)
|
||||
with open(os.environ["GITHUB_OUTPUT"], "a") as f:
|
||||
f.write("run=%s\n" % ("false" if done else "true"))
|
||||
print("commit already passed ci/headless -> skip" if done else "no prior pass -> run")
|
||||
PY
|
||||
|
||||
test:
|
||||
name: Lint & headless tests
|
||||
runs-on: ubuntu-latest # must match a label your act_runner registered with
|
||||
needs: guard
|
||||
if: ${{ needs.guard.outputs.run == 'true' }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Godot ${{ env.GODOT_VERSION }}
|
||||
run: |
|
||||
set -eux
|
||||
url="${GODOT_URL:-https://github.com/godotengine/godot-builds/releases/download/${GODOT_VERSION}/Godot_v${GODOT_VERSION}_linux.x86_64.zip}"
|
||||
curl -fL "$url" -o /tmp/godot.zip
|
||||
unzip -o /tmp/godot.zip -d /tmp/godot
|
||||
install -m 755 /tmp/godot/Godot_v*_linux.x86_64 /usr/local/bin/godot
|
||||
godot --version
|
||||
|
||||
- name: Install gdlint (gdtoolkit)
|
||||
# A venv sidesteps PEP-668 "externally managed" pip on recent runner images; putting its bin on
|
||||
# GITHUB_PATH lets run_tests.sh call bare `gdlint` in later steps.
|
||||
run: |
|
||||
set -eux
|
||||
python3 -m venv /tmp/venv
|
||||
/tmp/venv/bin/pip install "gdtoolkit==4.*"
|
||||
echo "/tmp/venv/bin" >> "$GITHUB_PATH"
|
||||
|
||||
# A cold .godot cache (CI always starts cold) needs two import passes: the first imports base
|
||||
# assets, the second resolves scenes that depend on them (the .fbx/.glb -> .scn imports). One pass
|
||||
# leaves those unresolved and the headless tests fail to load them. Warnings exit non-zero.
|
||||
- name: Import resources
|
||||
run: |
|
||||
godot --headless --import || true
|
||||
godot --headless --import || true
|
||||
|
||||
- name: gdlint
|
||||
run: gdlint actors systems ui props levels tests
|
||||
|
||||
- name: Headless tests
|
||||
env:
|
||||
GODOT: godot
|
||||
run: bash run_tests.sh --no-gameplay
|
||||
|
||||
# Stamp the commit so the daily guard recognizes it as already tested. Best-effort: a fork PR whose
|
||||
# token can't write statuses just skips this (the build still passes).
|
||||
- name: Mark commit as tested
|
||||
if: success()
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
api="${{ github.api_url }}"; [ -n "$api" ] || api="${{ github.server_url }}/api/v1"
|
||||
curl -sfL -X POST -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" \
|
||||
"$api/repos/${{ github.repository }}/statuses/${{ github.sha }}" \
|
||||
-d '{"context":"ci/headless","state":"success","description":"headless suite green"}' \
|
||||
>/dev/null || echo "could not post commit status (token lacks statuses:write?)"
|
||||
|
||||
- name: Upload test output on failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-output
|
||||
path: tests/output/
|
||||
if-no-files-found: ignore
|
||||
+6
-2
@@ -2,13 +2,17 @@
|
||||
.godot/
|
||||
/android/
|
||||
|
||||
# Godot editor autosave scratch files (e.g. Bear.tscn12015901897.tmp)
|
||||
*.tmp
|
||||
|
||||
# JetBrains IDE — contains absolute paths (Godot binary, SDK dirs), varies per user
|
||||
.idea/
|
||||
|
||||
# Blender render previews (generated by render_anim_preview.py)
|
||||
Blender/preview/
|
||||
art/blender/preview/
|
||||
|
||||
Blender/*.blend1
|
||||
# Blender auto-save backups (.blend1, .blend2, … — Blender rewrites these on every save)
|
||||
art/blender/*.blend[0-9]
|
||||
|
||||
# Test output (generated screenshots, frame strips)
|
||||
tests/output/
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,8 +0,0 @@
|
||||
extends Node3D
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group(&"public") # excluded from debug overlays — see publikum_controller.gd
|
||||
var anim := get_node_or_null(^"AnimationPlayer") as AnimationPlayer
|
||||
if anim:
|
||||
anim.play(&"Juubeldab")
|
||||
@@ -1 +0,0 @@
|
||||
uid://wj6tgh8nb0ka
|
||||
@@ -1,5 +0,0 @@
|
||||
extends Node3D
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
$AnimationPlayer.play("ArmatureAction")
|
||||
@@ -1 +0,0 @@
|
||||
uid://gt3kfchac3m3
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 MiB |
@@ -1,40 +0,0 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://ch1wqha62vhtb"
|
||||
path="res://.godot/imported/IG_square.png-c38dd1a0a4601b6ff4ffec2910ed2ce0.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://Assets/IG_square.png"
|
||||
dest_files=["res://.godot/imported/IG_square.png-c38dd1a0a4601b6ff4ffec2910ed2ce0.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 598 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 412 KiB |
@@ -1,45 +0,0 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://csy5f3l5lufp5"
|
||||
path.s3tc="res://.godot/imported/Lobby_level_wall.png-39243638bfc6ea5742b6e2fdc927ae2c.s3tc.ctex"
|
||||
path.etc2="res://.godot/imported/Lobby_level_wall.png-39243638bfc6ea5742b6e2fdc927ae2c.etc2.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
"md5": "0e7ed34c3c812d96732bd32f9dca0c93"
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://Assets/Lobby_level_wall.png"
|
||||
dest_files=["res://.godot/imported/Lobby_level_wall.png-39243638bfc6ea5742b6e2fdc927ae2c.s3tc.ctex", "res://.godot/imported/Lobby_level_wall.png-39243638bfc6ea5742b6e2fdc927ae2c.etc2.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=true
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=0
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 180 KiB |
@@ -1,45 +0,0 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://ym5273astq46"
|
||||
path.s3tc="res://.godot/imported/Lobby_level_wood.png-4817792c09af3fd5e04ad29e0f9bd0ce.s3tc.ctex"
|
||||
path.etc2="res://.godot/imported/Lobby_level_wood.png-4817792c09af3fd5e04ad29e0f9bd0ce.etc2.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
"md5": "23a7a951553ffe271c47693fd71314ca"
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://Assets/Lobby_level_wood.png"
|
||||
dest_files=["res://.godot/imported/Lobby_level_wood.png-4817792c09af3fd5e04ad29e0f9bd0ce.s3tc.ctex", "res://.godot/imported/Lobby_level_wood.png-4817792c09af3fd5e04ad29e0f9bd0ce.etc2.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=true
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=0
|
||||
@@ -1,42 +0,0 @@
|
||||
[gd_scene format=3 uid="uid://by4ifsgxlv0gj"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://bm6rmt3xprhjs" path="res://Assets/bear_level_arena.glb" id="1_beararena"]
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_floor"]
|
||||
|
||||
[node name="bear_level" type="Node3D" unique_id=2020009824]
|
||||
|
||||
[node name="Node3D" type="Node3D" parent="." unique_id=1366355859]
|
||||
|
||||
[node name="StaticBody3D" type="StaticBody3D" parent="Node3D" unique_id=721553462]
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="Node3D/StaticBody3D" unique_id=474515358]
|
||||
transform = Transform3D(30, 0, 0, 0, 1, 0, 0, 0, 30, 0, -0.5, 0)
|
||||
shape = SubResource("BoxShape3D_floor")
|
||||
|
||||
[node name="bear_level_arena" parent="." unique_id=948414670 instance=ExtResource("1_beararena")]
|
||||
transform = Transform3D(2.875, 0, 0, 0, 2.875, 0, 0, 0, 2.875, 0, 0, 0)
|
||||
|
||||
[node name="MatadorSpawner" type="Node3D" parent="." unique_id=1869467620]
|
||||
|
||||
[node name="Lights" type="Node3D" parent="." unique_id=1435665429]
|
||||
|
||||
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="Lights" unique_id=1814200565]
|
||||
transform = Transform3D(0.01535992, 0.56825536, -0.82270896, 0.16280846, 0.8104039, 0.56279576, 0.9865382, -0.14258848, -0.08006904, -4.08134, 9.197985, 0.97087634)
|
||||
light_energy = 0.4
|
||||
light_indirect_energy = 0.767
|
||||
shadow_blur = 0.47
|
||||
|
||||
[node name="SpotLight3D2" type="SpotLight3D" parent="Lights" unique_id=2071037822]
|
||||
transform = Transform3D(0.904133, 0.30962402, -0.2944087, 0.3084627, 0.0037425472, 0.9512291, 0.2956252, -0.95085174, -0.0921237, -3.629381, 10.86347, -2.0124264)
|
||||
light_energy = 16.0
|
||||
light_indirect_energy = 26.095
|
||||
distance_fade_enabled = true
|
||||
spot_range = 32.90586
|
||||
spot_angle = 73.93
|
||||
|
||||
[node name="SUN_DirectionalLight3D2" type="DirectionalLight3D" parent="Lights" unique_id=1003843933]
|
||||
transform = Transform3D(0.45811793, 0.34843865, -0.81775206, 0.5316611, 0.62986195, 0.56622505, 0.7123655, -0.69416475, 0.10329977, 0, 8.980043, 0)
|
||||
light_energy = 0.106
|
||||
shadow_enabled = true
|
||||
shadow_blur = 10.0
|
||||
Binary file not shown.
@@ -1,38 +0,0 @@
|
||||
[gd_scene format=3 uid="uid://bl6jlbq1dpkc"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://eu2lqjhowj4t" path="res://Assets/Matador.glb" id="1_m7x6l"]
|
||||
[ext_resource type="Script" uid="uid://dpoq87w5pvqa" path="res://matador_2.gd" id="2_mftd2"]
|
||||
|
||||
[node name="matador2" unique_id=1179710042 instance=ExtResource("1_m7x6l")]
|
||||
script = ExtResource("2_mftd2")
|
||||
|
||||
[node name="Skeleton3D" parent="Armature" parent_id_path=PackedInt32Array(1269629432) index="0" unique_id=978366810]
|
||||
bones/0/position = Vector3(0.056660548, 0.6424767, -0.0167365)
|
||||
bones/0/rotation = Quaternion(0.080643006, 0, 0, 0.996743)
|
||||
bones/1/rotation = Quaternion(0.083861314, 0, 0, 0.9964774)
|
||||
bones/3/rotation = Quaternion(0.04833339, 0.5302496, -0.71466804, 0.4535955)
|
||||
bones/4/rotation = Quaternion(0.031473875, -0.17018215, -0.15981929, 0.97185665)
|
||||
bones/5/rotation = Quaternion(0.6747468, -0.021427644, -0.06245957, 0.7350895)
|
||||
bones/6/rotation = Quaternion(3.1828083e-08, -9.5969504e-08, -0.0587008, 0.99827564)
|
||||
bones/7/rotation = Quaternion(0.1907551, 0.20328633, 0.7258648, 0.6288143)
|
||||
bones/8/rotation = Quaternion(0.16639966, -0.05474137, 0.010937729, 0.984477)
|
||||
bones/9/rotation = Quaternion(0.5914298, 0.005839911, -0.041949864, 0.8052435)
|
||||
bones/10/rotation = Quaternion(2.3105285e-08, 1.0152064e-07, 0.05870079, 0.99827564)
|
||||
bones/11/rotation = Quaternion(0.39202613, 0.47060952, 0.5799399, 0.53713316)
|
||||
bones/12/rotation = Quaternion(0.62021786, 0.40894383, 0.44191495, 0.5027981)
|
||||
bones/13/rotation = Quaternion(-0.11584477, 0.0021730124, 0.6343679, 0.7642989)
|
||||
bones/14/rotation = Quaternion(0.1454883, 0.14013468, -0.7254941, 0.65791625)
|
||||
bones/15/rotation = Quaternion(0.71668345, 0.065085255, -0.6923973, -0.05210323)
|
||||
bones/16/rotation = Quaternion(-0.10161893, -0.0019664483, -0.65947086, 0.7448275)
|
||||
bones/17/rotation = Quaternion(0.06964938, -0.042099692, 0.74388325, 0.6633358)
|
||||
bones/18/rotation = Quaternion(0.5305965, -0.28927496, 0.76720744, -0.21489602)
|
||||
bones/19/position = Vector3(0.16341288, 0.31487355, 0.3966543)
|
||||
bones/20/position = Vector3(0.22012731, 0.38421977, 0.95168185)
|
||||
bones/21/position = Vector3(-0.09492579, 0.40446192, -0.46390092)
|
||||
bones/21/rotation = Quaternion(0.7216726, 0.064735115, -0.6891729, 0.0062273443)
|
||||
bones/22/position = Vector3(-0.22012731, 0.38421977, 0.95168185)
|
||||
bones/23/position = Vector3(-0.0382514, 1.7423625, 0.0026792884)
|
||||
bones/23/rotation = Quaternion(0.8626007, 0.0068245474, -0.099159576, -0.49602506)
|
||||
bones/23/scale = Vector3(0.6647317, 1.001677, 1.0066445)
|
||||
bones/24/rotation = Quaternion(-0.007878712, 8.3020435e-09, -0.0023950166, 0.9999661)
|
||||
bones/25/rotation = Quaternion(5.5870206e-09, 5.5879354e-09, -2.0518176e-09, 1)
|
||||
@@ -1,12 +0,0 @@
|
||||
extends Node3D
|
||||
|
||||
|
||||
@onready var animation_player: AnimationPlayer = $AnimationPlayer
|
||||
|
||||
func _ready():
|
||||
animation_player.play("Taunt")
|
||||
animation_player.animation_finished.connect(_on_animation_finished)
|
||||
|
||||
func _on_animation_finished(anim_name):
|
||||
if anim_name == "Taunt":
|
||||
animation_player.play("Taunt")
|
||||
@@ -1 +0,0 @@
|
||||
uid://dycnoiadljbb1
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 296 KiB |
@@ -1,40 +0,0 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bxv1uuymonrp6"
|
||||
path="res://.godot/imported/menu_bg.png-3906063022fa3c81090e3c663f8866b0.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://Assets/menu_bg.png"
|
||||
dest_files=["res://.godot/imported/menu_bg.png-3906063022fa3c81090e3c663f8866b0.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
@@ -1,287 +0,0 @@
|
||||
[gd_scene format=3 uid="uid://o8ocjfprxa7u"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://cn7fhjkro7637" path="res://bear.gd" id="1_beargd"]
|
||||
[ext_resource type="PackedScene" uid="uid://cofv5dbavesis" path="res://Assets/Bear.glb" id="2_bearglb"]
|
||||
[ext_resource type="Script" uid="uid://bsbed05ifr18s" path="res://bear_test_cube.gd" id="3_rog1v"]
|
||||
[ext_resource type="PackedScene" uid="uid://bdbx0wwa73rhp" path="res://Assets/fx/Bear_FX.glb" id="4_rog1v"]
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_body"]
|
||||
radius = 1.0
|
||||
height = 2.4
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_hit"]
|
||||
radius = 1.3
|
||||
height = 2.7
|
||||
|
||||
[node name="Bear" type="CharacterBody3D" unique_id=1573237287]
|
||||
script = ExtResource("1_beargd")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=830443382]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.2, 0)
|
||||
shape = SubResource("CapsuleShape3D_body")
|
||||
|
||||
[node name="HitArea" type="Area3D" parent="." unique_id=365327397]
|
||||
monitorable = false
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="HitArea" unique_id=2111867348]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.35, 0)
|
||||
shape = SubResource("CapsuleShape3D_hit")
|
||||
|
||||
[node name="bear_model" parent="." unique_id=359217823 instance=ExtResource("2_bearglb")]
|
||||
|
||||
[node name="Skeleton3D" parent="bear_model/bear-rig" parent_id_path=PackedInt32Array(359217823, 1619621659) index="0" unique_id=1324818366]
|
||||
bones/1/position = Vector3(0.04601016, 1.9209582, 0.9051311)
|
||||
bones/1/rotation = Quaternion(0.5270001, -0.011489415, -0.05297156, 0.848135)
|
||||
bones/2/rotation = Quaternion(0.29015693, 0.002787927, 0.00639222, 0.9569537)
|
||||
bones/2/scale = Vector3(0.9999999, 1.0000008, 0.99999934)
|
||||
bones/3/rotation = Quaternion(0.06707396, 0.012082217, 0.011868715, 0.9976043)
|
||||
bones/4/rotation = Quaternion(0.3487565, 0.010761848, 0.013076123, 0.93706036)
|
||||
bones/5/rotation = Quaternion(0.15472108, -0.21109715, -0.09227382, 0.96072114)
|
||||
bones/6/rotation = Quaternion(0.4204113, 0.23492317, -0.11518617, 0.86879086)
|
||||
bones/7/rotation = Quaternion(0.76830006, 0.36476925, -0.4668917, -0.24222025)
|
||||
bones/8/rotation = Quaternion(0.5460264, -0.71257937, 0.40870878, -0.16444752)
|
||||
bones/9/rotation = Quaternion(-0.02719195, -0.14156067, -0.26938534, 0.9521832)
|
||||
bones/10/rotation = Quaternion(-0.12630412, -0.39785445, -0.057911687, 0.90686566)
|
||||
bones/11/rotation = Quaternion(0.70209646, -0.35357565, 0.59454584, -0.16899753)
|
||||
bones/12/rotation = Quaternion(0.5294657, 0.7712517, -0.33475664, -0.1130258)
|
||||
bones/13/rotation = Quaternion(-0.04075809, 0.17025389, 0.28913936, 0.94114333)
|
||||
bones/14/rotation = Quaternion(-0.03496992, 0.2568669, -0.0008229511, 0.9658136)
|
||||
bones/15/rotation = Quaternion(0.9957146, -0.032195006, 0.031220285, 0.08087847)
|
||||
bones/16/rotation = Quaternion(0.9367782, -0.012506716, 0.033659726, 0.3480767)
|
||||
bones/17/rotation = Quaternion(-0.860235, -0.08192984, 0.009118035, 0.50319016)
|
||||
bones/17/scale = Vector3(0.9999998, 1.0000002, 1)
|
||||
bones/19/rotation = Quaternion(-4.514978e-15, 1, 7.4505806e-08, -5.523351e-07)
|
||||
bones/20/rotation = Quaternion(0.9729887, -0.014750167, 0.014303936, 0.22993676)
|
||||
bones/21/rotation = Quaternion(0.9366144, -0.00601367, 0.016095275, 0.34994036)
|
||||
bones/22/rotation = Quaternion(-0.7732739, -0.06955802, 0.0075612683, 0.6302)
|
||||
bones/22/scale = Vector3(0.9999997, 0.9999998, 0.9999999)
|
||||
bones/24/rotation = Quaternion(6.628114e-12, 0.99999994, 5.2154085e-08, -0.00035543842)
|
||||
bones/25/position = Vector3(1.0117401, 1.3159828, 3.9725027)
|
||||
bones/25/rotation = Quaternion(-0.27285534, 0.9274522, 0.08017836, 0.2428039)
|
||||
bones/25/scale = Vector3(1.0000001, 12.140265, 1.0000001)
|
||||
bones/26/position = Vector3(-0.9025072, 1.4953425, 4.006362)
|
||||
bones/26/rotation = Quaternion(0.5100001, 0.6936306, 0.15438241, -0.48470873)
|
||||
bones/26/scale = Vector3(0.99999994, 11.348344, 1)
|
||||
bones/27/position = Vector3(0.70899034, 1.0356265, 0.611243)
|
||||
bones/27/rotation = Quaternion(0.7965308, 0.01196337, -0.0119632045, 0.6043613)
|
||||
bones/27/scale = Vector3(0.99999994, 29.328602, 1)
|
||||
bones/28/position = Vector3(-0.9047001, 1.0118608, 0.8510791)
|
||||
bones/28/rotation = Quaternion(0.776662, 0.010376095, -0.010376263, 0.6297467)
|
||||
bones/28/scale = Vector3(1, 26.535925, 1)
|
||||
bones/30/position = Vector3(0.07262612, 0.67294323, -1.1644177)
|
||||
bones/30/rotation = Quaternion(-0.7183768, 0.04937226, -0.02237074, 0.69353926)
|
||||
bones/31/position = Vector3(5.9604645e-08, -1.4631644, 0.23291233)
|
||||
bones/31/rotation = Quaternion(1.1641533e-09, 1.8335415e-09, 2.328307e-10, 1)
|
||||
bones/31/scale = Vector3(0.99999994, 0.9999999, 0.99999994)
|
||||
bones/32/position = Vector3(-0.0331195, -2.362161, -0.036310136)
|
||||
bones/32/rotation = Quaternion(0.20497073, -0.020519394, -0.033738203, 0.9779712)
|
||||
bones/32/scale = Vector3(0.9999999, 0.9999999, 0.99999994)
|
||||
bones/33/position = Vector3(8.195639e-08, -1.4631642, 0.23291235)
|
||||
bones/33/rotation = Quaternion(-8.288772e-08, 3.4342527e-09, -4.1909525e-09, 1)
|
||||
bones/33/scale = Vector3(0.9999998, 0.9999998, 0.99999994)
|
||||
bones/35/position = Vector3(-7.893884e-09, -1.0594703, 5.2079617e-08)
|
||||
bones/35/rotation = Quaternion(0.99971783, 1.2737803e-07, 5.365157e-06, -0.023754692)
|
||||
bones/35/scale = Vector3(0.99999976, 0.9999998, 1.0000012)
|
||||
bones/39/position = Vector3(0.8199172, 0.25251278, 0.47214246)
|
||||
bones/39/rotation = Quaternion(0.9961935, -0.035078514, -0.0045541152, 0.07967066)
|
||||
bones/39/scale = Vector3(1.0000004, 0.9999996, 0.9999991)
|
||||
bones/40/rotation = Quaternion(0.93738276, 1.2575202e-07, 3.4991626e-08, 0.3483009)
|
||||
bones/40/scale = Vector3(1.0000004, 1.0000001, 1.0000001)
|
||||
bones/41/rotation = Quaternion(-0.860235, -0.081929855, 0.009117997, 0.5031899)
|
||||
bones/41/scale = Vector3(0.99999946, 0.99999976, 0.9999996)
|
||||
bones/42/position = Vector3(-1.9729964e-07, 0.56145483, 7.283501e-08)
|
||||
bones/46/scale = Vector3(0.99999946, 0.99999964, 0.9999999)
|
||||
bones/48/rotation = Quaternion(9.8842406e-08, -0.054423165, -1.786034e-08, 0.998518)
|
||||
bones/48/scale = Vector3(0.9999993, 0.9999996, 0.9999999)
|
||||
bones/50/scale = Vector3(0.99999994, 0.99999994, 0.9999998)
|
||||
bones/51/rotation = Quaternion(-6.30798e-08, 0.026635498, 6.253992e-09, 0.99964523)
|
||||
bones/51/scale = Vector3(0.9999999, 0.99999964, 0.99999964)
|
||||
bones/53/position = Vector3(-0.8199191, 0.25251284, 0.47213903)
|
||||
bones/53/rotation = Quaternion(0.9730908, -0.0186988, -0.0024160824, 0.22964936)
|
||||
bones/53/scale = Vector3(1, 0.99999875, 0.99999946)
|
||||
bones/54/rotation = Quaternion(0.9367526, -1.1281111e-07, -3.6940495e-08, 0.34999225)
|
||||
bones/55/rotation = Quaternion(-0.77327377, -0.06955799, 0.007561261, 0.6302002)
|
||||
bones/55/scale = Vector3(1.0000002, 1, 1.0000002)
|
||||
bones/56/position = Vector3(-5.9126133e-08, 0.5614537, 7.70865e-07)
|
||||
bones/57/rotation = Quaternion(0.70073944, -0.70073956, -0.094679825, -0.09467953)
|
||||
bones/60/scale = Vector3(1.0000004, 1.0000004, 1.0000002)
|
||||
bones/62/rotation = Quaternion(5.8764627e-08, -0.042629674, 2.4442278e-08, 0.99909097)
|
||||
bones/62/scale = Vector3(1.0000002, 1.0000002, 1.0000001)
|
||||
bones/64/scale = Vector3(1.0000004, 1.0000004, 1)
|
||||
bones/65/rotation = Quaternion(8.029546e-08, 0.01272784, 4.3857337e-08, 0.999919)
|
||||
bones/65/scale = Vector3(1.0000002, 1.0000004, 1.0000004)
|
||||
bones/67/rotation = Quaternion(0.7179022, -7.671307e-07, 7.885642e-07, 0.696144)
|
||||
bones/67/scale = Vector3(1.0000004, 0.9999986, 0.99999994)
|
||||
bones/68/rotation = Quaternion(0.9776482, 2.0029378e-07, 1.0460874e-06, -0.2102479)
|
||||
bones/68/scale = Vector3(1.0000004, 1.0000004, 0.9999982)
|
||||
bones/69/position = Vector3(-2.4645832e-08, -6.861796e-09, 7.455772e-08)
|
||||
bones/69/rotation = Quaternion(-0.8315212, 2.5276703e-08, -9.3330055e-09, 0.5554931)
|
||||
bones/69/scale = Vector3(1, 1.0000001, 1.0000002)
|
||||
bones/70/rotation = Quaternion(0.67750055, 8.820757e-08, 7.575834e-08, 0.7355223)
|
||||
bones/70/scale = Vector3(1, 1, 1)
|
||||
bones/71/scale = Vector3(0.9999999, 1, 0.9999999)
|
||||
bones/72/rotation = Quaternion(-2.1641608e-07, -4.6565607e-10, -4.6576065e-10, 1)
|
||||
bones/75/rotation = Quaternion(0.25299338, 0.020847347, 0.028576687, 0.9668212)
|
||||
bones/76/position = Vector3(1.01587794e-07, -1.5306958e-07, -4.3554866e-07)
|
||||
bones/76/rotation = Quaternion(-0.5206146, -3.634943e-08, 7.6474e-08, 0.8537919)
|
||||
bones/77/rotation = Quaternion(0.9373828, 1.2785249e-07, 3.3241214e-08, 0.3483008)
|
||||
bones/78/position = Vector3(0.8199172, 0.25251257, 0.47214243)
|
||||
bones/78/rotation = Quaternion(0.99571455, -0.032195, 0.031220287, 0.08087897)
|
||||
bones/78/scale = Vector3(1.0000004, 0.9999997, 0.99999905)
|
||||
bones/80/rotation = Quaternion(0.7179022, -7.6713064e-07, 7.885641e-07, 0.696144)
|
||||
bones/80/scale = Vector3(1.0000004, 0.9999986, 0.99999994)
|
||||
bones/81/rotation = Quaternion(0.97764814, 1.9846473e-07, 1.0426893e-06, -0.21024783)
|
||||
bones/81/scale = Vector3(1.0000004, 1.0000002, 0.9999982)
|
||||
bones/82/position = Vector3(6.103571e-08, 8.8816435e-08, -3.6955274e-09)
|
||||
bones/82/rotation = Quaternion(-0.8315211, 2.3407136e-08, -1.2821512e-08, 0.5554932)
|
||||
bones/82/scale = Vector3(0.99999994, 0.99999994, 1)
|
||||
bones/83/rotation = Quaternion(0.6775009, -8.600083e-08, -8.977273e-08, 0.735522)
|
||||
bones/83/scale = Vector3(1, 1.0000001, 1)
|
||||
bones/84/scale = Vector3(0.99999994, 1.0000001, 0.9999999)
|
||||
bones/85/rotation = Quaternion(2.6833732e-07, -9.313788e-10, -2.5612494e-09, 1)
|
||||
bones/85/scale = Vector3(1.0000001, 1, 1.0000001)
|
||||
bones/87/rotation = Quaternion(-0.5230753, 0.20132942, 0.026226493, 0.8277505)
|
||||
bones/88/rotation = Quaternion(0.10320552, 0.011111915, 0.015231778, 0.99448144)
|
||||
bones/89/position = Vector3(2.953037e-07, 4.9207458e-08, -4.3015167e-08)
|
||||
bones/89/rotation = Quaternion(-0.5201237, 5.177172e-08, -1.16139375e-07, 0.8540909)
|
||||
bones/90/rotation = Quaternion(0.9367527, -1.0975643e-07, -3.3382268e-08, 0.34999216)
|
||||
bones/91/position = Vector3(-0.8199192, 0.2525128, 0.47213912)
|
||||
bones/91/rotation = Quaternion(0.9729887, -0.0147501845, 0.014303951, 0.22993667)
|
||||
bones/91/scale = Vector3(1.0000002, 0.999999, 0.99999964)
|
||||
bones/96/position = Vector3(-0.016559705, -1.9126627, 0.09830098)
|
||||
bones/96/rotation = Quaternion(0.10305436, -0.010316663, -0.016962789, 0.99447757)
|
||||
bones/96/scale = Vector3(0.9999998, 1, 1)
|
||||
bones/97/rotation = Quaternion(0.997074, -1.2813135e-07, 1.666339e-06, -0.07644201)
|
||||
bones/98/position = Vector3(0.0013585633, -0.23419222, 0.016214281)
|
||||
bones/98/rotation = Quaternion(-0.15130657, -0.005602528, -0.008209973, 0.98843694)
|
||||
bones/98/scale = Vector3(1.0000001, 1.0000006, 1.0000029)
|
||||
bones/101/position = Vector3(-0.016559973, 1.4684486, 0.06451648)
|
||||
bones/101/rotation = Quaternion(0.99944556, 0.016124567, -0.011581497, -0.02673253)
|
||||
bones/101/scale = Vector3(1.0000001, 0.9999993, 0.99999183)
|
||||
bones/102/rotation = Quaternion(0.95545256, -6.924111e-08, 2.2887033e-07, -0.29514462)
|
||||
bones/103/position = Vector3(-0.00705543, 0.6694702, -0.06780842)
|
||||
bones/103/rotation = Quaternion(0.15471841, -0.21109664, -0.09227309, 0.9607217)
|
||||
bones/103/scale = Vector3(1.0000005, 1.0000286, 1.0000087)
|
||||
bones/104/rotation = Quaternion(0.420411, 0.23492327, -0.115186565, 0.86879104)
|
||||
bones/104/scale = Vector3(0.9999993, 0.9999884, 0.9999856)
|
||||
bones/105/rotation = Quaternion(0.91976035, -0.013162326, 0.0148637965, 0.39197809)
|
||||
bones/105/scale = Vector3(1, 1.0000005, 1.0000114)
|
||||
bones/106/position = Vector3(-0.010389687, -0.091948025, 0.0016658108)
|
||||
bones/106/rotation = Quaternion(0.82734275, 0.15726796, -0.14167279, -0.52028793)
|
||||
bones/107/rotation = Quaternion(0.77328545, 0.16233337, -0.107321426, 0.6034564)
|
||||
bones/107/scale = Vector3(0.9999966, 0.99997723, 0.9999995)
|
||||
bones/108/position = Vector3(-1.1340098e-08, 7.923448e-07, 9.0939814e-08)
|
||||
bones/108/rotation = Quaternion(0.42463258, 0.043219328, 0.08968367, -0.89987564)
|
||||
bones/110/rotation = Quaternion(5.443812e-06, 3.6259098e-09, -1.2069942e-06, 1)
|
||||
bones/110/scale = Vector3(0.99999994, 0.99999976, 1.0000001)
|
||||
bones/113/position = Vector3(0.08179821, 0.64012206, 0.7106584)
|
||||
bones/113/rotation = Quaternion(0.7683008, 0.36476624, -0.46689337, -0.24221936)
|
||||
bones/114/position = Vector3(0.08595797, 0.6463289, -0.32087424)
|
||||
bones/114/rotation = Quaternion(0.5615046, -0.7056476, 0.3871702, -0.19202453)
|
||||
bones/114/scale = Vector3(1.0000049, 1.0000027, 1.0000052)
|
||||
bones/115/rotation = Quaternion(-0.037640397, -0.10443457, -0.2681242, 0.9569672)
|
||||
bones/115/scale = Vector3(0.9999974, 1.0000036, 0.99999976)
|
||||
bones/116/rotation = Quaternion(-0.12630361, -0.39785278, -0.057911348, 0.90686655)
|
||||
bones/116/scale = Vector3(1.0000005, 0.9999972, 1.0000013)
|
||||
bones/119/scale = Vector3(1.0000043, 0.9999975, 0.99999744)
|
||||
bones/121/rotation = Quaternion(-2.7027176e-07, -0.20424414, 2.9341666e-07, 0.97892)
|
||||
bones/121/scale = Vector3(1.0000037, 0.99999774, 0.9999977)
|
||||
bones/123/scale = Vector3(1.000003, 0.9999977, 0.99999917)
|
||||
bones/124/rotation = Quaternion(1.2542905e-06, -0.033494003, 5.941339e-07, 0.999439)
|
||||
bones/124/scale = Vector3(1.0000037, 0.9999978, 0.99999875)
|
||||
bones/126/rotation = Quaternion(-0.51635414, -0.1681693, 0.6340872, 0.5504825)
|
||||
bones/126/scale = Vector3(1.0000056, 1.0000031, 1.0000038)
|
||||
bones/127/rotation = Quaternion(-0.24332374, 0.12663902, 0.6612198, 0.6982439)
|
||||
bones/127/scale = Vector3(1.0000057, 1.0000007, 1.0000064)
|
||||
bones/128/position = Vector3(3.0230987e-07, -5.5011895e-07, -6.94335e-08)
|
||||
bones/128/rotation = Quaternion(0.17027588, 0.93388194, -0.2967173, 0.104064964)
|
||||
bones/129/rotation = Quaternion(-0.037640374, -0.10443452, -0.26812422, 0.9569671)
|
||||
bones/131/rotation = Quaternion(-0.54023695, -0.3639527, 0.10910668, 0.7508517)
|
||||
bones/132/rotation = Quaternion(-0.13163939, 0.9186296, -0.34996635, -0.12772812)
|
||||
bones/133/rotation = Quaternion(-0.012778445, 0.011854936, 0.00015161023, 0.99984807)
|
||||
bones/133/scale = Vector3(1.0422997, 1.0422999, 1.0422992)
|
||||
bones/134/rotation = Quaternion(0.02152282, -2.7759576e-08, -1.5735115e-07, 0.99976844)
|
||||
bones/134/scale = Vector3(0.98826313, 0.9882641, 0.9882627)
|
||||
bones/135/position = Vector3(0.08595812, 0.646329, -0.3208742)
|
||||
bones/135/rotation = Quaternion(0.5460276, -0.7125794, 0.40870702, -0.16444758)
|
||||
bones/135/scale = Vector3(1.000007, 1.0000004, 1.0000051)
|
||||
bones/137/position = Vector3(-0.09762837, 0.6401221, 0.7106583)
|
||||
bones/137/rotation = Quaternion(0.7020965, -0.35357338, 0.5945473, -0.16899663)
|
||||
bones/138/position = Vector3(-0.08595776, 0.646329, -0.3208746)
|
||||
bones/138/rotation = Quaternion(0.5492876, 0.7627154, -0.30113, -0.16083841)
|
||||
bones/138/scale = Vector3(1.0000051, 1.0000018, 1.0000046)
|
||||
bones/139/rotation = Quaternion(-0.058682628, 0.11130926, 0.28603986, 0.94991994)
|
||||
bones/139/scale = Vector3(0.9999983, 1.0000024, 0.9999992)
|
||||
bones/140/rotation = Quaternion(-0.034969915, 0.25686622, -0.0008231461, 0.96581376)
|
||||
bones/140/scale = Vector3(1.0000031, 0.9999977, 0.9999988)
|
||||
bones/143/scale = Vector3(1.000005, 0.99999815, 0.9999967)
|
||||
bones/145/rotation = Quaternion(-4.0431777e-07, 0.1295653, -5.1404834e-07, 0.99157095)
|
||||
bones/145/scale = Vector3(1.0000045, 0.99999803, 0.9999968)
|
||||
bones/147/scale = Vector3(1.0000035, 0.99999845, 0.9999981)
|
||||
bones/148/rotation = Quaternion(1.9343124e-06, 0.025161995, -3.222267e-07, 0.9996834)
|
||||
bones/148/scale = Vector3(1.000004, 0.9999991, 0.999998)
|
||||
bones/150/rotation = Quaternion(-0.5163542, 0.16816999, -0.6340877, 0.5504816)
|
||||
bones/150/scale = Vector3(1.0000052, 1.0000039, 1.0000025)
|
||||
bones/151/rotation = Quaternion(-0.24332343, -0.1266389, -0.6612192, 0.6982446)
|
||||
bones/151/scale = Vector3(1.0000049, 1.0000005, 1.0000063)
|
||||
bones/152/position = Vector3(-2.0525843e-07, -4.4378166e-07, 2.2242949e-07)
|
||||
bones/152/rotation = Quaternion(-0.1980554, 0.9486651, -0.20058647, -0.14343536)
|
||||
bones/153/rotation = Quaternion(-0.058682676, 0.11130926, 0.2860399, 0.94991994)
|
||||
bones/155/rotation = Quaternion(-0.48170745, 0.21343125, -0.09362694, 0.84477156)
|
||||
bones/155/scale = Vector3(0.99999994, 1, 0.9999998)
|
||||
bones/156/rotation = Quaternion(0.2123156, 0.9450362, -0.15822437, 0.19181721)
|
||||
bones/157/rotation = Quaternion(-0.01277751, -0.011854968, -0.0001513001, 0.9998481)
|
||||
bones/157/scale = Vector3(1.0423003, 1.0422994, 1.0423005)
|
||||
bones/158/rotation = Quaternion(0.021521209, -2.5687226e-07, -2.570953e-07, 0.9997684)
|
||||
bones/158/scale = Vector3(0.9882631, 0.98826426, 0.9882618)
|
||||
bones/159/position = Vector3(-0.085957706, 0.6463291, -0.32087478)
|
||||
bones/159/rotation = Quaternion(0.5294669, 0.7712517, -0.33475488, -0.1130262)
|
||||
bones/159/scale = Vector3(1.0000075, 1.0000001, 1.0000042)
|
||||
bones/161/position = Vector3(0.08179821, 0.64012206, 0.7106584)
|
||||
bones/161/rotation = Quaternion(0.7683008, 0.36476624, -0.46689337, -0.24221936)
|
||||
bones/162/position = Vector3(-0.09762837, 0.6401221, 0.7106583)
|
||||
bones/162/rotation = Quaternion(0.7020965, -0.35357338, 0.5945473, -0.16899663)
|
||||
bones/165/position = Vector3(-1.5017577e-08, 0.26895812, -1.6243575e-08)
|
||||
bones/165/rotation = Quaternion(-0.019247467, -0.20323175, 0.97894067, -0.0012537595)
|
||||
bones/166/position = Vector3(0.016703974, -2.2814677, 2.0323193)
|
||||
bones/167/position = Vector3(0.7698192, 2.760036, -1.7026894)
|
||||
bones/167/rotation = Quaternion(-0.93479383, -0.013274327, 0.31412983, 0.16524744)
|
||||
bones/167/scale = Vector3(0.9999947, 0.9999997, 1.0000055)
|
||||
bones/168/position = Vector3(0.18381567, -0.9622623, 0.9894084)
|
||||
bones/168/rotation = Quaternion(0.93622446, 1.3019375e-07, -0.34127164, -0.08377019)
|
||||
bones/168/scale = Vector3(1.0000024, 1.0000011, 1.0000095)
|
||||
bones/170/position = Vector3(0.748137, 0.69556385, 3.7252903e-09)
|
||||
bones/170/rotation = Quaternion(0.019250846, -0.20323181, 0.9789406, 0.001253055)
|
||||
bones/171/position = Vector3(0.381392, -2.057497, 2.0762825)
|
||||
bones/172/position = Vector3(-0.9278099, 2.7545004, -1.5181485)
|
||||
bones/172/rotation = Quaternion(-0.91823035, 0.06278634, -0.38804358, 0.04830265)
|
||||
bones/172/scale = Vector3(0.99999493, 0.9999995, 1.0000055)
|
||||
bones/173/position = Vector3(0.18381546, -0.96226203, 0.9894083)
|
||||
bones/173/rotation = Quaternion(0.9362244, 3.755922e-07, 0.3412715, -0.0837717)
|
||||
bones/173/scale = Vector3(1.0000001, 1.0000001, 0.9999949)
|
||||
bones/183/position = Vector3(-1.982537e-07, 5.9604563e-08, -1.517833e-08)
|
||||
bones/185/rotation = Quaternion(5.4077844e-07, -0.20349081, 0.97907686, -1.12395135e-07)
|
||||
bones/186/position = Vector3(0.78680634, 2.8457582, -0.41063747)
|
||||
bones/186/rotation = Quaternion(-0.9983755, 0.019092962, -0.050729975, -0.017562928)
|
||||
bones/186/scale = Vector3(0.9999997, 0.99999994, 1)
|
||||
bones/187/rotation = Quaternion(0.15451065, -2.0305349e-09, -2.0850066e-09, 0.9879911)
|
||||
bones/187/scale = Vector3(1.0000002, 0.99999994, 0.9999999)
|
||||
bones/197/position = Vector3(7.903883e-08, 1.4901151e-07, 8.8932055e-08)
|
||||
bones/199/rotation = Quaternion(0.00034800146, -0.20349097, 0.9790768, -7.2328476e-05)
|
||||
bones/200/position = Vector3(-0.84339374, 2.905353, -0.57784474)
|
||||
bones/200/rotation = Quaternion(-0.9983755, 0.019092962, -0.050729975, -0.017562928)
|
||||
bones/200/scale = Vector3(0.9999997, 0.99999994, 1)
|
||||
bones/201/rotation = Quaternion(0.15451066, -1.9585848e-09, -2.5450761e-09, 0.9879911)
|
||||
bones/201/scale = Vector3(1.0000002, 0.99999994, 0.99999994)
|
||||
|
||||
[node name="test_cube" type="Node3D" parent="." unique_id=646988893]
|
||||
script = ExtResource("3_rog1v")
|
||||
|
||||
[node name="Bear_FX" parent="test_cube" unique_id=1183391245 instance=ExtResource("4_rog1v")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.1459732, 0, -1.528423)
|
||||
|
||||
[node name="Bear_fx_claw_attack_R" parent="test_cube/Bear_FX" index="1" unique_id=941189652]
|
||||
transform = Transform3D(0.26165774, 0.06970292, -0.23443498, -0.046572562, 0.18028688, 0.0016228362, 0.22878301, 0.056650188, 0.2721929, -0.40204993, 0.29855162, 0.917461)
|
||||
|
||||
[editable path="bear_model"]
|
||||
[editable path="test_cube/Bear_FX"]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
uid://ymaeifakqovf
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -10,33 +10,64 @@ I am a **Godot 4.6+ expert**. I follow current best practices for GDScript, scen
|
||||
|
||||
## Project structure
|
||||
|
||||
Files are grouped by **role**, not by type. Scripts and scenes are `snake_case`;
|
||||
`PascalCase` is reserved for `class_name` identifiers (this avoids case-insensitive
|
||||
filesystem aliasing on the web/macOS targets).
|
||||
|
||||
```
|
||||
actors/{player,matador,bear}/ the three characters (script + scene together)
|
||||
systems/combat/ hitbox.gd, damage_event.gd, physics_layers.gd
|
||||
systems/fx/ Fx autoload + effect scenes (Siim's authoring dir)
|
||||
systems/input/ controls_manager.gd, touch_controls.gd
|
||||
systems/debug/ debug_params.gd (DP), debug_draw.gd, console.gd
|
||||
systems/render/ rigid_skin.gd, ps1_filter (post-process)
|
||||
systems/crowd/ billboard audience
|
||||
systems/config/ settings.gd (Settings autoload)
|
||||
ui/ hud, mobile_hud, main_menu, ui_theme/fonts, overlays
|
||||
props/ barrels, torch
|
||||
levels/ game_shell.tscn + level modules
|
||||
tools/ tests/ editor/bake scripts, headless tests
|
||||
art/ all raw art, grouped by role (snake_case):
|
||||
characters/ levels/ weapons/ bull/matador/bear, arena/lobby geometry, spear/sword
|
||||
fx/ anim/ textures/ bear_fx, crowd_clap, baked sheets
|
||||
ui/ icons/ fonts/ menu art, ability icons, Cinzel fonts
|
||||
hdri/ materials/ sky HDRIs, shared StandardMaterial3D resources
|
||||
blender/ Siim's .blend working files (Godot-ignored)
|
||||
```
|
||||
|
||||
| Path | Purpose |
|
||||
|---|---|
|
||||
| `scene.tscn` | The reusable **shell** loaded for every level: player, camera, HUD, PS1 filter, and an empty `LevelRoot`. Its `game_shell.gd` instances the active level's module into `LevelRoot` at runtime |
|
||||
| `levels/game_shell.tscn` | The reusable **shell** loaded for every level: player, camera, HUD, PS1 filter, and an empty `LevelRoot`. Its `game_shell.gd` instances the active level's module into `LevelRoot` at runtime |
|
||||
| `levels/` | The level system: per-level module scenes plus the code that defines, loads and sequences them (see rows below) |
|
||||
| `levels/game_shell.gd` | Shell controller — on load, frees any current level module and instances `Run.active_level().level_scene` into `LevelRoot`. Swapping the whole subtree (never hiding it) is what keeps one level's meshes/colliders/spawners from leaking into the next |
|
||||
| `levels/arena_level.tscn` | The arena **level module**: floor + wall colliders, arena meshes, `MatadorSpawner` (with child `Node3D` spawn points placed by the designer), lighting, audience (`PublikNode`). Instanced into the shell's `LevelRoot` |
|
||||
| `levels/bear_level.tscn` | The Bear's Den **level module**: bear-arena mesh, its own floor collider, lighting, and a `BearSpawner` (`bear_spawn.gd`) that places the bear at center. Instanced into the shell's `LevelRoot` |
|
||||
| `Player.tscn` | Player scene root (`CharacterBody3D`) |
|
||||
| `player.gd` | Movement, charge, turning logic |
|
||||
| `actors/player/player.tscn` | Player scene root (`CharacterBody3D`) |
|
||||
| `actors/player/player.gd` | Movement, charge, turning, and abilities (kick/dash/slam/roll). Ability & movement particle FX are **not** built here — they're authored scenes in `systems/fx/` (`bull_*.tscn`), fired via `Fx.spawn()` (one-shots) / `Fx.attach()` (continuous) |
|
||||
| `actors/player/bull_impact_fx.gd` | Bull's coded, non-particle impact FX (the slam shockwave rings + kick flash-pop — tweened meshes, not CPUParticles3D, so they can't be `systems/fx/` scenes). Created by `player.gd` |
|
||||
| `actors/player/bull_legs.gd` | Bull body-wobble + tail Verlet-chain simulation. Created by `player.gd` |
|
||||
| `camera_spring_arm.gd` | Mouse-look pivot (`Node3D` + `SpringArm3D`) |
|
||||
| `camera_follow.gd` | Smooth camera follow (`Camera3D`) |
|
||||
| `matador.gd` | Matador AI (wander / ragdoll state machine) |
|
||||
| `actors/matador/matador.gd` | Matador AI (wander / ragdoll state machine) |
|
||||
| `levels/matador_spawn.gd` | Arena spawner — one matador per designer-placed child `Node3D` spawn point (each enters with a jump-roll toward center); falls back to a programmatic spread if no spawn points are present. Also handles the split-on-death hydra logic |
|
||||
| `levels/bear_spawn.gd` | Bear-level spawner — instantiates `enemy_scene` at the arena center; emits the same `all_defeated` / `matador_killed` signals as `matador_spawn.gd` |
|
||||
| `Bear.tscn` / `bear.gd` | Dark-Souls-style bear boss (`CharacterBody3D`). Routine state machine (wind-up → active → recovery) picking telegraphed moves by range band: Double Swipe / Overhead Smash (close), Leap Slam (mid-far), plus Stalk (2-leg walk-in) / Prowl (4-leg lope) positioning. Melee steps in during the swing (`bear_lunge_speed`) + tracks the bull; the leap is frame-synced to JUMP_SMASH so the paws hit ground on the impact frame (`bear_leap_impact_frame`); slams kick up dust + shake. Combos chain, hyper-armour mid-swing + poise (`bear_stagger_cd`), phase 2 enrage under `bear_enrage_frac` HP. Never damages the bull by colliding — only swings/slams. Joins `&"matador"` so bull abilities already hit it. Tuning under the `Bear` DP section |
|
||||
| `levels/run_state.gd` | Run progression autoload (`Run`) — Slay-the-Spire map + player position across fights |
|
||||
| `levels/level_def.gd` | `LevelDef` resource — a level's name / map colour / `scene_path` (always the shell) / `level_scene` (the level module instanced into the shell) / optional `enemy_scene` (arena wave vs. a boss like the bear) |
|
||||
| `levels/map_node.gd` | `MapNode` — one runtime node on the run map (position, level, links) |
|
||||
| `levels/MapScreen.tscn` / `levels/map_screen.gd` | Between-fights map screen; win → pick a node → next level |
|
||||
| `crowd_marker.gd` | `CrowdMarker` (`@tool Node3D`, `class_name`) — designer drops one per billboard spectator via the editor's Add-Node dialog; picks one of six 60°-spaced facings from the `orientation` dropdown. Draws an editor-only preview (slab + forward arrow, never serialised) and joins the `&"crowd_marker"` group |
|
||||
| `crowd_billboards.gd` / `crowd_billboard.gdshader` | `MultiMeshInstance3D` that harvests every `CrowdMarker` (group `&"crowd_marker"`) into one draw call, using each marker's world position + chosen facing. Front/back sprite sheet baked by `tools/bake_crowd_sheet.gd`; falls back to a generated inward-facing ring when no markers are placed |
|
||||
| `debug_params.gd` | Runtime-tunable parameter registry (autoload `DP`) |
|
||||
| `Assets/` | Raw 3D assets (`.glb`, `.fbx`) |
|
||||
| `Blender/` | Blender source files, animation scripts, FBX exports |
|
||||
| `Blender/create_matador_anims.py` | Creates walk + idle animations and exports FBX |
|
||||
| `Blender/render_anim_preview.py` | Renders animation preview PNGs for visual verification |
|
||||
| `Blender/preview/` | Output directory for animation preview images |
|
||||
| `actors/bear/bear.tscn` / `actors/bear/bear.gd` | Dark-Souls-style bear boss (`CharacterBody3D`). Routine state machine (wind-up → active → recovery) picking telegraphed moves by range band: Double Swipe / Overhead Smash (close), Leap Slam (mid-far), plus Stalk (2-leg walk-in) / Prowl (4-leg lope) positioning. Melee steps in during the swing (`bear_lunge_speed`) + tracks the bull; the leap is frame-synced to JUMP_SMASH so the paws hit ground on the impact frame (`bear_leap_impact_frame`); slams kick up dust + shake. Combos chain, hyper-armour mid-swing + poise (`bear_stagger_cd`), phase 2 enrage under `bear_enrage_frac` HP. Never damages the bull by colliding — only swings/slams. A horns-first bull charge wounds it (`_sweep_bull_ram`: swept prev→now so a tunnelling/point-blank charge still lands, gated by `bear_ram_reach` + `bull_gore_arc`, edge-triggered = one charge per wound). Joins `&"matador"` so bull abilities already hit it. Tuning under the `Bear` DP section |
|
||||
| `actors/bear/bear_attack_hitboxes.tscn` | The bear's attack colliders as a self-contained sub-scene (instanced under `bear_model`): a `LeapSlamHitbox` (Hitbox + cylinder = ground circle) and `SmashHitbox` (Hitbox + box = forward shockwave lane), plus an `AnimationPlayer` whose `leap_slam` / `smash_travel` clips author each collider's size + travel — scrub to tweak, like `Bear_FX`. `actors/bear/bear.gd` only fires the clip, arms the knock-up shaping, and sweeps for overlaps while it runs (see `_setup_attack_hitboxes` / `_start_attack_hitbox`) |
|
||||
| `actors/matador/sword.tscn` / `systems/combat/hitbox.gd` | The matador's sword wrapper (glb + editor-tweakable `BladeHitbox`). `systems/combat/hitbox.gd` (`class_name Hitbox`, Area3D) is the shared attack collider: `activate()`/`deactivate()` gate an active window, `sweep()` catches bodies already inside (AoE), and knock-up can ramp by distance (`knock_up_min_dist`/`_max_dist`) for the bear's outer-ring launch / further-hit-flies-higher |
|
||||
| `levels/run_state.gd` | Run progression autoload (`Run`) — the lobby hub and which level is being fought, across scene reloads |
|
||||
| `levels/level_def.gd` | `LevelDef` resource — a level's name / `scene_path` (always the shell) / `level_scene` (the level module instanced into the shell) / optional `enemy_scene` (arena wave vs. a boss like the bear) |
|
||||
| `systems/crowd/crowd_marker.gd` | `CrowdMarker` (`@tool Node3D`, `class_name`) — designer drops one per billboard spectator via the editor's Add-Node dialog; picks one of six 60°-spaced facings from the `orientation` dropdown. Draws an editor-only preview (slab + forward arrow, never serialised) and joins the `&"crowd_marker"` group |
|
||||
| `systems/crowd/crowd_billboards.gd` / `systems/crowd/crowd_billboard.gdshader` | `MultiMeshInstance3D` that harvests every `CrowdMarker` (group `&"crowd_marker"`) into one draw call, using each marker's world position + chosen facing. Front/back sprite sheet baked by `tools/bake_crowd_sheet.gd`; falls back to a generated inward-facing ring when no markers are placed |
|
||||
| `systems/debug/debug_params.gd` | Runtime-tunable parameter registry (autoload `DP`). Mobile-relevant flags: `force_touch_controls`, `touch_debug` (also enabled on-device with `?debug=1` in the page URL — shows the live profiler overlay in `systems/input/touch_controls.gd`), and `use_rigid_skin` (default **off** — native GPU skinning everywhere now that the 4.7 web templates fixed the Mali/ANGLE invisible-skinning bug; `systems/render/rigid_skin.gd` is a retained opt-in fallback). Rigid-skin is gated through `Controls.rigid_skin_enabled()`, which a page URL param overrides for on-device A/B with no console: `?skin=1` forces the workaround on (for a device where native skinning still fails), `?noskin=1` forces native off. Turning the workaround off also removes its per-spawn SurfaceTool rebuild, which was the arena's matador-spawn lag |
|
||||
| `systems/input/touch_controls.gd` | On-screen thumb UI for touch/web (`CanvasLayer`, instanced by `ui/hud.gd`, self-gating on `Controls.use_touch_ui()`): a floating left-side joystick feeding the `move_*` actions + a right-side ability cluster, drawn/hit-tested in one IGNORE-filtered Control from raw `_input()`. The joystick self-heals — the latest left-zone touch re-acquires it (`_stick_index`), and a relayout / focus-out / vanished touch resets state so a dropped touchend can't strand a stuck stick. Ability buttons use grown hit-rects + a press flash. `_draw_debug()` (gated on `Controls.debug_overlay()` — DP `touch_debug` or `?debug=1`) is a live on-device profiler reading Godot's `Performance` monitors: FPS/frame-ms with a bottleneck verdict, `process`(main-thread GDScript) vs `physics`(ragdoll/Jolt) ms with 2 s peak-hold, draw calls/objects/primitives, node count, render resolution, and `crossOriginIsolated` — so a lag spike is pinned to CPU-script vs CPU-physics vs GPU-fill instead of guessed |
|
||||
| `ui/web_start_gate.gd` | Autoload `WebStartGate` — "TAP TO PLAY" panel shown on web+touch before play; the tap drives `Controls.request_fullscreen_landscape()` so the fullscreen/orientation churn happens with no in-flight touch to lose. Re-armable: polls browser fullscreen state and returns if the player leaves it (iPhone Safari can't fullscreen, so it stops nagging once a request never takes) |
|
||||
| `ui/mobile_hud.gd` | Mobile-only health readouts (`CanvasLayer`, instanced by `ui/hud.gd` only on touch): big bull pip row top-left + a Souls-style bear boss bar top-centre (discovered via group `&"bear"`, tracks `health_changed`). The desktop bottom pip cluster is hidden on touch |
|
||||
| `ui/orientation_guard.gd` | Autoload `OrientationGuard` — full-screen "rotate your device" prompt shown on touch devices while portrait |
|
||||
| `art/` | All raw art, grouped by role (`characters/ levels/ weapons/ fx/ anim/ textures/ ui/ icons/ fonts/`). GLBs use **Extract Textures** import, so their embedded textures land beside them (e.g. `art/levels/arena_*.png`) named from the GLB — expected, keep them tracked |
|
||||
| `art/blender/` | Blender source files, animation scripts, FBX exports (Godot-ignored via `.gdignore`) |
|
||||
| `art/blender/create_matador_anims.py` | Creates walk + idle animations and exports FBX |
|
||||
| `art/blender/render_anim_preview.py` | Renders animation preview PNGs for visual verification |
|
||||
| `art/blender/preview/` | Output directory for animation preview images |
|
||||
| `tests/` | Headless test scripts (screenshot capture, logic validation) |
|
||||
| `addons/rider-plugin/` | JetBrains Rider IDE integration |
|
||||
|
||||
@@ -70,7 +101,7 @@ I am a **Godot 4.6+ expert**. I follow current best practices for GDScript, scen
|
||||
|
||||
## Animation & bone conventions
|
||||
|
||||
### Blender rig (matador_v02)
|
||||
### Blender rig (matador)
|
||||
- **Armature bones:** `matador` (root) → `COG` → `chest` → `head`, `collarbone_L/R` → `arm_L/R` → `forearm_L/R` → `hand_L/R`, `leg_L/R` → `shin_L/R` → `foot_L/R`
|
||||
- **Rotation mode:** All animated bones use **XYZ Euler** (set in `create_matador_anims.py`)
|
||||
- **Bone-local axes:** Swing (forward/back) is on **local X**. Arm lowering from T-pose is on **local Y** (positive = left arm down, negative = right arm down). Knee bend is **positive X** (shins only bend backward).
|
||||
@@ -88,8 +119,8 @@ After FBX import, animations appear as `Armature|walk` and `Armature|idle` in An
|
||||
|
||||
### Verification workflow
|
||||
1. Edit bones/animations in Blender (`create_matador_anims.py`)
|
||||
2. Run render preview: `blender --background Blender/matador_v02.blend --python Blender/render_anim_preview.py`
|
||||
3. Inspect PNGs in `Blender/preview/` — check bone orientations **before** exporting to Godot
|
||||
2. Run render preview: `blender --background art/blender/matador.blend --python art/blender/render_anim_preview.py`
|
||||
3. Inspect PNGs in `art/blender/preview/` — check bone orientations **before** exporting to Godot
|
||||
4. Export FBX (done automatically by `create_matador_anims.py`)
|
||||
5. Re-import in Godot and test in-game
|
||||
|
||||
@@ -100,12 +131,26 @@ After FBX import, animations appear as `Armature|walk` and `Armature|idle` in An
|
||||
bash run_tests.sh
|
||||
|
||||
# Individual tests:
|
||||
gdlint *.gd levels/*.gd tests/*.gd # GDScript lint (gdtoolkit, installed via uv)
|
||||
gdlint actors systems ui props levels tests # GDScript lint (gdtoolkit, installed via uv)
|
||||
godot --headless --script tests/logic_test.gd # pure logic, ~2 s
|
||||
godot --headless --script tests/harness_selftest.gd # shared test_helpers: seeded RNG + approx + tally, ~1 s
|
||||
godot --headless --script tests/damage_pipeline_test.gd # bull's hit reception (take_sword_hit + typed receive_damage/DamageEvent), ~3 s
|
||||
godot --headless --script tests/fx_gallery_test.gd # every registered Fx effect spawns + self-frees, ~2 s
|
||||
godot --headless --script tests/performance_test.gd # frame-budget regression guard, ~4 s
|
||||
godot --headless --script tests/ragdoll_perf_test.gd # on-hit spike breakdown: ragdoll build / sim-start / blood / mass hit, ~90 s
|
||||
godot --headless --script tests/gameplay_test.gd # full scene, bone sanity, ~4 s
|
||||
godot --headless --script tests/level_switch_test.gd # no arena geometry leaks into the bear level, ~2 s
|
||||
godot --headless --script tests/touch_controls_test.gd # joystick self-heals: relayout/focus-out/vanished-touch never strand a stuck stick, ~2 s
|
||||
godot --headless --script tests/ui_layout_test.gd # UI layout invariants (no pixels): HUD/menu stay on-screen, text fits, tap targets ≥44px, scroll works — across resolutions + pseudo-locale, ~6 s
|
||||
godot --headless --script tests/rigid_skin_test.gd # web mesh conversion keeps every material (no bald / recoloured matadors), ~2 s
|
||||
godot --headless --script tests/bear_boss_test.gd # bear boss moveset: routines, leap landing, hyper-armour, ~10 s
|
||||
godot --headless --script tests/bear_fx_test.gd # bear attack-FX: each Bear_FX clip pops its mesh + self-hides, ~2 s
|
||||
godot --headless --script tests/bear_hitbox_test.gd # bear attack colliders: leap launches everywhere (ring_frac slider) + smash lane travel, ~3 s
|
||||
godot --headless --script tests/bear_ram_test.gd # a horns-first bull charge wounds the bear once; away/wide/slow spared, ~3 s
|
||||
godot --headless --script tests/matador_stab_test.gd # matador's BladeHitbox thrust gores a stationary bull, and a landed stab breaks it off to taunt (anti-spam), ~2 s
|
||||
godot --headless --script tests/matador_taunt_react_test.gd # a taunting matador answers a charge: spear from range, sword up close, ignores non-charges, ~2 s
|
||||
godot --headless --script tests/matador_charge_test.gd # bull's charge is swept (tunnel-proof) so a fast dash still gores; a committed charge pierces the survival roll, ~2 s
|
||||
godot --headless --script tests/matador_spear_test.gd # thrown spear masks the BULL layer and hits a stationary bull, ~2 s
|
||||
|
||||
# Gameplay test with real rendering (inspect frame strip visually):
|
||||
godot --script tests/gameplay_test.gd # opens a window, saves real screenshots
|
||||
|
||||
-137
@@ -1,137 +0,0 @@
|
||||
[gd_scene format=3 uid="uid://c4df4qgpc3d5g"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://eu2lqjhowj4t" path="res://Assets/Matador.glb" id="1_eicjn"]
|
||||
[ext_resource type="Script" uid="uid://dycnoiadljbb1" path="res://Assets/matador_intro_menu.gd" id="2_5jr0i"]
|
||||
[ext_resource type="PackedScene" uid="uid://dio67r8ambe7h" path="res://Assets/arena_placeholder_v01.glb" id="3_1v6n4"]
|
||||
[ext_resource type="PackedScene" uid="uid://dxe72ig8gfbcg" path="res://Assets/arena model2026.glb" id="3_5jr0i"]
|
||||
[ext_resource type="PackedScene" uid="uid://crtm0xtiql3r8" path="res://Player.tscn" id="4_eicjn"]
|
||||
[ext_resource type="Texture2D" uid="uid://3syhacswepyj" path="res://HDRI/kloppenheim_06_puresky_4k.hdr" id="6_1v6n4"]
|
||||
|
||||
[sub_resource type="CylinderShape3D" id="CylinderShape3D_eicjn"]
|
||||
|
||||
[sub_resource type="PanoramaSkyMaterial" id="PanoramaSkyMaterial_w7j4y"]
|
||||
panorama = ExtResource("6_1v6n4")
|
||||
energy_multiplier = 5.6
|
||||
|
||||
[sub_resource type="Sky" id="Sky_3el7n"]
|
||||
sky_material = SubResource("PanoramaSkyMaterial_w7j4y")
|
||||
|
||||
[sub_resource type="Environment" id="Environment_5jr0i"]
|
||||
background_mode = 2
|
||||
background_energy_multiplier = 0.18
|
||||
sky = SubResource("Sky_3el7n")
|
||||
ambient_light_color = Color(0.31061712, 0.31061712, 0.3106171, 1)
|
||||
ambient_light_energy = 16.0
|
||||
ssao_enabled = true
|
||||
ssao_radius = 1.17
|
||||
ssao_intensity = 0.0
|
||||
glow_intensity = 0.34
|
||||
glow_strength = 0.29
|
||||
glow_bloom = 0.1
|
||||
fog_light_color = Color(2.211, 2.211, 2.211, 1)
|
||||
fog_light_energy = 0.01
|
||||
fog_density = 0.0399
|
||||
volumetric_fog_enabled = true
|
||||
volumetric_fog_density = 0.0005
|
||||
volumetric_fog_emission = Color(1, 1, 1, 1)
|
||||
volumetric_fog_emission_energy = 7.85
|
||||
volumetric_fog_anisotropy = 0.9
|
||||
volumetric_fog_length = 69.07
|
||||
|
||||
[sub_resource type="CameraAttributesPractical" id="CameraAttributesPractical_5jr0i"]
|
||||
dof_blur_far_enabled = true
|
||||
dof_blur_far_distance = 26.01
|
||||
dof_blur_near_enabled = true
|
||||
dof_blur_near_distance = 2.6
|
||||
dof_blur_near_transition = 5.0
|
||||
|
||||
[node name="IntroMenu" type="Node3D" unique_id=2130618263]
|
||||
|
||||
[node name="Matador" parent="." unique_id=166852612 instance=ExtResource("1_eicjn")]
|
||||
transform = Transform3D(-0.56715536, 0, -0.8236109, 0, 1, 0, 0.8236109, 0, -0.56715536, 5.2114463, -0.08998108, 1.8927763)
|
||||
script = ExtResource("2_5jr0i")
|
||||
|
||||
[node name="Skeleton3D" parent="Matador/Armature" parent_id_path=PackedInt32Array(166852612, 1447304122) index="0" unique_id=357848305]
|
||||
bones/0/position = Vector3(0.056660548, 0.6424767, -0.0167365)
|
||||
bones/0/rotation = Quaternion(0.013179442, -0.2660563, 0.073319815, 0.9610747)
|
||||
bones/1/rotation = Quaternion(0.06234612, 0.049236443, 0.08183311, 0.99347484)
|
||||
bones/2/rotation = Quaternion(-0.073883414, -0.049860783, -0.17668208, 0.9802238)
|
||||
bones/3/rotation = Quaternion(0.1986702, -0.17794952, -0.71790075, 0.6430262)
|
||||
bones/4/rotation = Quaternion(0.34491858, -0.4473084, -0.25076818, 0.7861691)
|
||||
bones/5/rotation = Quaternion(0.2577085, 0.103201896, -0.6748433, 0.68375605)
|
||||
bones/6/rotation = Quaternion(1.7417646e-08, -1.0305205e-07, -0.05870078, 0.99827564)
|
||||
bones/7/rotation = Quaternion(0.24033272, 0.3103031, 0.5222128, 0.7571301)
|
||||
bones/8/rotation = Quaternion(5.5530215e-08, 1.2911333e-07, 0.055464897, 0.99846065)
|
||||
bones/9/rotation = Quaternion(-6.3051346e-09, -3.6333955e-07, 0.005667881, 0.999984)
|
||||
bones/10/rotation = Quaternion(2.5839595e-08, 9.468444e-08, 0.058700778, 0.99827564)
|
||||
bones/11/rotation = Quaternion(0.39202607, 0.47060946, 0.57993996, 0.53713304)
|
||||
bones/12/rotation = Quaternion(0.8712108, 0.008231261, 0.48523128, 0.073990606)
|
||||
bones/13/rotation = Quaternion(-0.056263044, 0.0027001738, 0.3080972, 0.9496859)
|
||||
bones/14/rotation = Quaternion(0.016082464, 0.024660494, -0.86586916, 0.4994036)
|
||||
bones/15/rotation = Quaternion(0.6067664, -0.3062479, -0.7163194, 0.15790276)
|
||||
bones/16/rotation = Quaternion(-0.0731876, -0.0023153822, -0.47496143, 0.87695485)
|
||||
bones/17/rotation = Quaternion(-0.11824294, -0.17776872, 0.80557114, 0.55269533)
|
||||
bones/18/rotation = Quaternion(0.5305965, -0.28927496, 0.76720744, -0.21489605)
|
||||
bones/20/position = Vector3(0.22012731, 0.38421977, 0.36399037)
|
||||
bones/21/position = Vector3(-0.23671798, 0.052281797, -0.027087089)
|
||||
bones/21/rotation = Quaternion(0.5078295, -0.4920459, -0.4920459, 0.50782955)
|
||||
bones/22/position = Vector3(-0.39721552, 0.31719273, 0.3595722)
|
||||
bones/23/position = Vector3(-0.9167187, 1.3206767, 0.42621902)
|
||||
bones/23/rotation = Quaternion(0.9993525, -0.0021175682, 0.03502641, -0.00795782)
|
||||
bones/24/rotation = Quaternion(0.08874816, -0.018719967, -0.04671622, 0.99478185)
|
||||
bones/25/rotation = Quaternion(0.09634846, -0.0268489, -0.04050248, 0.9941608)
|
||||
|
||||
[node name="matador" parent="Matador/Armature/Skeleton3D" index="0" unique_id=593562242]
|
||||
transform = Transform3D(0.8642424, 0, 0.50307554, 0, 1, 0, -0.50307554, 0, 0.8642424, 0, 0, 0)
|
||||
|
||||
[node name="Muleta" parent="Matador/Armature/Skeleton3D" index="1" unique_id=978680467]
|
||||
cast_shadow = 0
|
||||
|
||||
[node name="Camera3D" type="Camera3D" parent="." unique_id=1531255459]
|
||||
transform = Transform3D(0.42176014, 0.2362673, 0.8753835, -0.10687317, 0.97167695, -0.21076548, -0.900387, -0.0046625203, 0.4350652, 8.41945, 0.17132203, 2.0652685)
|
||||
fov = 41.6
|
||||
near = 0.001
|
||||
|
||||
[node name="arena model2026" parent="." unique_id=179544183 instance=ExtResource("3_5jr0i")]
|
||||
transform = Transform3D(-0.83746713, 0, 0.54648757, 0, 0.99999994, 0, -0.54648757, 0, -0.83746713, 3.8733478, -0.6, -1.1080065)
|
||||
|
||||
[node name="arena_placeholder_v01" parent="." unique_id=1343590504 instance=ExtResource("3_1v6n4")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.3344436, 0, -1.5401263)
|
||||
visible = false
|
||||
|
||||
[node name="StaticBody3D" type="StaticBody3D" parent="arena_placeholder_v01" unique_id=340133176]
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="arena_placeholder_v01/StaticBody3D" unique_id=201484167]
|
||||
transform = Transform3D(-33.265, 4.9526625e-07, -2.9081186e-06, 5.0228755e-06, 3.28, 0, -2.9081186e-06, 4.329755e-14, 33.265, 0, -3.3065948, 0)
|
||||
shape = SubResource("CylinderShape3D_eicjn")
|
||||
|
||||
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=660229295]
|
||||
transform = Transform3D(0.9491592, 0.31479403, -0.0014368258, -0.28033537, 0.84731936, 0.45106864, 0.14321098, -0.4277331, 0.89248836, 1.1162033, 7.3956842, -4.480528)
|
||||
light_energy = 1.32
|
||||
shadow_enabled = true
|
||||
shadow_blur = 8.689
|
||||
|
||||
[node name="DirectionalLight3D2" type="DirectionalLight3D" parent="." unique_id=498871251]
|
||||
transform = Transform3D(0.835524, -0.5376153, 0.11344351, -0.48260456, -0.6193575, 0.6192651, -0.26266432, -0.572159, -0.7769435, 1.1162033, 7.3956842, -4.480528)
|
||||
visible = false
|
||||
light_energy = 0.415
|
||||
light_indirect_energy = 16.0
|
||||
light_volumetric_fog_energy = 1.11
|
||||
shadow_blur = 10.0
|
||||
|
||||
[node name="Player" parent="." unique_id=165510044 instance=ExtResource("4_eicjn")]
|
||||
transform = Transform3D(0.9563367, 0, 0.29226705, 0, 1, 0, -0.29226705, 0, 0.9563367, 3.0920272, 0.39692497, -4.302477)
|
||||
|
||||
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=114793752]
|
||||
environment = SubResource("Environment_5jr0i")
|
||||
camera_attributes = SubResource("CameraAttributesPractical_5jr0i")
|
||||
|
||||
[node name="SpotLight3D" type="SpotLight3D" parent="." unique_id=1428020323]
|
||||
transform = Transform3D(0.94930077, -0.05846006, -0.30888584, 0.21308771, 0.8420559, 0.4955155, 0.23113126, -0.53621286, 0.81182146, -20.807917, 10.690277, 20.135067)
|
||||
visible = false
|
||||
light_energy = 16.0
|
||||
light_volumetric_fog_energy = 16.0
|
||||
shadow_enabled = true
|
||||
spot_range = 84.316696
|
||||
|
||||
[editable path="Matador"]
|
||||
@@ -1,2 +0,0 @@
|
||||
[InternetShortcut]
|
||||
URL=https://www.youtube.com/watch?v=ZCb12AHKMfE
|
||||
+232
-69
@@ -26,7 +26,25 @@ var _phase: Phase = Phase.WINDUP
|
||||
var _bull: CharacterBody3D = null
|
||||
var _skeleton: Skeleton3D = null
|
||||
var _anim_player: AnimationPlayer = null
|
||||
# Separate player baked into art/fx/bear_fx.glb: each clip scales one FX mesh up from 0
|
||||
# and back at its strike frame (self-hiding), so bear.gd just fires the matching clip.
|
||||
var _fx_player: AnimationPlayer = null
|
||||
var _fx_root: Node = null
|
||||
# Shaped attack colliders live in bear_attack_hitboxes.tscn (under bear_model, so they track the
|
||||
# bear's facing): a ground-circle cylinder for the leap slam and a forward box lane for the smash.
|
||||
# Their sizes AND their travel/pop motion are authored in that scene's AnimationPlayer (clips
|
||||
# "leap_slam" / "smash_travel") — scrub them there to tweak. bear.gd only fires the clip, sets the
|
||||
# knock-up shaping, activates monitoring, and sweeps for overlaps while the clip runs.
|
||||
var _leap_hitbox: Hitbox = null
|
||||
var _smash_hitbox: Hitbox = null
|
||||
var _hitbox_anim: AnimationPlayer = null
|
||||
var _active_hitbox: Hitbox = null
|
||||
var _blood_burst: CPUParticles3D = null
|
||||
# Bull-ram tracking: the bull's position last frame (to sweep its travel) and whether it was mid-ram
|
||||
# last frame (edge-trigger so one charge = one wound, re-arming when the bull breaks off).
|
||||
var _bull_prev_pos: Vector3 = Vector3.ZERO
|
||||
var _bull_prev_seen: bool = false
|
||||
var _bull_ramming: bool = false
|
||||
|
||||
var _hp: int = 10
|
||||
var _enraged: bool = false
|
||||
@@ -78,12 +96,10 @@ const _BULL_STANDOFF: float = 1.9
|
||||
@onready var _mesh: Node3D = $bear_model
|
||||
@onready var _hit_area: Area3D = $HitArea
|
||||
var _dust: CPUParticles3D = null
|
||||
# Claw-slash bursts bound to the paw bones, fired one per hit of the 1-2 punch.
|
||||
var _claw_l: CPUParticles3D = null
|
||||
var _claw_r: CPUParticles3D = null
|
||||
|
||||
|
||||
const RigidSkin = preload("res://rigid_skin.gd")
|
||||
|
||||
const RigidSkin = preload("res://systems/render/rigid_skin.gd")
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -93,9 +109,17 @@ func _ready() -> void:
|
||||
health_changed.emit(_hp, _hp)
|
||||
_skeleton = _find_skeleton(_mesh)
|
||||
_anim_player = _find_anim_player(_mesh)
|
||||
# Web (Mali/ANGLE) can't run Compatibility vertex-skinning; rebuild the bear as
|
||||
# non-skinned bone-attached pieces there. Desktop keeps smooth GPU skinning.
|
||||
if OS.has_feature("web"):
|
||||
_fx_root = _mesh.get_node_or_null("Bear_FX")
|
||||
if _fx_root:
|
||||
_fx_player = _fx_root.get_node_or_null("AnimationPlayer") as AnimationPlayer
|
||||
if _fx_player:
|
||||
# The smash/leap clips end held at full scale (Siim only ramped the claw clips back
|
||||
# down), so snap each effect back to 0 when its clip finishes — keeps them one-shot.
|
||||
_fx_player.animation_finished.connect(_on_fx_finished)
|
||||
# Native GPU skinning by default (the Mali/ANGLE invisible-skinning bug is fixed in the 4.7
|
||||
# web templates). The bear is a single dense organic mesh, so the rigid-skin fallback tears
|
||||
# seam holes as it animates — another reason native is the default. See rigid_skin_enabled().
|
||||
if Controls.rigid_skin_enabled():
|
||||
RigidSkin.convert_tree(self)
|
||||
if _anim_player:
|
||||
_anim_idle = _resolve_anim(["IDLE_ON4LEGS", "IDLE"])
|
||||
@@ -113,12 +137,11 @@ func _ready() -> void:
|
||||
for a in [_anim_swipe, _anim_smash, _anim_leap, _anim_death]:
|
||||
_set_no_loop(a)
|
||||
_anim_player.playback_default_blend_time = 0.12
|
||||
_hit_area.body_entered.connect(_on_body_entered)
|
||||
_blood_burst = preload("res://blood_burst.gd").new()
|
||||
_blood_burst = preload("res://systems/fx/blood_burst.gd").new()
|
||||
add_child(_blood_burst)
|
||||
_dust = _make_slam_dust()
|
||||
add_child(_dust)
|
||||
_setup_claws()
|
||||
_setup_attack_hitboxes()
|
||||
_enter_neutral()
|
||||
|
||||
#take hit flashing
|
||||
@@ -166,8 +189,6 @@ func _clear_hit_flash() -> void:
|
||||
func _setup_claws() -> void:
|
||||
if _skeleton == null:
|
||||
return
|
||||
_claw_r = _attach_claw("hand_r")
|
||||
_claw_l = _attach_claw("hand_l")
|
||||
|
||||
|
||||
func _attach_claw(bone: String) -> CPUParticles3D:
|
||||
@@ -216,6 +237,9 @@ func _physics_process(delta: float) -> void:
|
||||
_acquire_bull()
|
||||
_update_enrage()
|
||||
_stagger_cd = maxf(0.0, _stagger_cd - delta)
|
||||
_tick_attack_hitbox()
|
||||
if _bull != null:
|
||||
_sweep_bull_ram()
|
||||
|
||||
match _state:
|
||||
State.NEUTRAL: _tick_neutral(delta)
|
||||
@@ -285,15 +309,19 @@ func _decide() -> void:
|
||||
|
||||
var dist := _flat_dist_to_bull()
|
||||
var options: Array
|
||||
|
||||
if dist <= DP.f("bear_close_range"):
|
||||
# In strike range: punch or overhead (both step in during the swing).
|
||||
options = [[State.SWIPE, 3.0], [State.SMASH, 3.0 if _enraged else 2.5]]
|
||||
elif dist <= DP.f("bear_mid_range"):
|
||||
# Mid: mostly walk the bull down on two legs; occasionally leap the gap.
|
||||
options = [[State.STALK, 3.0], [State.LEAP, 2.0 if _enraged else 1.5]]
|
||||
# Short range: claw attack
|
||||
options = [[State.SWIPE, 3.0]]
|
||||
|
||||
else:
|
||||
# Far: lope in on all fours, or spring across with a leap.
|
||||
options = [[State.PROWL, 3.0], [State.LEAP, 1.5]]
|
||||
# At distance: overhead smash 1/3 of the time, jump smash 2/3 — both close the gap as
|
||||
# they land, so the bear slams its way in rather than walking (stalk/prowl).
|
||||
options = [
|
||||
[State.SMASH, 1.0],
|
||||
[State.LEAP, 2.0]
|
||||
]
|
||||
|
||||
_start_routine(_weighted_pick(options))
|
||||
|
||||
|
||||
@@ -302,12 +330,21 @@ func _decide() -> void:
|
||||
func _range_allows(routine: State) -> bool:
|
||||
if _bull == null:
|
||||
return routine == State.PROWL
|
||||
|
||||
var dist := _flat_dist_to_bull()
|
||||
|
||||
match routine:
|
||||
State.SWIPE, State.SMASH:
|
||||
State.SWIPE:
|
||||
return dist <= DP.f("bear_close_range") * 1.4
|
||||
|
||||
State.LEAP:
|
||||
return dist <= DP.f("bear_mid_range") * 1.5
|
||||
|
||||
State.SMASH:
|
||||
# Smash can be started from any distance.
|
||||
# The attack itself will move toward the bull.
|
||||
return true
|
||||
|
||||
return true
|
||||
|
||||
|
||||
@@ -394,12 +431,12 @@ func _tick_swipe(delta: float) -> void:
|
||||
var frac := 1.0 - _phase_timer / maxf(_active_len, 0.001)
|
||||
if _hits_done == 0 and frac >= 0.25:
|
||||
_hits_done = 1
|
||||
_play_fx(_claw_fx_clip(0))
|
||||
_try_hit(DP.f("bear_swipe_reach"))
|
||||
_claw_slash(true) # first paw (right) rakes across
|
||||
elif _hits_done == 1 and frac >= 0.65:
|
||||
_hits_done = 2
|
||||
_play_fx(_claw_fx_clip(1))
|
||||
_try_hit(DP.f("bear_swipe_reach"))
|
||||
_claw_slash(false) # back-hand with the left paw
|
||||
if _phase_timer <= 0.0:
|
||||
_begin_recover(DP.f("bear_swipe_recover"))
|
||||
Phase.RECOVER:
|
||||
@@ -426,13 +463,16 @@ func _tick_smash(delta: float) -> void:
|
||||
if _phase_timer <= 0.0:
|
||||
_begin_attack_active(_anim_smash, 0.9)
|
||||
Phase.ACTIVE:
|
||||
# The overhead steps in less than the swipe — it's a commitment, not a rush.
|
||||
_lunge_and_track(DP.f("bear_lunge_speed") * 0.5, delta)
|
||||
# The overhead steps in less than the swipe — it's a commitment, not a rush. no rotation
|
||||
_lunge_and_track_no_rotation(DP.f("bear_lunge_speed") * 0.5, delta)
|
||||
_phase_timer -= delta
|
||||
var frac := 1.0 - _phase_timer / maxf(_active_len, 0.001)
|
||||
if _hits_done == 0 and frac >= 0.4:
|
||||
_hits_done = 1
|
||||
_slam(DP.f("bear_smash_radius"), 0.8)
|
||||
_play_fx(&"Bear_fx_smash_hit")
|
||||
_slam_dust()
|
||||
_shake(0.8)
|
||||
_begin_smash_hitbox() # plays the scene's "smash_travel" clip; swept in _tick_attack_hitbox
|
||||
if _phase_timer <= 0.0:
|
||||
_begin_recover(DP.f("bear_smash_recover"))
|
||||
Phase.RECOVER:
|
||||
@@ -483,7 +523,8 @@ func _tick_leap(delta: float) -> void:
|
||||
elif _leap_airborne or _phase_timer <= 0.0:
|
||||
velocity.x = 0.0
|
||||
velocity.z = 0.0
|
||||
_slam(DP.f("bear_slam_radius"), 0.7)
|
||||
_play_fx(&"Bear_fx_jump_smash")
|
||||
_fire_leap_hitbox()
|
||||
_phase = Phase.RECOVER
|
||||
_phase_timer = _scaled(DP.f("bear_leap_recover"))
|
||||
Phase.RECOVER:
|
||||
@@ -561,6 +602,20 @@ func _lunge_and_track(speed: float, delta: float) -> void:
|
||||
_decelerate(_PLANT_DECEL, delta)
|
||||
|
||||
|
||||
func _lunge_and_track_no_rotation(speed: float, delta: float) -> void:
|
||||
if _bull == null:
|
||||
_decelerate(_PLANT_DECEL, delta)
|
||||
return
|
||||
|
||||
var to_bull := _flat_to_bull()
|
||||
var dist := to_bull.length()
|
||||
|
||||
if speed > 0.0 and dist > _BULL_STANDOFF:
|
||||
_accelerate(to_bull / dist, speed, delta)
|
||||
else:
|
||||
_decelerate(_PLANT_DECEL, delta)
|
||||
|
||||
|
||||
# ── Hitting the bull ───────────────────────────────────────────────────────────────
|
||||
|
||||
# Directional strike: the bull is mauled inside `reach` of the bear that's facing it
|
||||
@@ -575,22 +630,88 @@ func _try_hit(reach: float) -> bool:
|
||||
return false
|
||||
if _facing().dot(to_bull / dist) < cos(deg_to_rad(DP.f("bear_hit_arc"))):
|
||||
return false
|
||||
_bull.call(&"take_sword_hit", "mauled")
|
||||
DamageEvent.deal(_bull, DamageEvent.Cause.MAULED, _bull.global_position, self)
|
||||
_shake(0.55)
|
||||
return true
|
||||
|
||||
|
||||
# Radial slam (smash / leap landing): hits any way the bull is, within `radius`, with a
|
||||
# ground-pound shake and a burst of dust kicked up at the paws. No facing arc — you're
|
||||
# caught if you're near where it comes down.
|
||||
func _slam(radius: float, shake: float) -> void:
|
||||
_slam_dust()
|
||||
if _bull == null:
|
||||
_shake(shake)
|
||||
# Grab the designer-authored collider sub-scene (bear_attack_hitboxes.tscn instanced under
|
||||
# bear_model) and park both hitboxes deactivated. Shapes + travel are authored in that scene.
|
||||
func _setup_attack_hitboxes() -> void:
|
||||
var root := _mesh.get_node_or_null("AttackHitboxes") if _mesh else null
|
||||
if root == null:
|
||||
return
|
||||
if _flat_dist_to_bull() <= radius:
|
||||
_bull.call(&"take_sword_hit", "mauled")
|
||||
_shake(shake)
|
||||
_leap_hitbox = root.get_node_or_null("LeapSlamHitbox") as Hitbox
|
||||
_smash_hitbox = root.get_node_or_null("SmashHitbox") as Hitbox
|
||||
_hitbox_anim = root.get_node_or_null("AnimationPlayer") as AnimationPlayer
|
||||
if _hitbox_anim != null:
|
||||
# Advance the shape's travel in the physics step so it stays in lockstep with the sweep.
|
||||
_hitbox_anim.callback_mode_process = AnimationMixer.ANIMATION_CALLBACK_MODE_PROCESS_PHYSICS
|
||||
if _leap_hitbox != null:
|
||||
_leap_hitbox.deactivate()
|
||||
if _smash_hitbox != null:
|
||||
_smash_hitbox.deactivate()
|
||||
|
||||
|
||||
# Leap landing: kick up dust + shake, launch the ground-circle collider clip, and arm it so the bull
|
||||
# is launched from ANYWHERE in the circle (bear_leap_ring_frac 0). Raise ring_frac to restore a
|
||||
# grounded inner zone that only mauls; the launch zone then starts at that fraction of the radius.
|
||||
func _fire_leap_hitbox() -> void:
|
||||
_slam_dust()
|
||||
_shake(0.7)
|
||||
if _leap_hitbox == null:
|
||||
return
|
||||
_leap_hitbox.knock_up = DP.f("bear_leap_knockup")
|
||||
_leap_hitbox.knock_up_min_dist = DP.f("bear_leap_ring_frac") * _shape_extent(_leap_hitbox, true)
|
||||
_leap_hitbox.knock_up_max_dist = 0.0 # binary: full-height launch everywhere it launches
|
||||
_start_attack_hitbox(_leap_hitbox, &"leap_slam")
|
||||
|
||||
|
||||
# Overhead smash: launch the lane clip; knock-up ramps 0→full with distance (further = higher),
|
||||
# maxing at the lane's authored full length so the shape and the launch curve stay in sync.
|
||||
func _begin_smash_hitbox() -> void:
|
||||
if _smash_hitbox == null:
|
||||
return
|
||||
_smash_hitbox.knock_up = DP.f("bear_smash_knockup")
|
||||
_smash_hitbox.knock_up_min_dist = 0.0
|
||||
_smash_hitbox.knock_up_max_dist = _shape_extent(_smash_hitbox, false)
|
||||
_start_attack_hitbox(_smash_hitbox, &"smash_travel")
|
||||
|
||||
|
||||
# Activate the hitbox and play its authored motion clip; _tick_attack_hitbox sweeps for overlaps
|
||||
# each frame while the clip runs, then deactivates when it ends.
|
||||
func _start_attack_hitbox(hb: Hitbox, clip: StringName) -> void:
|
||||
if _active_hitbox != null and _active_hitbox != hb:
|
||||
_active_hitbox.deactivate()
|
||||
hb.activate()
|
||||
_active_hitbox = hb
|
||||
if _hitbox_anim != null and _hitbox_anim.has_animation(clip):
|
||||
_hitbox_anim.play(clip)
|
||||
_hitbox_anim.seek(0.0, true)
|
||||
|
||||
|
||||
func _tick_attack_hitbox() -> void:
|
||||
if _active_hitbox == null:
|
||||
return
|
||||
_active_hitbox.sweep()
|
||||
# The clip drives the shape's travel; when it stops, the swing is over — close the hitbox.
|
||||
if _hitbox_anim == null or not _hitbox_anim.is_playing():
|
||||
_active_hitbox.deactivate()
|
||||
_active_hitbox = null
|
||||
|
||||
|
||||
# Full extent (planar) of a hitbox's authored shape, for the knock-up distance math: the circle's
|
||||
# radius (cylinder) or the lane's length (box half-size along local +Z, forward from the bear).
|
||||
func _shape_extent(hb: Hitbox, radial: bool) -> float:
|
||||
var cs := hb.get_node_or_null("CollisionShape3D") as CollisionShape3D
|
||||
if cs == null:
|
||||
return 0.0
|
||||
if radial and cs.shape is CylinderShape3D:
|
||||
return (cs.shape as CylinderShape3D).radius
|
||||
if not radial and cs.shape is BoxShape3D:
|
||||
# Box spans [0, size.z] because it's offset forward by half its length.
|
||||
return (cs.shape as BoxShape3D).size.z
|
||||
return 0.0
|
||||
|
||||
|
||||
# Kick up a one-shot ring of dust at the bear's feet where the slam lands.
|
||||
@@ -651,15 +772,6 @@ func _make_slam_dust() -> CPUParticles3D:
|
||||
return p
|
||||
|
||||
|
||||
# Fire the claw-slash burst on one paw for a swipe hit (right paw first, then left).
|
||||
func _claw_slash(right: bool) -> void:
|
||||
var claw := _claw_r if right else _claw_l
|
||||
if claw == null:
|
||||
return
|
||||
claw.restart()
|
||||
claw.emitting = true
|
||||
|
||||
|
||||
# A short fan of pale streaks raked from a paw — thin boxes aligned to their velocity so
|
||||
# they read as claw marks slashing outward. Built in code and parented to a hand bone.
|
||||
func _make_claw_slash() -> CPUParticles3D:
|
||||
@@ -723,18 +835,44 @@ func apply_ability_hit(hit_dir: Vector3, strength: float, _up_boost: float = 0.0
|
||||
_take_hit(hit_dir, strength)
|
||||
|
||||
|
||||
# Ramming: only a real charge from the bull wounds the bear; a gentle nudge is shrugged
|
||||
# off. The bear never damages the bull by colliding — only its swings/slams do.
|
||||
func _on_body_entered(body: Node3D) -> void:
|
||||
if _state == State.DEAD or not body.is_in_group(&"player"):
|
||||
# Only a horns-first charge wounds the bear. Swept (prev → now) so a tunnelling or point-blank charge
|
||||
# still lands where body_entered wouldn't; edge-triggered (_bull_ramming) so one charge = one wound.
|
||||
func _sweep_bull_ram() -> void:
|
||||
var now := _bull.global_position
|
||||
var seg_from := _bull_prev_pos if _bull_prev_seen else now
|
||||
_bull_prev_pos = now
|
||||
_bull_prev_seen = true
|
||||
if _state == State.DEAD:
|
||||
_bull_ramming = false
|
||||
return
|
||||
var player := body as CharacterBody3D
|
||||
if player.velocity.length() < DP.f("bear_hit_threshold"):
|
||||
return
|
||||
var flat := Vector3(player.velocity.x, 0.0, player.velocity.z)
|
||||
var dir := flat.normalized() if flat.length() > 0.5 else \
|
||||
(global_position - player.global_position).normalized()
|
||||
_take_hit(dir, player.velocity.length())
|
||||
var vel := Vector3(_bull.velocity.x, 0.0, _bull.velocity.z)
|
||||
var speed := vel.length()
|
||||
var ramming := false
|
||||
if speed >= DP.f("bear_hit_threshold") and _horns_first(seg_from):
|
||||
var a := Vector3(seg_from.x, 0.0, seg_from.z)
|
||||
var b := Vector3(now.x, 0.0, now.z)
|
||||
var p := Vector3(global_position.x, 0.0, global_position.z)
|
||||
if p.distance_to(Geometry3D.get_closest_point_to_segment(p, a, b)) <= DP.f("bear_ram_reach"):
|
||||
ramming = true
|
||||
if ramming and not _bull_ramming:
|
||||
var dir := vel.normalized() if speed > 0.5 else (global_position - now).normalized()
|
||||
_take_hit(dir, speed)
|
||||
_bull_ramming = ramming
|
||||
|
||||
|
||||
# Horns-first gate: wound only when the bull's velocity points within bull_gore_arc of the direction
|
||||
# from `from` to the bear. The dash ability skips the gate.
|
||||
func _horns_first(from: Vector3) -> bool:
|
||||
if _bull.has_method(&"is_dashing") and _bull.call(&"is_dashing"):
|
||||
return true # the dash ability is a committed lunge — it rams on contact, no angle gate
|
||||
var vel := Vector3(_bull.velocity.x, 0.0, _bull.velocity.z)
|
||||
if vel.length() < 0.5:
|
||||
return false
|
||||
var to_bear := global_position - from
|
||||
to_bear.y = 0.0
|
||||
if to_bear.length() < 0.01:
|
||||
return true
|
||||
return vel.normalized().dot(to_bear.normalized()) > cos(deg_to_rad(DP.f("bull_gore_arc")))
|
||||
|
||||
|
||||
func _take_hit(hit_dir: Vector3, strength: float) -> void:
|
||||
@@ -782,18 +920,6 @@ func _die(hit_dir: Vector3) -> void:
|
||||
velocity = Vector3.ZERO
|
||||
_blood_burst.burst(global_position + Vector3(0.0, 1.4, 0.0), hit_dir)
|
||||
_play(_anim_death)
|
||||
# No death clip on the rig — topple the beast onto its side (about the hit direction,
|
||||
# so it falls the way it was struck) and sink it away, then free.
|
||||
#var fall_axis := Vector3(-hit_dir.z, 0.0, hit_dir.x)
|
||||
#if fall_axis.length() < 0.01:
|
||||
# fall_axis = Vector3.RIGHT
|
||||
#var tween := create_tween()
|
||||
#tween.tween_property(_mesh, "rotation",
|
||||
# _mesh.rotation + fall_axis.normalized() * deg_to_rad(88.0), 0.7)\
|
||||
# .set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
|
||||
#tween.tween_interval(2.5)
|
||||
#tween.tween_property(_mesh, "position:y", _mesh.position.y - 3.0, 1.0)
|
||||
#tween.tween_callback(queue_free)
|
||||
|
||||
|
||||
# ── Interface shared with the matador (guarded elsewhere via has_method) ─────────────
|
||||
@@ -907,6 +1033,43 @@ func _play(anim_name: StringName) -> void:
|
||||
_anim_player.play(anim_name)
|
||||
|
||||
|
||||
# The two swipe hits map to the L/R claw-slash clips; bear_fx_claw_swap flips the order in
|
||||
# case the baked left/right clips read reversed in-game. hit 0 = first swing, 1 = second.
|
||||
func _claw_fx_clip(hit: int) -> StringName:
|
||||
var swap := DP.f("bear_fx_claw_swap") >= 0.5
|
||||
var first_left := not swap
|
||||
if (hit == 0) == first_left:
|
||||
return &"Bear_fx_claw_attack_L"
|
||||
return &"Bear_fx_claw_attack_R"
|
||||
|
||||
|
||||
# Fire a one-shot FX clip from the Bear_FX glb (crater / slash / debris). Each clip scales
|
||||
# its mesh up at the strike and back to 0 on its own, so this is a plain fire-and-forget.
|
||||
func _play_fx(clip: StringName) -> void:
|
||||
if _fx_player == null or not _fx_player.has_animation(clip):
|
||||
return
|
||||
if _fx_root is Node3D:
|
||||
(_fx_root as Node3D).rotation.y = deg_to_rad(DP.f("bear_fx_yaw"))
|
||||
_fx_player.play(clip)
|
||||
_fx_player.seek(0.0, true)
|
||||
|
||||
|
||||
func _on_fx_finished(anim: StringName) -> void:
|
||||
if _fx_root == null:
|
||||
return
|
||||
var mesh := _fx_root.get_node_or_null(NodePath(anim)) as Node3D
|
||||
if mesh == null:
|
||||
return
|
||||
# The crater clip only ramps up (it ends held at full scale with no shrink), so leave it on
|
||||
# the ground for a beat after the slam, then hide it. The others self-clear immediately.
|
||||
if anim == &"Bear_fx_jump_smash":
|
||||
var linger := DP.f("bear_fx_crater_linger")
|
||||
if linger > 0.0:
|
||||
get_tree().create_timer(linger).timeout.connect(func() -> void: mesh.scale = Vector3.ZERO)
|
||||
return
|
||||
mesh.scale = Vector3.ZERO
|
||||
|
||||
|
||||
# Restart a one-shot attack clip from its first frame at `speed` (even if already current).
|
||||
func _play_attack(anim_name: StringName, speed: float) -> void:
|
||||
if _anim_player == null or anim_name == &"":
|
||||
@@ -0,0 +1,44 @@
|
||||
[gd_scene format=3 uid="uid://o8ocjfprxa7u"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://cn7fhjkro7637" path="res://actors/bear/bear.gd" id="1_beargd"]
|
||||
[ext_resource type="PackedScene" uid="uid://cofv5dbavesis" path="res://art/characters/bear.glb" id="2_bearglb"]
|
||||
[ext_resource type="PackedScene" uid="uid://bdbx0wwa73rhp" path="res://art/fx/bear_fx.glb" id="3_bearfx"]
|
||||
[ext_resource type="Material" uid="uid://blpkmyikjnhbo" path="res://art/materials/bear_claw_fx.tres" id="4_clawfx"]
|
||||
[ext_resource type="PackedScene" path="res://actors/bear/bear_attack_hitboxes.tscn" id="5_hitboxes"]
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_body"]
|
||||
radius = 1.0
|
||||
height = 2.4
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_hit"]
|
||||
radius = 1.3
|
||||
height = 2.7
|
||||
|
||||
[node name="Bear" type="CharacterBody3D" unique_id=1573237287]
|
||||
script = ExtResource("1_beargd")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=830443382]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.2, 0)
|
||||
shape = SubResource("CapsuleShape3D_body")
|
||||
|
||||
[node name="HitArea" type="Area3D" parent="." unique_id=365327397]
|
||||
monitorable = false
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="HitArea" unique_id=2111867348]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.35, 0)
|
||||
shape = SubResource("CapsuleShape3D_hit")
|
||||
|
||||
[node name="bear_model" parent="." unique_id=359217823 instance=ExtResource("2_bearglb")]
|
||||
|
||||
[node name="Bear_FX" parent="bear_model" unique_id=2070553519 instance=ExtResource("3_bearfx")]
|
||||
|
||||
[node name="Bear_fx_claw_attack_R" parent="bear_model/Bear_FX" index="2" unique_id=1939248432]
|
||||
surface_material_override/0 = ExtResource("4_clawfx")
|
||||
|
||||
[node name="Bear_fx_claw_attack_L" parent="bear_model/Bear_FX" index="3" unique_id=834640978]
|
||||
surface_material_override/0 = ExtResource("4_clawfx")
|
||||
|
||||
[node name="AttackHitboxes" parent="bear_model" instance=ExtResource("5_hitboxes")]
|
||||
|
||||
[editable path="bear_model"]
|
||||
[editable path="bear_model/Bear_FX"]
|
||||
@@ -0,0 +1,70 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://systems/combat/hitbox.gd" id="1_qxn4a"]
|
||||
|
||||
[sub_resource type="CylinderShape3D" id="CylinderShape3D_6dow8"]
|
||||
height = 3.0
|
||||
radius = 4.0
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_tmmfg"]
|
||||
size = Vector3(2.6, 2.5, 20)
|
||||
|
||||
[sub_resource type="Animation" id="Animation_wcwtv"]
|
||||
length = 0.25
|
||||
tracks/0/type = "scale_3d"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath("LeapSlamHitbox/CollisionShape3D")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = PackedFloat32Array(0, 1, 0.12, 1, 0.12, 0.06, 1, 1, 1, 1, 0.25, 1, 1, 1, 1)
|
||||
|
||||
[sub_resource type="Animation" id="Animation_bg5u7"]
|
||||
length = 0.3
|
||||
tracks/0/type = "scale_3d"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath("SmashHitbox/CollisionShape3D")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = PackedFloat32Array(0, 1, 1, 1, 0.02, 0.1, 1, 1, 1, 1, 0.3, 1, 1, 1, 1)
|
||||
tracks/1/type = "position_3d"
|
||||
tracks/1/imported = false
|
||||
tracks/1/enabled = true
|
||||
tracks/1/path = NodePath("SmashHitbox/CollisionShape3D")
|
||||
tracks/1/interp = 1
|
||||
tracks/1/loop_wrap = true
|
||||
tracks/1/keys = PackedFloat32Array(0, 1, 0, 0.9, 0.2, 0.1, 1, 0, 0.9, 10, 0.3, 1, 0, 0.9, 10)
|
||||
|
||||
[sub_resource type="AnimationLibrary" id="AnimationLibrary_u1id2"]
|
||||
_data = {
|
||||
&"leap_slam": SubResource("Animation_wcwtv"),
|
||||
&"smash_travel": SubResource("Animation_bg5u7")
|
||||
}
|
||||
|
||||
[node name="AttackHitboxes" type="Node3D" unique_id=2010404706]
|
||||
|
||||
[node name="LeapSlamHitbox" type="Area3D" parent="." unique_id=1751893032]
|
||||
collision_layer = 0
|
||||
collision_mask = 2
|
||||
monitoring = false
|
||||
monitorable = false
|
||||
script = ExtResource("1_qxn4a")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="LeapSlamHitbox" unique_id=1320172052]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)
|
||||
shape = SubResource("CylinderShape3D_6dow8")
|
||||
|
||||
[node name="SmashHitbox" type="Area3D" parent="." unique_id=1579648726]
|
||||
collision_layer = 0
|
||||
collision_mask = 2
|
||||
monitoring = false
|
||||
monitorable = false
|
||||
script = ExtResource("1_qxn4a")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="SmashHitbox" unique_id=528637962]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.9, 10)
|
||||
shape = SubResource("BoxShape3D_tmmfg")
|
||||
|
||||
[node name="AnimationPlayer" type="AnimationPlayer" parent="." unique_id=2139876757]
|
||||
libraries/ = SubResource("AnimationLibrary_u1id2")
|
||||
@@ -1,13 +1,9 @@
|
||||
extends CharacterBody3D
|
||||
|
||||
# WANDER → bull outside flee range, roaming freely
|
||||
# FLEE → bull within range, matador backs away (raycast-steered around walls)
|
||||
# BRACE → charge detected: plant feet, face the bull (cite phase)
|
||||
# SIDESTEP→ bull commits: sharp lateral step to let it pass (pase phase)
|
||||
# ATTACK → post-dodge or mid-range: chase and strike
|
||||
# ROLL → hit by bull but roll chance fires: survive and recover
|
||||
# THROW → planted, winding up an overhand throw, then releasing the sword
|
||||
# RAGDOLL → hit by bull, no roll chance
|
||||
# WANDER → roam/taunt outside flee range. FLEE → bull close: back away (raycast-steered).
|
||||
# BRACE → charge detected: plant + face (cite). SIDESTEP → bull commits: lateral pass (pase).
|
||||
# ATTACK → chase and strike. ROLL → hit but rolled: survive. THROW → wind up + hurl the spear.
|
||||
# RAGDOLL → hit, no roll.
|
||||
enum State { WANDER, FLEE, BRACE, SIDESTEP, ATTACK, ROLL, THROW, RAGDOLL }
|
||||
|
||||
# The matador's high-level *want*, chosen by a weighted utility vote (see
|
||||
@@ -32,7 +28,9 @@ var _dodge_cd: float = 0.0
|
||||
var _will_dodge: bool = true
|
||||
var _attack_timer: float = 0.0
|
||||
var _swing_timer: float = 0.0
|
||||
var _lunge_timer: float = 0.0
|
||||
var _attack_cd: float = 0.0
|
||||
var _bull_prev_pos: Vector3 = Vector3.ZERO
|
||||
var _bull_prev_seen: bool = false
|
||||
var _roll_timer: float = 0.0
|
||||
var _roll_dir: Vector3 = Vector3.ZERO
|
||||
var _is_spawning: bool = false
|
||||
@@ -52,7 +50,7 @@ var _drawing: bool = false
|
||||
var _sheathing: bool = false
|
||||
var _draw_timer: float = 0.0
|
||||
var _draw_len: float = 0.0
|
||||
var _sword_blade_area: Area3D = null
|
||||
var _sword_blade_area: Hitbox = null
|
||||
var _spear_node: Node3D = null
|
||||
var _blade_hit_cd: float = 0.0
|
||||
var _death_player: AudioStreamPlayer = null
|
||||
@@ -76,8 +74,9 @@ const _ANIM_TAUNTS: Array[StringName] = [&"Taunt", &"Taunt_B", &"Taunt_C"]
|
||||
# reparents from holster to hand at this point in the animation.
|
||||
const _DRAW_GRAB_AT: float = 0.55
|
||||
|
||||
const _SWORD_SCENE: PackedScene = preload("res://Assets/sword.glb")
|
||||
const _SPEAR_SCENE: PackedScene = preload("res://Assets/spear.glb")
|
||||
# Wrapper scenes (glb + an editor-tweakable hitbox / collider) — see sword.tscn / spear.tscn.
|
||||
const _SWORD_SCENE: PackedScene = preload("res://actors/matador/sword.tscn")
|
||||
const _SPEAR_SCENE: PackedScene = preload("res://actors/matador/spear.tscn")
|
||||
|
||||
const _GRAVITY: float = 9.8 # arena gravity, for the spear's ballistic arc
|
||||
|
||||
@@ -94,7 +93,7 @@ const _TURN_FACE: float = 16.0 # face the bull while engaging
|
||||
const _TURN_SHARP: float = 24.0 # snap onto a sidestep / roll direction
|
||||
|
||||
|
||||
const RigidSkin = preload("res://rigid_skin.gd")
|
||||
const RigidSkin = preload("res://systems/render/rigid_skin.gd")
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -103,9 +102,10 @@ func _ready() -> void:
|
||||
_anim_player = _find_anim_player(_mesh)
|
||||
if _skeleton:
|
||||
_sim = MatadorRagdoll.build(_skeleton)
|
||||
# Web (Mali/ANGLE) can't run Compatibility vertex-skinning; rebuild as non-skinned
|
||||
# bone-attached pieces. Runs after the ragdoll sim is built (both drive the same bones).
|
||||
if OS.has_feature("web"):
|
||||
# Native GPU skinning by default; rigid_skin is an opt-in fallback now (the Mali/ANGLE
|
||||
# invisible-skinning bug is fixed in the 4.7 web templates). See Controls.rigid_skin_enabled().
|
||||
# When on, it runs after the ragdoll sim is built (both drive the same bones).
|
||||
if Controls.rigid_skin_enabled():
|
||||
RigidSkin.convert_tree(self)
|
||||
if _anim_player:
|
||||
for anim in [_ANIM_RUN, _ANIM_ATTACK] + _ANIM_TAUNTS:
|
||||
@@ -121,7 +121,7 @@ func _ready() -> void:
|
||||
_throw_aim_cd = randf_range(2.0, 5.0)
|
||||
_hit_area.body_entered.connect(_on_body_entered)
|
||||
_pick_wander_target()
|
||||
_blood_burst = preload("res://blood_burst.gd").new()
|
||||
_blood_burst = preload("res://systems/fx/blood_burst.gd").new()
|
||||
add_child(_blood_burst)
|
||||
if ResourceLoader.exists("res://sounds/ole.ogg"):
|
||||
_ole_player = AudioStreamPlayer.new()
|
||||
@@ -168,6 +168,9 @@ func _physics_process(delta: float) -> void:
|
||||
_dodge_cd = maxf(0.0, _dodge_cd - delta)
|
||||
_blade_hit_cd = maxf(0.0, _blade_hit_cd - delta)
|
||||
_throw_aim_cd = maxf(0.0, _throw_aim_cd - delta)
|
||||
_attack_cd = maxf(0.0, _attack_cd - delta)
|
||||
if _bull != null:
|
||||
_sweep_bull_gore()
|
||||
if _state != State.RAGDOLL and _bull != null:
|
||||
_update_ai_state(delta)
|
||||
|
||||
@@ -185,9 +188,8 @@ func _physics_process(delta: float) -> void:
|
||||
_advance_draw(delta)
|
||||
|
||||
|
||||
# Utility-driven brain. The physically committed states (a brace, a pass, a roll, a
|
||||
# throw wind-up) run to completion on their own timers; from the free states the
|
||||
# matador re-scores its options every mat_decide_interval and acts on the winner.
|
||||
# Utility-driven brain. Committed states (brace / pass / roll / throw wind-up) run to completion on
|
||||
# their own timers; from the free states the matador re-scores every mat_decide_interval.
|
||||
func _update_ai_state(delta: float) -> void:
|
||||
if _state == State.BRACE or _state == State.SIDESTEP \
|
||||
or _state == State.ROLL or _state == State.THROW:
|
||||
@@ -204,9 +206,8 @@ func _update_ai_state(delta: float) -> void:
|
||||
_enter_intent(intent)
|
||||
|
||||
|
||||
# Score each intent from weighted heuristics and return the winner. Weights (the
|
||||
# mat_w_* params) shape the personality; a small commit bonus on the current intent
|
||||
# stops it flip-flopping between two near-tied options.
|
||||
# Score each intent from weighted mat_w_* heuristics and return the winner; a small commit bonus on
|
||||
# the current intent stops it flip-flopping between two near-tied options.
|
||||
func _choose_intent() -> Intent:
|
||||
var dist := global_position.distance_to(_bull.global_position)
|
||||
var bull_flat := Vector3(_bull.velocity.x, 0.0, _bull.velocity.z)
|
||||
@@ -227,6 +228,9 @@ func _choose_intent() -> Intent:
|
||||
# A matador won't commit to a strike it can't bail out of, so a spent dodge
|
||||
# cooldown makes attacking (and dodging outright) less attractive.
|
||||
var dodge_ready := 1.0 if _dodge_cd <= 0.0 else 0.35
|
||||
# The post-stab recovery beat (mat_stab_recovery) kills ATTACK outright — the matador circles
|
||||
# or gives ground between thrusts rather than machine-gunning the bull; DODGE/FLEE stay live.
|
||||
var attack_ready := 0.0 if _attack_cd > 0.0 else 1.0
|
||||
var throw_ok := _throw_aim_cd <= 0.0 and is_instance_valid(_sword_node) \
|
||||
and dist >= DP.f("mat_throw_min_dist") and dist <= DP.f("mat_throw_range")
|
||||
|
||||
@@ -247,7 +251,7 @@ func _choose_intent() -> Intent:
|
||||
# Press the attack; ease off a touch when a charge is barrelling in. Supporting
|
||||
# matadors barely press — they yield the pass to whoever's engaging.
|
||||
Intent.ATTACK: DP.f("mat_w_aggression") * (0.45 + 0.55 * close) \
|
||||
* (1.0 - 0.55 * danger) * dodge_ready * engage_mult,
|
||||
* (1.0 - 0.55 * danger) * dodge_ready * engage_mult * attack_ready,
|
||||
# Sidestep / plant against an incoming charge.
|
||||
Intent.DODGE: DP.f("mat_w_caution") * danger * (1.0 if _dodge_cd <= 0.0 else 0.0),
|
||||
# Hurl the blade — an occasional ranged flourish from an engaging matador when
|
||||
@@ -255,7 +259,10 @@ func _choose_intent() -> Intent:
|
||||
Intent.THROW: (DP.f("mat_w_throw") * (0.35 + 0.65 * (1.0 - close))) \
|
||||
if (throw_ok and is_engager) else -1.0,
|
||||
}
|
||||
score[_intent] = float(score[_intent]) + DP.f("mat_w_commit")
|
||||
# Commit bonus keeps a near-tied choice from flip-flopping — but don't re-reinforce a
|
||||
# just-swung ATTACK we're recovering from, or the matador clings to it and spams the bull.
|
||||
if not (_intent == Intent.ATTACK and _attack_cd > 0.0):
|
||||
score[_intent] = float(score[_intent]) + DP.f("mat_w_commit")
|
||||
|
||||
var best: Intent = Intent.WANDER
|
||||
var best_score := -INF
|
||||
@@ -301,9 +308,8 @@ func _enter_intent(intent: Intent) -> void:
|
||||
_start_throw()
|
||||
|
||||
|
||||
# How many other live matadors are closer to the bull than this one. 0 = the
|
||||
# closest (the natural lead engager); the utility brain gives the nearest
|
||||
# mat_engage_slots the go-ahead to press while the rest spread out and taunt.
|
||||
# How many other live matadors are closer to the bull than this one (0 = the lead engager); the
|
||||
# brain lets the nearest mat_engage_slots press while the rest spread out and taunt.
|
||||
func _engagement_rank() -> int:
|
||||
if _bull == null:
|
||||
return 0
|
||||
@@ -347,6 +353,19 @@ func _is_charge_incoming() -> bool:
|
||||
return bull_flat.normalized().dot(to_me.normalized()) > 0.85
|
||||
|
||||
|
||||
# Bull charging fast and aimed straight at the matador, at ANY range (unlike _is_charge_incoming,
|
||||
# which is capped to brace range) — so a taunting matador can answer a charge from across the arena.
|
||||
func _charge_aimed_at_me() -> bool:
|
||||
var bull_flat := Vector3(_bull.velocity.x, 0.0, _bull.velocity.z)
|
||||
if bull_flat.length() < DP.f("mat_charge_speed"):
|
||||
return false
|
||||
var to_me := global_position - _bull.global_position
|
||||
to_me.y = 0.0
|
||||
if to_me.length() < 0.1:
|
||||
return true
|
||||
return bull_flat.normalized().dot(to_me.normalized()) > 0.9
|
||||
|
||||
|
||||
# Returns a wall-clear direction, recomputed at most every _STEER_INTERVAL seconds.
|
||||
# Randomises among available clear angles so juke direction is unpredictable.
|
||||
func _steer_clear(desired_dir: Vector3, delta: float) -> Vector3:
|
||||
@@ -407,6 +426,19 @@ func _face_point(point: Vector3, delta: float, rate: float) -> void:
|
||||
func _tick_wander(delta: float) -> void:
|
||||
# Plant the feet while drawing / sheathing instead of sliding through the clip.
|
||||
if _drawing or _sheathing or _idle_timer > 0.0:
|
||||
# A charge aimed at a taunting matador snaps it out of the pose: hurl a spear at a bull
|
||||
# barrelling in from range, or switch to the sword (brace/cite) to meet a closer rush.
|
||||
if _idle_timer > 0.0 and not _drawing and not _sheathing \
|
||||
and _bull != null and _charge_aimed_at_me():
|
||||
var dist := global_position.distance_to(_bull.global_position)
|
||||
var throw_ok := _throw_aim_cd <= 0.0 and is_instance_valid(_sword_node) \
|
||||
and dist >= DP.f("mat_throw_min_dist") and dist <= DP.f("mat_throw_range")
|
||||
if dist > DP.f("mat_brace_range") and throw_ok:
|
||||
_start_throw() # far charge — get the spear off before it arrives
|
||||
return
|
||||
if dist <= DP.f("mat_brace_range"):
|
||||
_start_brace() # closer rush — draw the sword and cite the charge
|
||||
return
|
||||
_idle_timer = maxf(0.0, _idle_timer - delta)
|
||||
_decelerate(10.0, delta)
|
||||
# Taunt toward the bull so the posturing plays to it, not off into space.
|
||||
@@ -531,6 +563,7 @@ func _start_attack() -> void:
|
||||
|
||||
func _tick_attack(delta: float) -> void:
|
||||
_attack_timer -= delta
|
||||
var was_swinging := _swing_timer > 0.0
|
||||
_swing_timer = maxf(0.0, _swing_timer - delta)
|
||||
# Plant the feet while drawing instead of sliding toward the bull mid-clip.
|
||||
if _drawing or _sheathing:
|
||||
@@ -543,22 +576,41 @@ func _tick_attack(delta: float) -> void:
|
||||
to_bull.y = 0.0
|
||||
var dist := to_bull.length()
|
||||
|
||||
# Commit to a full swing once in range instead of flipping Run/Attack at the
|
||||
# distance boundary every frame (the old foot-slide jitter). When the swing
|
||||
# ends the matador re-closes if the bull slipped away, or stabs again if not.
|
||||
if _swing_timer <= 0.0 and dist <= DP.f("mat_attack_min_dist") and _sword_in_hand:
|
||||
# A finished stab enforces a recovery beat (no re-commit for mat_stab_recovery), then decides
|
||||
# what's next: a landed hit very likely breaks into a taunt/retreat, otherwise the usual roll
|
||||
# to give ground / circle / throw — anything but instantly drilling the same thrust again.
|
||||
if was_swinging and _swing_timer <= 0.0:
|
||||
_attack_cd = DP.f("mat_stab_recovery")
|
||||
var landed := _sword_blade_area != null and _sword_blade_area.did_hit()
|
||||
if landed and randf() < DP.f("mat_taunt_after_hit"):
|
||||
_do_taunt_retreat()
|
||||
return
|
||||
if randf() < DP.f("mat_stab_followup_chance"):
|
||||
_do_stab_followup()
|
||||
return
|
||||
|
||||
# Run in (Run clip) to mat_stab_standoff, then plant and swing on the spot — the standoff sits
|
||||
# inside the sword's ~3.75 m thrust reach; killing velocity on commit stops the feet sliding.
|
||||
# The recovery beat gates the next commit so back-to-back stabs can't machine-gun the bull.
|
||||
if _swing_timer <= 0.0 and _attack_cd <= 0.0 and dist <= DP.f("mat_stab_standoff") and _sword_in_hand:
|
||||
_swing_timer = _anim_length(_ANIM_ATTACK, 0.9)
|
||||
_lunge_timer = DP.f("mat_lunge_time")
|
||||
velocity.x = 0.0
|
||||
velocity.z = 0.0
|
||||
# Re-arm fresh each swing — back-to-back stabs commit before the else-branch deactivate runs.
|
||||
if _sword_blade_area != null:
|
||||
_sword_blade_area.deactivate()
|
||||
|
||||
if _swing_timer > 0.0:
|
||||
_lunge_timer = maxf(0.0, _lunge_timer - delta)
|
||||
if _lunge_timer > 0.0 and dist > DP.f("mat_attack_min_dist"):
|
||||
_accelerate(to_bull.normalized(), DP.f("mat_lunge_speed"), delta)
|
||||
else:
|
||||
_decelerate(22.0, delta)
|
||||
_decelerate(22.0, delta)
|
||||
_play_anim(_ANIM_ATTACK)
|
||||
_try_melee_hit(DP.f("mat_stab_reach"))
|
||||
# Damage off the sword's own BladeHitbox (sword.tscn), riding the animated blade — gored only
|
||||
# when the thrust reaches it. Arm at the thrust (mat_stab_strike_at) and sweep for the overlap.
|
||||
if _sword_blade_area != null and _swing_phase() >= DP.f("mat_stab_strike_at"):
|
||||
_sword_blade_area.activate()
|
||||
_sword_blade_area.sweep()
|
||||
else:
|
||||
if _sword_blade_area != null:
|
||||
_sword_blade_area.deactivate()
|
||||
var dir := _steer_clear(to_bull.normalized(), delta)
|
||||
_accelerate(dir, DP.f("mat_attack_speed"), delta)
|
||||
_play_anim(_ANIM_RUN)
|
||||
@@ -566,6 +618,50 @@ func _tick_attack(delta: float) -> void:
|
||||
move_and_slide()
|
||||
|
||||
|
||||
# How far through the attack swing: 0 (just committed) → 1 (finished), tracking the clip's progress.
|
||||
func _swing_phase() -> float:
|
||||
var length := _anim_length(_ANIM_ATTACK, 0.9)
|
||||
if length <= 0.0:
|
||||
return 1.0
|
||||
return clampf(1.0 - _swing_timer / length, 0.0, 1.0)
|
||||
|
||||
|
||||
# Break off after a stab instead of drilling the same thrust: give ground, hurl a spear, or circle.
|
||||
# The brain re-engages after; _decide_cd is reset so the pick plays out before it re-scores.
|
||||
func _do_stab_followup() -> void:
|
||||
_decide_cd = DP.f("mat_decide_interval")
|
||||
_dodge_cd = DP.f("mat_dodge_cooldown")
|
||||
_steer_cd = 0.0
|
||||
var roll := randf()
|
||||
if roll < 0.2 and _throw_aim_cd <= 0.0 and is_instance_valid(_sword_node):
|
||||
_start_throw() # ranged flourish
|
||||
elif roll < 0.6:
|
||||
_state = State.FLEE # give ground after the thrust
|
||||
_intent = Intent.FLEE
|
||||
else:
|
||||
_state = State.WANDER # circle to a fresh angle before pressing again
|
||||
_intent = Intent.WANDER
|
||||
_pick_wander_target()
|
||||
|
||||
|
||||
# A stab that LANDED: milk it. Break into a long taunt held toward the bull, then drift out to the
|
||||
# showmanship ring — and lock ATTACK out for the whole hold (_attack_cd) so the brain can't drag the
|
||||
# matador straight back into another thrust. DODGE/FLEE stay live, so a charging bull is still
|
||||
# answered. This is what stops the "stab, stab, stab" spam after the matador connects.
|
||||
func _do_taunt_retreat() -> void:
|
||||
_state = State.WANDER
|
||||
_intent = Intent.WANDER
|
||||
_idle_timer = randf_range(DP.f("mat_taunt_hold") * 0.7, DP.f("mat_taunt_hold") * 1.3)
|
||||
_idle_anim = _pick_taunt()
|
||||
_taunt_swap = randf_range(1.5, 3.0)
|
||||
_attack_cd = maxf(_attack_cd, _idle_timer)
|
||||
_dodge_cd = DP.f("mat_dodge_cooldown")
|
||||
_steer_cd = 0.0
|
||||
_pick_wander_target()
|
||||
if _sword_blade_area != null:
|
||||
_sword_blade_area.deactivate()
|
||||
|
||||
|
||||
func _enter_roll(hit_dir: Vector3) -> void:
|
||||
_state = State.ROLL
|
||||
# Match the state to the actual clip so the roll plays through once instead of
|
||||
@@ -579,9 +675,8 @@ func _enter_roll(hit_dir: Vector3) -> void:
|
||||
_anim_player.seek(0.0, true)
|
||||
|
||||
|
||||
# Called by the spawner (deferred) to make the matador leap-roll toward a target
|
||||
# point on entry — the cinematic arena entrance. After the roll it transitions to
|
||||
# normal AI via the standard ROLL → FLEE path.
|
||||
# Called (deferred) by the spawner to make the matador leap-roll toward a target on entry — the
|
||||
# cinematic arena entrance. After the roll it transitions to normal AI via the ROLL → FLEE path.
|
||||
func enter_spawn_roll(toward: Vector3) -> void:
|
||||
var dir := toward - global_position
|
||||
dir.y = 0.0
|
||||
@@ -770,34 +865,84 @@ func _on_body_entered(body: Node3D) -> void:
|
||||
if not body.is_in_group(&"player"):
|
||||
return
|
||||
var player := body as CharacterBody3D
|
||||
if player.velocity.length() < DP.f("mat_hit_threshold"):
|
||||
var bull_speed := player.velocity.length()
|
||||
if bull_speed < DP.f("mat_hit_threshold"):
|
||||
return
|
||||
if not _horns_first(player.global_position):
|
||||
return
|
||||
var flat_vel := Vector3(player.velocity.x, 0.0, player.velocity.z)
|
||||
var hit_dir := flat_vel.normalized() if flat_vel.length() > 0.5 else \
|
||||
(global_position - player.global_position).normalized()
|
||||
_take_bull_charge(bull_speed, hit_dir)
|
||||
|
||||
if randf() < DP.f("mat_roll_chance"):
|
||||
|
||||
# Tunnel-proof charge detection: at dash speed the bull skips clean over the thin HitArea between
|
||||
# ticks, so body_entered never fires. Sweep its actual travel (prev → now) and gore on any pass within
|
||||
# mat_charge_gore_radius. Runs every frame (even ragdolled) to keep the segment fresh.
|
||||
func _sweep_bull_gore() -> void:
|
||||
var now := _bull.global_position
|
||||
var seg_from := _bull_prev_pos if _bull_prev_seen else now
|
||||
_bull_prev_pos = now
|
||||
_bull_prev_seen = true
|
||||
if _state == State.RAGDOLL or _state == State.ROLL:
|
||||
return
|
||||
var flat_vel := Vector3(_bull.velocity.x, 0.0, _bull.velocity.z)
|
||||
var bull_speed := flat_vel.length()
|
||||
if bull_speed < DP.f("mat_hit_threshold"):
|
||||
return
|
||||
var a := Vector3(seg_from.x, 0.0, seg_from.z)
|
||||
var b := Vector3(now.x, 0.0, now.z)
|
||||
var p := Vector3(global_position.x, 0.0, global_position.z)
|
||||
var closest := Geometry3D.get_closest_point_to_segment(p, a, b)
|
||||
if p.distance_to(closest) > DP.f("mat_charge_gore_radius"):
|
||||
return
|
||||
if not _horns_first(seg_from):
|
||||
return
|
||||
var hit_dir := flat_vel.normalized() if bull_speed > 0.5 \
|
||||
else (global_position - now).normalized()
|
||||
_take_bull_charge(bull_speed, hit_dir)
|
||||
|
||||
|
||||
# Horns-first gate: gore only when the bull's velocity points within bull_gore_arc of the direction
|
||||
# from `from` to the matador — not a sideways brush or backing in. The dash ability skips the gate.
|
||||
func _horns_first(from: Vector3) -> bool:
|
||||
if _bull == null:
|
||||
return false
|
||||
if _bull.has_method(&"is_dashing") and _bull.call(&"is_dashing"):
|
||||
return true # the dash ability is a committed lunge — it gores on contact, no angle gate
|
||||
var vel := Vector3(_bull.velocity.x, 0.0, _bull.velocity.z)
|
||||
if vel.length() < 0.5:
|
||||
return false
|
||||
var to_me := global_position - from
|
||||
to_me.y = 0.0
|
||||
if to_me.length() < 0.01:
|
||||
return true
|
||||
return vel.normalized().dot(to_me.normalized()) > cos(deg_to_rad(DP.f("bull_gore_arc")))
|
||||
|
||||
|
||||
# Resolve a connected charge: the survival roll fades as bull speed climbs (mat_charge_pierce), so a
|
||||
# committed charge gores rather than getting shrugged off.
|
||||
func _take_bull_charge(bull_speed: float, hit_dir: Vector3) -> void:
|
||||
# The boulder roll bowls matadors over with its own launch + chain (player._roll_try_pop); don't
|
||||
# pre-empt that with a quiet gore, or the ball rolls straight through without popping.
|
||||
if _bull.has_method(&"is_rolling") and _bull.call(&"is_rolling"):
|
||||
return
|
||||
# The dash is a committed lunge — a connect is a guaranteed kill, no survival roll (it already
|
||||
# bypasses the horns cone). A passive cruise-charge still leaves a roll chance that fades with speed.
|
||||
var roll_chance := 0.0
|
||||
if not (_bull.has_method(&"is_dashing") and _bull.call(&"is_dashing")):
|
||||
var floor_spd := DP.f("mat_hit_threshold")
|
||||
var full_spd := maxf(DP.f("mat_charge_speed"), floor_spd + 0.1)
|
||||
var commit := clampf((bull_speed - floor_spd) / (full_spd - floor_spd), 0.0, 1.0)
|
||||
roll_chance = DP.f("mat_roll_chance") * (1.0 - commit * DP.f("mat_charge_pierce"))
|
||||
if randf() < roll_chance:
|
||||
_enter_roll(hit_dir)
|
||||
else:
|
||||
_enter_ragdoll(hit_dir, player.velocity.length())
|
||||
_enter_ragdoll(hit_dir, bull_speed)
|
||||
|
||||
|
||||
func _on_blade_hit(body: Node3D) -> void:
|
||||
if _state != State.ATTACK and _state != State.SIDESTEP and _state != State.BRACE:
|
||||
return
|
||||
if not body.is_in_group(&"player"):
|
||||
return
|
||||
if _blade_hit_cd > 0.0:
|
||||
return
|
||||
_blade_hit_cd = DP.f("mat_stab_cooldown")
|
||||
body.call(&"take_sword_hit", "gored")
|
||||
|
||||
|
||||
# Reach-based stab. A charging bull crosses metres per physics frame, so it easily
|
||||
# tunnels through the thin blade Area between frames — this distance check is the
|
||||
# reliable hit. Called from the offensive ticks (active swing / pass) whenever the
|
||||
# blade is drawn: if the bull is inside reach AND the matador is facing it — the
|
||||
# blade actually presented, not held off to the side — it's gored.
|
||||
# Reach-based hit for the sidestep PASS (fast lateral sweep — the thin blade Area tunnels, so a
|
||||
# distance+facing check is the reliable gore; the ATTACK stab uses the blade collider instead).
|
||||
func _try_melee_hit(reach: float) -> void:
|
||||
if _blade_hit_cd > 0.0 or not _sword_in_hand or _bull == null:
|
||||
return
|
||||
@@ -806,19 +951,17 @@ func _try_melee_hit(reach: float) -> void:
|
||||
var dist := to_bull.length()
|
||||
if dist > reach or dist < 0.01:
|
||||
return
|
||||
# Only lands within the frontal arc — you're gored on a presented blade, not by
|
||||
# brushing a matador whose sword is pointing elsewhere.
|
||||
var facing := Vector3(sin(_mesh.rotation.y), 0.0, cos(_mesh.rotation.y))
|
||||
if facing.dot(to_bull / dist) < cos(deg_to_rad(DP.f("mat_stab_arc"))):
|
||||
return
|
||||
_blade_hit_cd = DP.f("mat_stab_cooldown")
|
||||
_bull.call(&"take_sword_hit", "gored")
|
||||
DamageEvent.deal(_bull, DamageEvent.Cause.GORED, _bull.global_position, self)
|
||||
|
||||
|
||||
func _enter_ragdoll(hit_dir: Vector3, bull_speed: float, up_boost: float = 0.0) -> void:
|
||||
_state = State.RAGDOLL
|
||||
if _sword_blade_area:
|
||||
_sword_blade_area.monitoring = false
|
||||
if _sword_blade_area != null:
|
||||
_sword_blade_area.deactivate()
|
||||
_clear_spear() # a spear mid-wind-up dies with the matador, not in the air
|
||||
killed.emit()
|
||||
var cleanup_timer := get_tree().create_timer(4.0)
|
||||
@@ -1011,10 +1154,9 @@ func _pick_wander_target() -> void:
|
||||
|
||||
# ── Sword attachment ──────────────────────────────────────────────────────────
|
||||
|
||||
# The rig (matador_v03) carries two purpose-built bones: weapon_bone (right-hand
|
||||
# grip) and weapon_rest_bone (hip holster). The sword rides in the holster and is
|
||||
# carried to the hand in combat (see _carry_sword); each bone has its own DP-tunable
|
||||
# local offset/rotation (sword_* / sword_rest_*) so the poses seat independently.
|
||||
# The rig (matador_v03) carries two bones: weapon_bone (right-hand grip) and weapon_rest_bone (hip
|
||||
# holster). The sword rides in the holster, carried to the hand in combat (_carry_sword); each bone
|
||||
# has its own DP-tunable offset/rotation (sword_* / sword_rest_*) so the poses seat independently.
|
||||
func _setup_sword() -> void:
|
||||
if not _skeleton:
|
||||
return
|
||||
@@ -1047,28 +1189,15 @@ func _spawn_sword() -> void:
|
||||
_drawing = false
|
||||
_apply_sword_grip()
|
||||
|
||||
# Blade collider along +Z (hilt at origin), tipped from the capsule's default Y.
|
||||
# Only monitors while the sword is in hand (see _carry_sword).
|
||||
var blade_cap := CapsuleShape3D.new()
|
||||
blade_cap.radius = 0.18
|
||||
blade_cap.height = 1.30
|
||||
var blade_cs := CollisionShape3D.new()
|
||||
blade_cs.shape = blade_cap
|
||||
blade_cs.position = Vector3(0.0, 0.0, 0.8)
|
||||
blade_cs.rotation_degrees = Vector3(90.0, 0.0, 0.0)
|
||||
|
||||
_sword_blade_area = Area3D.new()
|
||||
_sword_blade_area.collision_layer = 0
|
||||
_sword_blade_area.collision_mask = 1
|
||||
_sword_blade_area.monitoring = _sword_in_hand
|
||||
_sword_blade_area.add_child(blade_cs)
|
||||
_sword_node.add_child(_sword_blade_area)
|
||||
_sword_blade_area.body_entered.connect(_on_blade_hit)
|
||||
# The blade hitbox (sword.tscn, hitbox.gd) is armed only during the ATTACK thrust (see
|
||||
# _tick_attack); off while merely carried, so brushing a drawn matador never gores the bull.
|
||||
_sword_blade_area = _sword_node.get_node_or_null("BladeHitbox") as Hitbox
|
||||
if _sword_blade_area != null:
|
||||
_sword_blade_area.deactivate()
|
||||
|
||||
|
||||
# The sword is MELEE-only: drawn while fighting up close (ATTACK) or guarding
|
||||
# (BRACE / SIDESTEP), holstered otherwise. Ranged throws use a spear instead
|
||||
# (see _spawn_spear_in_hand), so THROW keeps the sword holstered.
|
||||
# The sword is MELEE-only: drawn for ATTACK / BRACE / SIDESTEP, holstered otherwise. Ranged throws
|
||||
# use a spear (_spawn_spear_in_hand), so THROW keeps the sword holstered.
|
||||
func _wants_sword_drawn() -> bool:
|
||||
return _state == State.ATTACK \
|
||||
or _state == State.BRACE or _state == State.SIDESTEP
|
||||
@@ -1077,6 +1206,9 @@ func _wants_sword_drawn() -> bool:
|
||||
func _update_sword_carry() -> void:
|
||||
if not is_instance_valid(_sword_node) or _state == State.RAGDOLL:
|
||||
return
|
||||
# The blade collider lives only during the ATTACK thrust; leaving ATTACK disarms it.
|
||||
if _state != State.ATTACK and _sword_blade_area != null:
|
||||
_sword_blade_area.deactivate()
|
||||
if _wants_sword_drawn():
|
||||
if not _sword_in_hand and not _drawing:
|
||||
_begin_draw()
|
||||
@@ -1156,8 +1288,9 @@ func _carry_sword(in_hand: bool) -> void:
|
||||
_sword_in_hand = in_hand
|
||||
_sword_node.reparent(target, false)
|
||||
_apply_sword_grip()
|
||||
if _sword_blade_area:
|
||||
_sword_blade_area.monitoring = in_hand
|
||||
# Carrying never arms the blade — only the ATTACK thrust does (see _tick_attack).
|
||||
if _sword_blade_area != null:
|
||||
_sword_blade_area.deactivate()
|
||||
|
||||
|
||||
func _apply_sword_grip() -> void:
|
||||
@@ -1,7 +1,7 @@
|
||||
[gd_scene format=3 uid="uid://matador_scene_v1"]
|
||||
[gd_scene format=3 uid="uid://d0te4mwbh33ag"]
|
||||
|
||||
[ext_resource type="Script" path="res://matador.gd" id="1_matgd"]
|
||||
[ext_resource type="PackedScene" uid="uid://eu2lqjhowj4t" path="res://Assets/Matador.glb" id="2_matfbx"]
|
||||
[ext_resource type="Script" uid="uid://bvfa7xi0luysk" path="res://actors/matador/matador.gd" id="1_matgd"]
|
||||
[ext_resource type="PackedScene" uid="uid://eu2lqjhowj4t" path="res://art/characters/matador.glb" id="2_matfbx"]
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_body"]
|
||||
radius = 0.25
|
||||
@@ -11,19 +11,18 @@ height = 1.2
|
||||
radius = 0.45
|
||||
height = 1.4
|
||||
|
||||
[node name="Matador" type="CharacterBody3D"]
|
||||
[node name="Matador" type="CharacterBody3D" unique_id=1781857630]
|
||||
script = ExtResource("1_matgd")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=2101651042]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.85, 0)
|
||||
shape = SubResource("CapsuleShape3D_body")
|
||||
|
||||
[node name="HitArea" type="Area3D" parent="."]
|
||||
monitoring = true
|
||||
[node name="HitArea" type="Area3D" parent="." unique_id=745037545]
|
||||
monitorable = false
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="HitArea"]
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="HitArea" unique_id=1497356275]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.85, 0)
|
||||
shape = SubResource("CapsuleShape3D_hit")
|
||||
|
||||
[node name="matador2" parent="." instance=ExtResource("2_matfbx")]
|
||||
[node name="matador2" parent="." unique_id=1487256381 instance=ExtResource("2_matfbx")]
|
||||
@@ -4,6 +4,8 @@ extends RefCounted
|
||||
## limited swing/twist (DP-tunable) so the corpse keeps its silhouette instead of
|
||||
## folding into a flat "deflated" pile; the cape stays free (PIN) so it drapes.
|
||||
|
||||
const PhysicsLayers = preload("res://systems/combat/physics_layers.gd")
|
||||
|
||||
# [bone, radius] → sphere collider; [bone, radius, height] → capsule.
|
||||
const _BONES: Array = [
|
||||
["matador", 0.18],
|
||||
@@ -53,10 +55,15 @@ static func build(skeleton: Skeleton3D) -> PhysicalBoneSimulator3D:
|
||||
pb.bone_name = bname
|
||||
pb.mass = 0.4
|
||||
pb.linear_damp = 0.1
|
||||
pb.collision_layer = 1
|
||||
pb.collision_mask = 1
|
||||
# The ragdoll only simulates once the matador is dead, so it lives on the CORPSE
|
||||
# layer and masks to CORPSE alone: it rests on the ground/walls (which also carry
|
||||
# CORPSE) but the bull — which collides only with WORLD — passes straight through
|
||||
# the bodies instead of tripping over them.
|
||||
pb.collision_layer = PhysicsLayers.CORPSE
|
||||
pb.collision_mask = PhysicsLayers.CORPSE
|
||||
if bname.begins_with("Bone"):
|
||||
# Cape: floppy cloth — free swing, light, draggy, one-way collision.
|
||||
# Cape: floppy cloth — free swing, light, draggy, one-way collision (layer 0
|
||||
# so nothing collides with it; it still drapes on the CORPSE-masked ground).
|
||||
pb.joint_type = PhysicalBone3D.JOINT_TYPE_PIN
|
||||
pb.mass = 0.15
|
||||
pb.linear_damp = 0.5
|
||||
@@ -0,0 +1,15 @@
|
||||
[gd_scene format=3 uid="uid://c8spearwrap01"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://jepc74w8opbu" path="res://art/weapons/spear.glb" id="1_spearglb"]
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_shaft"]
|
||||
radius = 0.13
|
||||
height = 2.2
|
||||
|
||||
[node name="Spear" type="Node3D"]
|
||||
|
||||
[node name="spear" parent="." instance=ExtResource("1_spearglb")]
|
||||
|
||||
[node name="HitShape" type="CollisionShape3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, -1, 0, 1, -4.371139e-08, 0, 0, 0.79)
|
||||
shape = SubResource("CapsuleShape3D_shaft")
|
||||
@@ -10,6 +10,8 @@ extends RigidBody3D
|
||||
# (see shed_all, which the bull's slam calls to fling them all off); other hits pin
|
||||
# it in place until the despawn timer clears it.
|
||||
|
||||
const PhysicsLayers = preload("res://systems/combat/physics_layers.gd")
|
||||
|
||||
const _DESPAWN_SEC: float = 8.0
|
||||
const _STUCK_GROUP: StringName = &"stuck_spear"
|
||||
# Shaft geometry in spear.glb local space: tip at +Z, butt behind the origin.
|
||||
@@ -34,16 +36,25 @@ static func launch(parent: Node, mesh: Node3D, origin: Vector3, velocity: Vector
|
||||
var s := SpearProjectile.new()
|
||||
s.gravity_scale = gravity
|
||||
s.collision_layer = 0
|
||||
s.collision_mask = 1
|
||||
# Mask WORLD so a miss sticks in the arena, and BULL so it can actually hit the
|
||||
# bull — the bull's body is on layer BULL, not WORLD, so a WORLD-only mask flew
|
||||
# straight through it (every throw whiffed, even against a stationary bull).
|
||||
s.collision_mask = PhysicsLayers.WORLD | PhysicsLayers.BULL
|
||||
s.contact_monitor = true
|
||||
s.max_contacts_reported = 6
|
||||
parent.add_child(s)
|
||||
# Seat the body so its local +Z (the shaft, see spear.glb) points along the throw.
|
||||
s.global_transform = Transform3D(_aim_basis(velocity), origin)
|
||||
# Take the editor-tweakable collider from the spear wrapper (Spear.tscn's HitShape)
|
||||
# before reparenting, then drop the now-nested template.
|
||||
var collider := _collider_from(mesh)
|
||||
mesh.reparent(s, false)
|
||||
mesh.transform = Transform3D.IDENTITY
|
||||
s._mesh = mesh
|
||||
s.add_child(_shaft_collider())
|
||||
var tmpl := mesh.get_node_or_null("HitShape")
|
||||
if tmpl != null:
|
||||
tmpl.queue_free()
|
||||
s.add_child(collider)
|
||||
s.linear_velocity = velocity
|
||||
_despawn(s)
|
||||
return s
|
||||
@@ -84,6 +95,18 @@ static func shed_all(bull: Node) -> void:
|
||||
_despawn(rb)
|
||||
|
||||
|
||||
# Clone the wrapper's editor-tweakable HitShape (Spear.tscn) into a fresh CollisionShape3D
|
||||
# for the projectile body; falls back to the built-in shaft collider if it's missing.
|
||||
static func _collider_from(source: Node) -> CollisionShape3D:
|
||||
var tmpl := source.get_node_or_null("HitShape") as CollisionShape3D
|
||||
if tmpl != null and tmpl.shape != null:
|
||||
var cs := CollisionShape3D.new()
|
||||
cs.shape = tmpl.shape
|
||||
cs.transform = tmpl.transform
|
||||
return cs
|
||||
return _shaft_collider()
|
||||
|
||||
|
||||
static func _shaft_collider() -> CollisionShape3D:
|
||||
# Collider along local +Z (mesh runs z≈-0.34 … 1.92), fattened so a fast throw
|
||||
# can't tunnel past the bull's collision spheres.
|
||||
@@ -200,7 +223,7 @@ func _embed(other: Object, contact: Vector3, flight: Vector3) -> void:
|
||||
var pose := Transform3D(_aim_basis(f), contact + f * (_EMBED_DEPTH - _TIP_Z))
|
||||
var bull := other as Node
|
||||
if bull != null and bull.is_in_group(&"player"):
|
||||
bull.call(&"take_sword_hit", "thrown", contact)
|
||||
DamageEvent.deal(bull, DamageEvent.Cause.THROWN, contact, self)
|
||||
if is_instance_valid(_mesh) and is_instance_valid(bull):
|
||||
_mesh.reparent(_spear_anchor(bull), false)
|
||||
_mesh.global_transform = pose
|
||||
@@ -0,0 +1,20 @@
|
||||
[gd_scene format=3 uid="uid://c8swordwrap01"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://xyvank512l5n" path="res://art/weapons/sword.glb" id="1_swordglb"]
|
||||
[ext_resource type="Script" path="res://systems/combat/hitbox.gd" id="2_hitbox"]
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_blade"]
|
||||
radius = 0.18
|
||||
height = 1.3
|
||||
|
||||
[node name="Sword" type="Node3D"]
|
||||
|
||||
[node name="sword" parent="." instance=ExtResource("1_swordglb")]
|
||||
|
||||
[node name="BladeHitbox" type="Area3D" parent="."]
|
||||
script = ExtResource("2_hitbox")
|
||||
damage_cause = "gored"
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="BladeHitbox"]
|
||||
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, -1, 0, 1, -4.371139e-08, 0, 0, 0.8)
|
||||
shape = SubResource("CapsuleShape3D_blade")
|
||||
@@ -0,0 +1,110 @@
|
||||
extends Node
|
||||
## Procedural (non-particle) bull impact FX: the expanding ground-slam shockwave rings and the
|
||||
## kick flash-pop. These are tween-animated MeshInstance3D flashes, not CPUParticles3D, so they
|
||||
## can't be authored as systems/fx/ scenes and live in code here. Created by player.gd; the
|
||||
## transient meshes are parented into the level (the bull's parent) so they hold their world
|
||||
## position while the bull keeps moving.
|
||||
|
||||
var _player: Node3D
|
||||
var _ring_mesh: ArrayMesh = null
|
||||
|
||||
|
||||
func setup(player: Node3D) -> void:
|
||||
_player = player
|
||||
|
||||
|
||||
# Three nested shockwave rings racing outward from a ground slam, each fading as it grows.
|
||||
func slam_rings(origin: Vector3, max_r: float) -> void:
|
||||
if _ring_mesh == null:
|
||||
_ring_mesh = _make_ring_mesh()
|
||||
_launch_ring(origin, max_r, 0.00, 0.55, Color(1.0, 0.70, 0.10, 0.85))
|
||||
_launch_ring(origin, max_r, 0.10, 0.52, Color(1.0, 0.50, 0.10, 0.65))
|
||||
_launch_ring(origin, max_r, 0.20, 0.48, Color(0.9, 0.30, 0.10, 0.50))
|
||||
|
||||
|
||||
# A quick bright flash-pop at the kick's contact point.
|
||||
func kick_flash(origin: Vector3) -> void:
|
||||
var mat := _flat_material(Color(1.0, 0.97, 0.40, 0.0))
|
||||
var sphere := SphereMesh.new()
|
||||
sphere.radius = 1.0
|
||||
sphere.height = 2.0
|
||||
sphere.radial_segments = 8
|
||||
sphere.rings = 4
|
||||
sphere.material = mat
|
||||
|
||||
var mi := MeshInstance3D.new()
|
||||
mi.mesh = sphere
|
||||
mi.material_override = mat
|
||||
mi.scale = Vector3(0.05, 0.05, 0.05)
|
||||
_host().add_child(mi)
|
||||
mi.global_position = origin
|
||||
|
||||
var tween := create_tween()
|
||||
tween.set_parallel(true)
|
||||
tween.tween_property(mi, "scale", Vector3(0.7, 0.7, 0.7), 0.13)\
|
||||
.set_trans(Tween.TRANS_EXPO).set_ease(Tween.EASE_OUT)
|
||||
var fade := func(a: float) -> void: mat.albedo_color.a = a
|
||||
tween.tween_method(fade, 0.92, 0.0, 0.14)\
|
||||
.set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN)
|
||||
tween.tween_callback(mi.queue_free).set_delay(0.16)
|
||||
|
||||
|
||||
func _launch_ring(origin: Vector3, max_r: float, delay: float, duration: float, col: Color) -> void:
|
||||
var mat := _flat_material(Color(col.r, col.g, col.b, 0.0))
|
||||
var mi := MeshInstance3D.new()
|
||||
mi.mesh = _ring_mesh
|
||||
mi.material_override = mat
|
||||
mi.scale = Vector3(0.02, 1.0, 0.02)
|
||||
_host().add_child(mi)
|
||||
mi.global_position = origin
|
||||
|
||||
var tween := create_tween()
|
||||
tween.set_parallel(true)
|
||||
tween.tween_property(mi, "scale", Vector3(max_r, 1.0, max_r), duration)\
|
||||
.set_delay(delay).set_trans(Tween.TRANS_EXPO).set_ease(Tween.EASE_OUT)
|
||||
var fade := func(a: float) -> void: mat.albedo_color.a = a
|
||||
tween.tween_method(fade, col.a, 0.0, duration)\
|
||||
.set_delay(delay).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN)
|
||||
tween.tween_callback(mi.queue_free).set_delay(delay + duration + 0.05)
|
||||
|
||||
|
||||
func _flat_material(color: Color) -> StandardMaterial3D:
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||
mat.albedo_color = color
|
||||
return mat
|
||||
|
||||
|
||||
# A flat annulus in the XZ plane (unit radius), scaled up at runtime into the shockwave ring.
|
||||
func _make_ring_mesh() -> ArrayMesh:
|
||||
const INNER_R: float = 0.82
|
||||
const OUTER_R: float = 1.00
|
||||
const SEGMENTS: int = 48
|
||||
|
||||
var verts := PackedVector3Array()
|
||||
var indices := PackedInt32Array()
|
||||
for i: int in SEGMENTS:
|
||||
var a0 := i / float(SEGMENTS) * TAU
|
||||
var a1 := (i + 1) / float(SEGMENTS) * TAU
|
||||
var vi := i * 4
|
||||
verts.append(Vector3(cos(a0) * INNER_R, 0.0, sin(a0) * INNER_R))
|
||||
verts.append(Vector3(cos(a0) * OUTER_R, 0.0, sin(a0) * OUTER_R))
|
||||
verts.append(Vector3(cos(a1) * OUTER_R, 0.0, sin(a1) * OUTER_R))
|
||||
verts.append(Vector3(cos(a1) * INNER_R, 0.0, sin(a1) * INNER_R))
|
||||
indices.append_array([vi, vi + 2, vi + 1, vi, vi + 3, vi + 2])
|
||||
|
||||
var arrays: Array = []
|
||||
arrays.resize(Mesh.ARRAY_MAX)
|
||||
arrays[Mesh.ARRAY_VERTEX] = verts
|
||||
arrays[Mesh.ARRAY_INDEX] = indices
|
||||
var mesh := ArrayMesh.new()
|
||||
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||
return mesh
|
||||
|
||||
|
||||
func _host() -> Node:
|
||||
if _player != null and _player.get_parent() != null:
|
||||
return _player.get_parent()
|
||||
return _player
|
||||
@@ -0,0 +1 @@
|
||||
uid://sf5etma4kjt0
|
||||
@@ -1,3 +1,4 @@
|
||||
class_name IsoCamera
|
||||
extends Camera3D
|
||||
|
||||
@export var height: float = 25.0
|
||||
@@ -16,6 +17,11 @@ var _shake_amount: float = 0.0
|
||||
var _shake_decay: float = 0.0
|
||||
var _user_fov: float = 0.0
|
||||
|
||||
# Per-level framing multiplier: 1.0 = the designer's base shot, >1 dollies the camera
|
||||
# back for a wider view (the lobby uses this). Set by game_shell when a level loads;
|
||||
# kept separate from the scroll-wheel FOV zoom so the two don't fight.
|
||||
var _framing_scale: float = 1.0
|
||||
|
||||
# Trailing-camera state: _focus is the smoothed point the camera frames (it lags
|
||||
# the bull), _drag_offset is the velocity-driven trail that slides it behind.
|
||||
var _focus: Vector3 = Vector3.ZERO
|
||||
@@ -51,15 +57,21 @@ func adjust_zoom(ratio: float) -> void:
|
||||
_user_fov = clampf(_user_fov / ratio, fov_min, fov_max)
|
||||
|
||||
|
||||
## Dolly the camera in/out for the level being loaded. 1.0 is the base framing; >1 pulls
|
||||
## back for a wider shot (the lobby hub uses this). Independent of the scroll-wheel zoom.
|
||||
func set_framing_scale(scale: float) -> void:
|
||||
_framing_scale = maxf(scale, 0.1)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
fov = lerpf(fov, _user_fov, delta * 8.0)
|
||||
|
||||
var angle_rad := deg_to_rad(horizontal_angle_deg)
|
||||
|
||||
var offset := Vector3(
|
||||
sin(angle_rad) * horizontal_distance,
|
||||
height,
|
||||
cos(angle_rad) * horizontal_distance
|
||||
sin(angle_rad) * horizontal_distance * _framing_scale,
|
||||
height * _framing_scale,
|
||||
cos(angle_rad) * horizontal_distance * _framing_scale
|
||||
)
|
||||
|
||||
var target := get_parent() as Node3D
|
||||
+186
-430
@@ -8,7 +8,7 @@ const HOOF_OFFSETS: Array = [
|
||||
Vector3( 0.30, -0.25, 0.60),
|
||||
]
|
||||
|
||||
# Bull animation clips (Armature|* action names in Assets/bull.fbx). IDLE is the
|
||||
# Bull animation clips (Armature|* action names in art/characters/bull.fbx). IDLE is the
|
||||
# looping base pose; the others are one-shot ability clips that return to it.
|
||||
const _ANIM_IDLE: StringName = &"Armature|IDLE"
|
||||
const _ANIM_KICK: StringName = &"Armature|KICK"
|
||||
@@ -27,7 +27,11 @@ var _legs: Node
|
||||
var _was_on_floor: bool = false
|
||||
var _pre_slide_vel_y: float = 0.0
|
||||
|
||||
var _slam_ring_mesh: ArrayMesh = null
|
||||
# Built once and reused: a fresh StandardMaterial3D/SphereMesh per spawn makes a mobile WebGL
|
||||
# driver recompile the shader + re-upload the mesh — a hitch "when attacking" absent on desktop.
|
||||
var _unshaded_mat: StandardMaterial3D = null
|
||||
var _particle_sphere_cache: Dictionary = {} # "radius:seg:ring" -> SphereMesh (shared)
|
||||
var _impact_fx: Node = null # bull_impact_fx.gd — the coded (non-particle) slam rings + kick flash
|
||||
|
||||
# Ability system — kick=0, dash=1, slam=2, roll=3
|
||||
var ability_cd: Array[float] = [0.0, 0.0, 0.0, 0.0]
|
||||
@@ -67,6 +71,9 @@ signal health_changed(current: int, max_hp: int)
|
||||
signal hit_taken(cause: String)
|
||||
var _max_hp: int = 10
|
||||
var _hp: int = 10
|
||||
# Bonus pips granted by lobby rewards (e.g. smashing every barrel). Tacked on top of the
|
||||
# base bar; the HUD paints these top pips yellow instead of red.
|
||||
var _bonus_hp: int = 0
|
||||
var _hit_iframes: float = 0.0
|
||||
|
||||
var _huff_player: AudioStreamPlayer = null
|
||||
@@ -75,25 +82,34 @@ var _huff_timer: float = 0.0
|
||||
|
||||
enum DustState { NONE, WALK, CHARGE }
|
||||
var _dust_state: DustState = DustState.NONE
|
||||
var _walk_dust_ramp: Gradient = null
|
||||
var _charge_dust_ramp: Gradient = null
|
||||
# Hoof-dust scale/velocity per state (the look — mesh, colour, curve — lives in bull_hoof_dust.tscn).
|
||||
# A charge sprint kicks up bigger, faster dust than a walk.
|
||||
const _WALK_DUST_SCALE: Vector2 = Vector2(0.20, 0.40)
|
||||
const _WALK_DUST_VEL: Vector2 = Vector2(0.8, 1.8)
|
||||
const _CHARGE_DUST_SCALE: Vector2 = Vector2(0.35, 0.70)
|
||||
const _CHARGE_DUST_VEL: Vector2 = Vector2(2.8, 5.5)
|
||||
|
||||
|
||||
const RigidSkin = preload("res://rigid_skin.gd")
|
||||
const RigidSkin = preload("res://systems/render/rigid_skin.gd")
|
||||
|
||||
var _hit_flash_tween: Tween
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group(&"player")
|
||||
# Mali/ANGLE mobile GPUs can't run Godot's Compatibility vertex-skinning (transform
|
||||
# feedback) — skinned meshes render invisible on the web build. Rebuild the bull as
|
||||
# non-skinned bone-attached pieces there; desktop keeps smooth GPU skinning.
|
||||
if OS.has_feature("web"):
|
||||
# Native GPU skinning by default (the old Mali/ANGLE invisible-skinning bug is fixed in the
|
||||
# 4.7 web templates). rigid_skin is now an opt-in fallback — see Controls.rigid_skin_enabled().
|
||||
if Controls.rigid_skin_enabled():
|
||||
RigidSkin.convert_tree(self)
|
||||
_max_hp = maxi(1, int(DP.f("bull_max_hp")))
|
||||
_hp = _max_hp
|
||||
# Carry any run-long bonus pips (barrel reward) into this level so they survive the
|
||||
# scene reload between fights; the HUD paints these top pips yellow.
|
||||
_bonus_hp = Run.bonus_hp
|
||||
_max_hp += _bonus_hp
|
||||
_hp += _bonus_hp
|
||||
_cube_scale = cube_guy.scale
|
||||
_setup_impact_fx()
|
||||
_setup_hoof_dust()
|
||||
_setup_legs()
|
||||
_setup_charge_trail()
|
||||
@@ -101,123 +117,44 @@ func _ready() -> void:
|
||||
_setup_audio()
|
||||
_bull_anim = _find_anim_player(cube_guy)
|
||||
_setup_bull_idle()
|
||||
DP.any_changed.connect(_on_dp_changed)
|
||||
|
||||
|
||||
func _setup_legs() -> void:
|
||||
_legs = Node.new()
|
||||
_legs.set_script(load("res://bull_legs.gd"))
|
||||
_legs.set_script(load("res://actors/player/bull_legs.gd"))
|
||||
add_child(_legs)
|
||||
_legs.call(&"setup", self, cube_guy)
|
||||
|
||||
|
||||
func _setup_impact_fx() -> void:
|
||||
_impact_fx = Node.new()
|
||||
_impact_fx.set_script(load("res://actors/player/bull_impact_fx.gd"))
|
||||
add_child(_impact_fx)
|
||||
_impact_fx.call(&"setup", self)
|
||||
|
||||
|
||||
# The bull's ability/movement particle effects are authored as scenes in systems/fx/ (so Siim can
|
||||
# tune them in the inspector). The continuous ones follow the bull — instanced once here as
|
||||
# children, then toggled via `emitting`. The one-shots are fired on demand through Fx.spawn().
|
||||
func _setup_hoof_dust() -> void:
|
||||
var sphere := _particle_sphere(0.07, 4, 2, _unshaded_material())
|
||||
|
||||
_walk_dust_ramp = Gradient.new()
|
||||
_walk_dust_ramp.set_color(0, Color(0.94, 0.90, 0.83, 1.0))
|
||||
_walk_dust_ramp.set_color(1, Color(0.94, 0.90, 0.83, 0.0))
|
||||
|
||||
# White-beige dust while running
|
||||
_charge_dust_ramp = Gradient.new()
|
||||
_charge_dust_ramp.set_color(0, Color(0.94, 0.90, 0.83, 1.0))
|
||||
_charge_dust_ramp.set_color(1, Color(0.94, 0.90, 0.83, 0.0))
|
||||
|
||||
var scale_curve := Curve.new()
|
||||
scale_curve.add_point(Vector2(0.0, 0.2))
|
||||
scale_curve.add_point(Vector2(0.4, 1.0))
|
||||
scale_curve.add_point(Vector2(1.0, 0.0))
|
||||
|
||||
for offset: Vector3 in HOOF_OFFSETS:
|
||||
var p := CPUParticles3D.new()
|
||||
var p := Fx.attach(&"bull_hoof_dust", cube_guy) as CPUParticles3D
|
||||
if p == null:
|
||||
continue
|
||||
p.position = offset
|
||||
p.emitting = false
|
||||
p.amount = 12
|
||||
p.lifetime = DP.f("dust_lifetime")
|
||||
p.explosiveness = DP.f("dust_explosiveness")
|
||||
p.randomness = 0.6
|
||||
p.local_coords = false
|
||||
p.direction = Vector3(0.0, 1.0, 0.0)
|
||||
p.spread = DP.f("dust_spread")
|
||||
p.gravity = Vector3(0.0, DP.f("dust_gravity_y"), 0.0)
|
||||
p.initial_velocity_min = DP.f("walk_vel_min")
|
||||
p.initial_velocity_max = DP.f("walk_vel_max")
|
||||
p.scale_amount_min = DP.f("walk_scale_min")
|
||||
p.scale_amount_max = DP.f("walk_scale_max")
|
||||
p.mesh = sphere
|
||||
p.color_ramp = _walk_dust_ramp
|
||||
p.scale_amount_curve = scale_curve
|
||||
cube_guy.add_child(p)
|
||||
_hoof_emitters.append(p)
|
||||
|
||||
|
||||
func _setup_charge_trail() -> void:
|
||||
# Blue energy trail — streams backward during a fast run / roll
|
||||
var trail_sphere := _particle_sphere(0.065, 4, 2, _unshaded_material())
|
||||
|
||||
var trail_ramp := Gradient.new()
|
||||
trail_ramp.set_color(0, Color(0.30, 0.65, 1.0, 1.0))
|
||||
trail_ramp.set_color(1, Color(0.30, 0.65, 1.0, 0.0))
|
||||
|
||||
var trail_scale := Curve.new()
|
||||
trail_scale.add_point(Vector2(0.0, 1.0))
|
||||
trail_scale.add_point(Vector2(0.6, 0.8))
|
||||
trail_scale.add_point(Vector2(1.0, 0.0))
|
||||
|
||||
_charge_trail_emitter = CPUParticles3D.new()
|
||||
_charge_trail_emitter.position = Vector3(0.0, 0.25, 0.3)
|
||||
_charge_trail_emitter.emitting = false
|
||||
_charge_trail_emitter.one_shot = false
|
||||
_charge_trail_emitter.amount = 18
|
||||
_charge_trail_emitter.lifetime = 0.30
|
||||
_charge_trail_emitter.randomness = 0.4
|
||||
_charge_trail_emitter.local_coords = false
|
||||
_charge_trail_emitter.direction = Vector3(0.0, 0.2, 1.0).normalized()
|
||||
_charge_trail_emitter.spread = 25.0
|
||||
_charge_trail_emitter.gravity = Vector3(0.0, 2.0, 0.0)
|
||||
_charge_trail_emitter.initial_velocity_min = 4.0
|
||||
_charge_trail_emitter.initial_velocity_max = 9.0
|
||||
_charge_trail_emitter.scale_amount_min = 0.4
|
||||
_charge_trail_emitter.scale_amount_max = 1.2
|
||||
_charge_trail_emitter.mesh = trail_sphere
|
||||
_charge_trail_emitter.color_ramp = trail_ramp
|
||||
_charge_trail_emitter.scale_amount_curve = trail_scale
|
||||
cube_guy.add_child(_charge_trail_emitter)
|
||||
_charge_trail_emitter = Fx.attach(&"bull_charge_trail", cube_guy) as CPUParticles3D
|
||||
if _charge_trail_emitter != null:
|
||||
_charge_trail_emitter.position = Vector3(0.0, 0.25, 0.3)
|
||||
|
||||
|
||||
func _setup_roll_sparks() -> void:
|
||||
# Rammus Powerball ground spray — dirt/sparks flung outward from the spinning
|
||||
# ball. World-space (local_coords off) so they peel off into a trail behind it.
|
||||
var spark := _particle_sphere(0.05, 4, 2, _unshaded_material())
|
||||
|
||||
var ramp := Gradient.new()
|
||||
ramp.set_color(0, Color(1.00, 0.78, 0.32, 1.0))
|
||||
ramp.set_color(1, Color(0.70, 0.42, 0.14, 0.0))
|
||||
|
||||
var sc := Curve.new()
|
||||
sc.add_point(Vector2(0.0, 0.30))
|
||||
sc.add_point(Vector2(0.3, 1.00))
|
||||
sc.add_point(Vector2(1.0, 0.00))
|
||||
|
||||
_roll_spark_emitter = CPUParticles3D.new()
|
||||
_roll_spark_emitter.position = Vector3(0.0, 0.10, 0.0)
|
||||
_roll_spark_emitter.emitting = false
|
||||
_roll_spark_emitter.one_shot = false
|
||||
_roll_spark_emitter.amount = 46
|
||||
_roll_spark_emitter.lifetime = 0.45
|
||||
_roll_spark_emitter.randomness = 0.7
|
||||
_roll_spark_emitter.local_coords = false
|
||||
_roll_spark_emitter.direction = Vector3(0.0, 1.0, 0.0)
|
||||
_roll_spark_emitter.spread = 180.0
|
||||
_roll_spark_emitter.gravity = Vector3(0.0, -6.0, 0.0)
|
||||
_roll_spark_emitter.initial_velocity_min = 3.0
|
||||
_roll_spark_emitter.initial_velocity_max = 8.0
|
||||
_roll_spark_emitter.scale_amount_min = 0.5
|
||||
_roll_spark_emitter.scale_amount_max = 1.3
|
||||
_roll_spark_emitter.mesh = spark
|
||||
_roll_spark_emitter.color_ramp = ramp
|
||||
_roll_spark_emitter.scale_amount_curve = sc
|
||||
cube_guy.add_child(_roll_spark_emitter)
|
||||
_roll_spark_emitter = Fx.attach(&"bull_roll_sparks", cube_guy) as CPUParticles3D
|
||||
if _roll_spark_emitter != null:
|
||||
_roll_spark_emitter.position = Vector3(0.0, 0.10, 0.0)
|
||||
|
||||
|
||||
func _setup_audio() -> void:
|
||||
@@ -239,42 +176,20 @@ func _setup_audio() -> void:
|
||||
add_child(_crowd_player)
|
||||
|
||||
|
||||
# Toggle the hoof emitters and swap their scale/velocity for the walk vs charge feel. The blue
|
||||
# charge trail is the roll/dash ability's, driven from the roll loop, not by dust state.
|
||||
func _set_dust_state(new_state: DustState) -> void:
|
||||
if new_state == _dust_state:
|
||||
return
|
||||
_dust_state = new_state
|
||||
var scale := _CHARGE_DUST_SCALE if new_state == DustState.CHARGE else _WALK_DUST_SCALE
|
||||
var vel := _CHARGE_DUST_VEL if new_state == DustState.CHARGE else _WALK_DUST_VEL
|
||||
for p: CPUParticles3D in _hoof_emitters:
|
||||
p.lifetime = DP.f("dust_lifetime")
|
||||
p.explosiveness = DP.f("dust_explosiveness")
|
||||
p.spread = DP.f("dust_spread")
|
||||
p.gravity = Vector3(0.0, DP.f("dust_gravity_y"), 0.0)
|
||||
match new_state:
|
||||
DustState.NONE:
|
||||
p.emitting = false
|
||||
DustState.WALK:
|
||||
p.color_ramp = _walk_dust_ramp
|
||||
p.scale_amount_min = DP.f("walk_scale_min")
|
||||
p.scale_amount_max = DP.f("walk_scale_max")
|
||||
p.initial_velocity_min = DP.f("walk_vel_min")
|
||||
p.initial_velocity_max = DP.f("walk_vel_max")
|
||||
p.emitting = true
|
||||
DustState.CHARGE:
|
||||
p.color_ramp = _charge_dust_ramp
|
||||
p.scale_amount_min = DP.f("charge_scale_min")
|
||||
p.scale_amount_max = DP.f("charge_scale_max")
|
||||
p.initial_velocity_min = DP.f("charge_vel_min")
|
||||
p.initial_velocity_max = DP.f("charge_vel_max")
|
||||
p.emitting = true
|
||||
# Blue energy trail belongs to the roll/dash ability only, not to fast WASD
|
||||
# running — driven explicitly in the roll loop / _end_roll, not by dust state.
|
||||
|
||||
|
||||
func _on_dp_changed(_key: String, _val: Variant) -> void:
|
||||
if _hoof_emitters.is_empty() or _dust_state == DustState.NONE:
|
||||
return
|
||||
var prev := _dust_state
|
||||
_dust_state = DustState.NONE
|
||||
_set_dust_state(prev)
|
||||
p.scale_amount_min = scale.x
|
||||
p.scale_amount_max = scale.y
|
||||
p.initial_velocity_min = vel.x
|
||||
p.initial_velocity_max = vel.y
|
||||
p.emitting = new_state != DustState.NONE
|
||||
|
||||
|
||||
func _find_anim_player(node: Node) -> AnimationPlayer:
|
||||
@@ -511,6 +426,7 @@ func _tick_roll(delta: float, _steer: Vector3) -> void:
|
||||
|
||||
move_and_slide()
|
||||
_was_on_floor = is_on_floor()
|
||||
_smash_barrels_in_radius(DP.f("roll_hit_radius"))
|
||||
_roll_try_pop(speed)
|
||||
|
||||
|
||||
@@ -549,22 +465,37 @@ func _roll_try_pop(speed: float) -> bool:
|
||||
return false
|
||||
|
||||
|
||||
# Closest active matador we haven't already popped this roll, or null if none left.
|
||||
# Closest thing the roll should home on — an active matador (arena) or an intact barrel
|
||||
# (lobby) we haven't already smashed this roll. Null if nothing is left to hit.
|
||||
func _roll_pick_target() -> Node3D:
|
||||
var best: Node3D = null
|
||||
var best_d := INF
|
||||
for mat: Node in get_tree().get_nodes_in_group(&"matador"):
|
||||
if _roll_hit.has(mat) or not _mat_active(mat):
|
||||
continue
|
||||
var d: float = (mat as Node3D).global_position.distance_squared_to(global_position)
|
||||
for cand: Node3D in _roll_targets():
|
||||
var d: float = cand.global_position.distance_squared_to(global_position)
|
||||
if d < best_d:
|
||||
best_d = d
|
||||
best = mat as Node3D
|
||||
best = cand
|
||||
return best
|
||||
|
||||
|
||||
# Live roll targets: active matadors plus standing barrels, minus anything already popped.
|
||||
func _roll_targets() -> Array[Node3D]:
|
||||
var out: Array[Node3D] = []
|
||||
for mat: Node in get_tree().get_nodes_in_group(&"matador"):
|
||||
if not _roll_hit.has(mat) and _mat_active(mat):
|
||||
out.append(mat as Node3D)
|
||||
for barrel: Node in get_tree().get_nodes_in_group(&"barrel"):
|
||||
if _barrel_alive(barrel):
|
||||
out.append(barrel as Node3D)
|
||||
return out
|
||||
|
||||
|
||||
func _roll_target_valid() -> bool:
|
||||
return is_instance_valid(_roll_target) and _mat_active(_roll_target)
|
||||
if not is_instance_valid(_roll_target):
|
||||
return false
|
||||
if _roll_target.is_in_group(&"barrel"):
|
||||
return _barrel_alive(_roll_target)
|
||||
return _mat_active(_roll_target)
|
||||
|
||||
|
||||
# A matador still in play — valid, not ragdolled/consumed. Ragdolled bodies report
|
||||
@@ -573,8 +504,39 @@ func _mat_active(mat: Node) -> bool:
|
||||
return is_instance_valid(mat) and mat.has_method(&"is_active") and mat.call(&"is_active")
|
||||
|
||||
|
||||
# End the Powerball early: brake hard (the ball "pops" and unrolls) and drop back to
|
||||
# the normal locomotion path next frame.
|
||||
# A lobby barrel still standing — valid and not already queued for removal by an earlier hit.
|
||||
func _barrel_alive(barrel: Node) -> bool:
|
||||
return is_instance_valid(barrel) and not barrel.is_queued_for_deletion()
|
||||
|
||||
|
||||
# Burst every barrel whose centre sits within range_m on the ground plane. Shared by the
|
||||
# boulder roll (which ploughs straight through them) and the slam shockwave. Each barrel
|
||||
# self-removes and leaves the &"barrel" group, so the HUD's clear-the-lobby tally counts it
|
||||
# exactly once.
|
||||
func _smash_barrels_in_radius(range_m: float) -> void:
|
||||
for barrel: Node in get_tree().get_nodes_in_group(&"barrel"):
|
||||
if not _barrel_alive(barrel):
|
||||
continue
|
||||
var to_b: Vector3 = (barrel as Node3D).global_position - global_position
|
||||
to_b.y = 0.0
|
||||
if to_b.length() <= range_m:
|
||||
barrel.call(&"destroy_barrel")
|
||||
|
||||
|
||||
# True while the boulder roll is live — matadors defer their charge-gore to the roll's own pop
|
||||
# (is_rolling guard in matador._take_bull_charge) so the ball pops instead of quietly goring.
|
||||
func is_rolling() -> bool:
|
||||
return _active_ability == 3 and _ability_active
|
||||
|
||||
|
||||
# True while the dash ability is live. A dash is a committed lunge — it gores/rams on contact
|
||||
# regardless of angle (enemies bypass their horns-first cone while it's set).
|
||||
func is_dashing() -> bool:
|
||||
return _active_ability == 1 and _ability_active
|
||||
|
||||
|
||||
# End the Powerball early: brake hard (the ball "pops" and unrolls) and drop back to the normal
|
||||
# locomotion path next frame.
|
||||
func _end_roll() -> void:
|
||||
_ability_active = false
|
||||
_active_ability = -1
|
||||
@@ -622,27 +584,33 @@ func _hit_matadors_radius(range_m: float, strength: float, up_boost: float = 0.0
|
||||
mat.call(&"apply_ability_hit", (away + Vector3.UP * 0.6).normalized(), strength, up_boost)
|
||||
|
||||
|
||||
# ── Particle helpers ──────────────────────────────────────────────────────────
|
||||
# All FX in this script use unshaded, vertex-coloured, alpha-blended particles;
|
||||
# these three helpers remove the boilerplate each spawn function used to repeat.
|
||||
|
||||
# ── Blood-spurt particle helpers ──────────────────────────────────────────────
|
||||
# The ability effects now live in systems/fx/; only the spear-wound blood below is still built in
|
||||
# code. Shared material, built once so the mobile driver compiles this particle shader a single
|
||||
# time (colour comes per-particle from the colour_ramp via vertex colour, not this material).
|
||||
func _unshaded_material() -> StandardMaterial3D:
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
mat.vertex_color_use_as_albedo = true
|
||||
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||
return mat
|
||||
if _unshaded_mat == null:
|
||||
_unshaded_mat = StandardMaterial3D.new()
|
||||
_unshaded_mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
_unshaded_mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
_unshaded_mat.vertex_color_use_as_albedo = true
|
||||
_unshaded_mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||
return _unshaded_mat
|
||||
|
||||
|
||||
# Low-poly sphere sized for a particle mesh (height is always the diameter).
|
||||
# Low-poly particle-sphere mesh (height = diameter), cached per size so repeats reuse one GPU upload.
|
||||
func _particle_sphere(radius: float, segments: int, rings: int, mat: Material) -> SphereMesh:
|
||||
var key := "%.4f:%d:%d" % [radius, segments, rings]
|
||||
var cached: SphereMesh = _particle_sphere_cache.get(key)
|
||||
if cached != null:
|
||||
return cached
|
||||
var sphere := SphereMesh.new()
|
||||
sphere.radius = radius
|
||||
sphere.height = radius * 2.0
|
||||
sphere.radial_segments = segments
|
||||
sphere.rings = rings
|
||||
sphere.material = mat
|
||||
_particle_sphere_cache[key] = sphere
|
||||
return sphere
|
||||
|
||||
|
||||
@@ -655,281 +623,31 @@ func _free_after(node: Node, delay: float) -> void:
|
||||
|
||||
|
||||
func _spawn_slam_fx() -> void:
|
||||
if not _slam_ring_mesh:
|
||||
_slam_ring_mesh = _make_slam_ring_mesh()
|
||||
var max_r := DP.f("slam_range")
|
||||
var origin := global_position + Vector3(0.0, -0.5, 0.0)
|
||||
_launch_slam_ring(origin, max_r, 0.00, 0.55, Color(1.00, 0.70, 0.10, 0.85))
|
||||
_launch_slam_ring(origin, max_r, 0.10, 0.52, Color(1.00, 0.50, 0.10, 0.65))
|
||||
_launch_slam_ring(origin, max_r, 0.20, 0.48, Color(0.90, 0.30, 0.10, 0.50))
|
||||
_spawn_slam_dust(origin)
|
||||
_impact_fx.call(&"slam_rings", origin, DP.f("slam_range"))
|
||||
Fx.spawn(&"bull_slam", Transform3D(Basis(), origin))
|
||||
camera_pivot.call(&"trigger_hit", 0.60)
|
||||
|
||||
|
||||
func _launch_slam_ring(origin: Vector3, max_r: float,
|
||||
delay: float, duration: float, col: Color) -> void:
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
mat.albedo_color = Color(col.r, col.g, col.b, 0.0)
|
||||
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||
|
||||
var mi := MeshInstance3D.new()
|
||||
mi.mesh = _slam_ring_mesh
|
||||
mi.material_override = mat
|
||||
mi.scale = Vector3(0.02, 1.0, 0.02)
|
||||
get_parent().add_child(mi)
|
||||
mi.global_position = origin
|
||||
|
||||
var tween := create_tween()
|
||||
tween.set_parallel(true)
|
||||
tween.tween_property(mi, "scale",
|
||||
Vector3(max_r, 1.0, max_r), duration)\
|
||||
.set_delay(delay)\
|
||||
.set_trans(Tween.TRANS_EXPO)\
|
||||
.set_ease(Tween.EASE_OUT)
|
||||
var fade := func(a: float) -> void: mat.albedo_color.a = a
|
||||
tween.tween_method(fade, col.a, 0.0, duration)\
|
||||
.set_delay(delay)\
|
||||
.set_trans(Tween.TRANS_SINE)\
|
||||
.set_ease(Tween.EASE_IN)
|
||||
tween.tween_callback(mi.queue_free).set_delay(delay + duration + 0.05)
|
||||
|
||||
|
||||
func _spawn_slam_dust(origin: Vector3) -> void:
|
||||
var sphere := _particle_sphere(0.09, 4, 2, _unshaded_material())
|
||||
|
||||
var ramp := Gradient.new()
|
||||
ramp.set_color(0, Color(0.88, 0.74, 0.46, 1.0))
|
||||
ramp.set_color(1, Color(0.88, 0.74, 0.46, 0.0))
|
||||
|
||||
var scale_curve := Curve.new()
|
||||
scale_curve.add_point(Vector2(0.0, 0.25))
|
||||
scale_curve.add_point(Vector2(0.35, 1.6))
|
||||
scale_curve.add_point(Vector2(1.00, 0.0))
|
||||
|
||||
var p := CPUParticles3D.new()
|
||||
p.one_shot = true
|
||||
p.explosiveness = 0.85
|
||||
p.amount = 48
|
||||
p.lifetime = 1.1
|
||||
p.randomness = 0.5
|
||||
p.local_coords = false
|
||||
p.direction = Vector3.UP
|
||||
p.spread = 85.0
|
||||
p.gravity = Vector3(0.0, -7.0, 0.0)
|
||||
p.initial_velocity_min = 3.5
|
||||
p.initial_velocity_max = 10.0
|
||||
p.scale_amount_min = 0.5
|
||||
p.scale_amount_max = 2.0
|
||||
p.mesh = sphere
|
||||
p.color_ramp = ramp
|
||||
p.scale_amount_curve = scale_curve
|
||||
get_parent().add_child(p)
|
||||
p.global_position = origin
|
||||
p.restart()
|
||||
_free_after(p, 2.5)
|
||||
|
||||
|
||||
func _spawn_kick_fx(back_dir: Vector3) -> void:
|
||||
var origin := global_position + back_dir * 0.55 + Vector3(0.0, -0.25, 0.0)
|
||||
_spawn_kick_dust(origin, back_dir)
|
||||
_spawn_kick_stones(origin, back_dir)
|
||||
_spawn_kick_sparks(origin, back_dir)
|
||||
_spawn_kick_flash(origin)
|
||||
Fx.spawn(&"bull_kick", _aimed_xform(origin, back_dir))
|
||||
_impact_fx.call(&"kick_flash", origin)
|
||||
camera_pivot.call(&"trigger_hit", 0.35)
|
||||
|
||||
|
||||
func _spawn_kick_dust(origin: Vector3, back_dir: Vector3) -> void:
|
||||
var sphere := _particle_sphere(0.12, 4, 2, _unshaded_material())
|
||||
|
||||
var ramp := Gradient.new()
|
||||
ramp.set_color(0, Color(1.0, 0.78, 0.18, 0.95))
|
||||
ramp.set_color(1, Color(1.0, 0.45, 0.05, 0.0))
|
||||
|
||||
var scale_curve := Curve.new()
|
||||
scale_curve.add_point(Vector2(0.0, 0.3))
|
||||
scale_curve.add_point(Vector2(0.3, 1.5))
|
||||
scale_curve.add_point(Vector2(1.0, 0.0))
|
||||
|
||||
var p := CPUParticles3D.new()
|
||||
p.one_shot = true
|
||||
p.explosiveness = 0.80
|
||||
p.amount = 40
|
||||
p.lifetime = 1.2
|
||||
p.randomness = 0.5
|
||||
p.local_coords = false
|
||||
p.direction = (back_dir + Vector3(0.0, 0.7, 0.0)).normalized()
|
||||
p.spread = 65.0
|
||||
p.gravity = Vector3(0.0, -6.0, 0.0)
|
||||
p.initial_velocity_min = 3.0
|
||||
p.initial_velocity_max = 9.0
|
||||
p.scale_amount_min = 0.5
|
||||
p.scale_amount_max = 2.2
|
||||
p.mesh = sphere
|
||||
p.color_ramp = ramp
|
||||
p.scale_amount_curve = scale_curve
|
||||
get_parent().add_child(p)
|
||||
p.global_position = origin
|
||||
p.restart()
|
||||
_free_after(p, 2.5)
|
||||
|
||||
|
||||
func _spawn_kick_stones(origin: Vector3, back_dir: Vector3) -> void:
|
||||
var sphere := _particle_sphere(0.055, 3, 1, _unshaded_material())
|
||||
|
||||
var ramp := Gradient.new()
|
||||
ramp.set_color(0, Color(0.62, 0.58, 0.52, 1.0))
|
||||
ramp.set_color(1, Color(0.62, 0.58, 0.52, 0.0))
|
||||
|
||||
var scale_curve := Curve.new()
|
||||
scale_curve.add_point(Vector2(0.0, 1.0))
|
||||
scale_curve.add_point(Vector2(0.6, 1.0))
|
||||
scale_curve.add_point(Vector2(1.0, 0.0))
|
||||
|
||||
var p := CPUParticles3D.new()
|
||||
p.one_shot = true
|
||||
p.explosiveness = 0.92
|
||||
p.amount = 14
|
||||
p.lifetime = 0.65
|
||||
p.randomness = 0.6
|
||||
p.local_coords = false
|
||||
p.direction = (back_dir + Vector3(0.0, 0.35, 0.0)).normalized()
|
||||
p.spread = 45.0
|
||||
p.gravity = Vector3(0.0, -9.8, 0.0)
|
||||
p.initial_velocity_min = 5.0
|
||||
p.initial_velocity_max = 14.0
|
||||
p.scale_amount_min = 0.5
|
||||
p.scale_amount_max = 1.8
|
||||
p.mesh = sphere
|
||||
p.color_ramp = ramp
|
||||
p.scale_amount_curve = scale_curve
|
||||
get_parent().add_child(p)
|
||||
p.global_position = origin
|
||||
p.restart()
|
||||
_free_after(p, 2.0)
|
||||
|
||||
|
||||
func _spawn_kick_flash(origin: Vector3) -> void:
|
||||
var mat := _unshaded_material()
|
||||
mat.albedo_color = Color(1.0, 0.97, 0.40, 0.0)
|
||||
|
||||
var sphere := _particle_sphere(1.0, 8, 4, mat)
|
||||
|
||||
var mi := MeshInstance3D.new()
|
||||
mi.mesh = sphere
|
||||
mi.material_override = mat
|
||||
mi.scale = Vector3(0.05, 0.05, 0.05)
|
||||
get_parent().add_child(mi)
|
||||
mi.global_position = origin
|
||||
|
||||
var tween := create_tween()
|
||||
tween.set_parallel(true)
|
||||
tween.tween_property(mi, "scale", Vector3(0.7, 0.7, 0.7), 0.13)\
|
||||
.set_trans(Tween.TRANS_EXPO).set_ease(Tween.EASE_OUT)
|
||||
var fade := func(a: float) -> void: mat.albedo_color.a = a
|
||||
tween.tween_method(fade, 0.92, 0.0, 0.14)\
|
||||
.set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN)
|
||||
tween.tween_callback(mi.queue_free).set_delay(0.16)
|
||||
|
||||
|
||||
func _spawn_dash_burst() -> void:
|
||||
var box := BoxMesh.new()
|
||||
box.size = Vector3(0.045, 0.045, 0.30)
|
||||
box.material = _unshaded_material()
|
||||
|
||||
var ramp := Gradient.new()
|
||||
ramp.set_color(0, Color(0.50, 0.82, 1.0, 1.0))
|
||||
ramp.set_color(1, Color(0.50, 0.82, 1.0, 0.0))
|
||||
|
||||
var scale_curve := Curve.new()
|
||||
scale_curve.add_point(Vector2(0.0, 1.0))
|
||||
scale_curve.add_point(Vector2(1.0, 0.0))
|
||||
|
||||
var p := CPUParticles3D.new()
|
||||
p.one_shot = true
|
||||
p.explosiveness = 0.92
|
||||
p.amount = 18
|
||||
p.lifetime = 0.22
|
||||
p.randomness = 0.15
|
||||
p.local_coords = false
|
||||
p.direction = _dash_dir
|
||||
p.spread = 15.0
|
||||
p.gravity = Vector3.ZERO
|
||||
p.initial_velocity_min = 18.0
|
||||
p.initial_velocity_max = 35.0
|
||||
p.scale_amount_min = 0.8
|
||||
p.scale_amount_max = 2.8
|
||||
p.mesh = box
|
||||
p.color_ramp = ramp
|
||||
p.scale_amount_curve = scale_curve
|
||||
get_parent().add_child(p)
|
||||
p.global_position = global_position + Vector3(0.0, 0.2, 0.0)
|
||||
p.restart()
|
||||
_free_after(p, 0.6)
|
||||
Fx.spawn(&"bull_dash", _aimed_xform(global_position + Vector3(0.0, 0.2, 0.0), _dash_dir))
|
||||
|
||||
|
||||
func _spawn_kick_sparks(origin: Vector3, back_dir: Vector3) -> void:
|
||||
var sphere := _particle_sphere(0.04, 3, 1, _unshaded_material())
|
||||
|
||||
var ramp := Gradient.new()
|
||||
ramp.set_color(0, Color(1.0, 0.98, 0.60, 1.0))
|
||||
ramp.set_color(1, Color(1.0, 1.0, 1.0, 0.0))
|
||||
|
||||
var scale_curve := Curve.new()
|
||||
scale_curve.add_point(Vector2(0.0, 1.0))
|
||||
scale_curve.add_point(Vector2(0.5, 0.6))
|
||||
scale_curve.add_point(Vector2(1.0, 0.0))
|
||||
|
||||
var p := CPUParticles3D.new()
|
||||
p.one_shot = true
|
||||
p.explosiveness = 0.95
|
||||
p.amount = 22
|
||||
p.lifetime = 0.28
|
||||
p.randomness = 0.4
|
||||
p.local_coords = false
|
||||
p.direction = (back_dir + Vector3(0.0, 0.5, 0.0)).normalized()
|
||||
p.spread = 30.0
|
||||
p.gravity = Vector3(0.0, -14.0, 0.0)
|
||||
p.initial_velocity_min = 8.0
|
||||
p.initial_velocity_max = 18.0
|
||||
p.scale_amount_min = 0.6
|
||||
p.scale_amount_max = 2.0
|
||||
p.mesh = sphere
|
||||
p.color_ramp = ramp
|
||||
p.scale_amount_curve = scale_curve
|
||||
get_parent().add_child(p)
|
||||
p.global_position = origin
|
||||
p.restart()
|
||||
_free_after(p, 1.0)
|
||||
|
||||
|
||||
func _make_slam_ring_mesh() -> ArrayMesh:
|
||||
const INNER_R: float = 0.82
|
||||
const OUTER_R: float = 1.00
|
||||
const SEGMENTS: int = 48
|
||||
|
||||
var verts := PackedVector3Array()
|
||||
var indices := PackedInt32Array()
|
||||
|
||||
for i: int in SEGMENTS:
|
||||
var a0 := i / float(SEGMENTS) * TAU
|
||||
var a1 := (i + 1) / float(SEGMENTS) * TAU
|
||||
var vi := i * 4
|
||||
verts.append(Vector3(cos(a0) * INNER_R, 0.0, sin(a0) * INNER_R))
|
||||
verts.append(Vector3(cos(a0) * OUTER_R, 0.0, sin(a0) * OUTER_R))
|
||||
verts.append(Vector3(cos(a1) * OUTER_R, 0.0, sin(a1) * OUTER_R))
|
||||
verts.append(Vector3(cos(a1) * INNER_R, 0.0, sin(a1) * INNER_R))
|
||||
indices.append_array([vi, vi + 2, vi + 1, vi, vi + 3, vi + 2])
|
||||
|
||||
var arrays: Array = []
|
||||
arrays.resize(Mesh.ARRAY_MAX)
|
||||
arrays[Mesh.ARRAY_VERTEX] = verts
|
||||
arrays[Mesh.ARRAY_INDEX] = indices
|
||||
|
||||
var mesh := ArrayMesh.new()
|
||||
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||
return mesh
|
||||
# A transform at `origin` whose local -Z faces `forward` (flattened to horizontal). The kick/dash
|
||||
# effects bake their spray in local space, so aiming the spawn transform points the burst the
|
||||
# right way. Falls back to an unrotated transform when `forward` has no horizontal component.
|
||||
func _aimed_xform(origin: Vector3, forward: Vector3) -> Transform3D:
|
||||
var flat := Vector3(forward.x, 0.0, forward.z)
|
||||
if flat.length_squared() < 0.0001:
|
||||
return Transform3D(Basis(), origin)
|
||||
return Transform3D(Basis(), origin).looking_at(origin + flat.normalized(), Vector3.UP)
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
@@ -1019,13 +737,15 @@ func _physics_process(delta: float) -> void:
|
||||
cube_guy.rotation.y = lerp_angle(
|
||||
cube_guy.rotation.y, target_angle, delta * DP.f("visual_turn_speed"))
|
||||
|
||||
# Looping DASH clip while genuinely moving on the ground, idle otherwise.
|
||||
# Looping WALK clip while genuinely moving on the ground, idle otherwise.
|
||||
_update_locomotion_anim(on_floor and flat_speed > 1.0)
|
||||
|
||||
_pre_slide_vel_y = velocity.y
|
||||
var pre_wall_vel := Vector3(velocity.x, 0.0, velocity.z)
|
||||
move_and_slide()
|
||||
|
||||
|
||||
|
||||
# ── Wall bounce — no sliding, guaranteed exit ────────────────────────────
|
||||
# pre_wall_vel is captured before move_and_slide strips the into-wall component,
|
||||
# so into_spd correctly reflects how hard the bull hit the wall even when stationary.
|
||||
@@ -1041,11 +761,14 @@ func _physics_process(delta: float) -> void:
|
||||
velocity.z = flat_n.z * exit_spd
|
||||
pre_wall_vel = Vector3(velocity.x, 0.0, velocity.z)
|
||||
|
||||
# ── Bounce on landing ─────────────────────────────────────────────────────
|
||||
# Ram straight through a barrel that formed the wall we bounced off.
|
||||
var hit := col.get_collider()
|
||||
if hit != null and hit.has_method(&"destroy_barrel"):
|
||||
hit.call(&"destroy_barrel")
|
||||
|
||||
# ── Landing. Don't bounce ─────────────────────────────────────────────────────
|
||||
if is_on_floor() and not _was_on_floor:
|
||||
var impact := -_pre_slide_vel_y
|
||||
if impact > DP.f("bounce_min_impact") and not _slam_pending:
|
||||
velocity.y = impact * DP.f("bounce_restitution")
|
||||
velocity.y = 0.0
|
||||
|
||||
# ── Slam landing ──────────────────────────────────────────────────────────
|
||||
if _slam_pending and is_on_floor() and not _was_on_floor:
|
||||
@@ -1058,6 +781,7 @@ func _physics_process(delta: float) -> void:
|
||||
_play_idle()
|
||||
_spawn_slam_fx()
|
||||
_hit_matadors_radius(DP.f("slam_range"), DP.f("slam_strength"), DP.f("slam_launch_up"))
|
||||
_smash_barrels_in_radius(DP.f("slam_range"))
|
||||
|
||||
# ── Dust ──────────────────────────────────────────────────────────────────
|
||||
if flat_speed > DP.f("dust_charge_spd") and on_floor:
|
||||
@@ -1073,8 +797,6 @@ func _physics_process(delta: float) -> void:
|
||||
if vel_flat.length_squared() > 0.1:
|
||||
_charge_trail_emitter.direction = (-vel_flat.normalized() + Vector3(0.0, 0.25, 0.0)).normalized()
|
||||
|
||||
# ── Hoof sounds ───────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
func get_hp() -> int:
|
||||
return _hp
|
||||
@@ -1084,6 +806,22 @@ func get_max_hp() -> int:
|
||||
return _max_hp
|
||||
|
||||
|
||||
# Bonus pips currently on the bar — the HUD colours this many top pips yellow.
|
||||
func get_bonus_hp() -> int:
|
||||
return _bonus_hp
|
||||
|
||||
|
||||
# Award extra health (from a lobby reward): grows both the cap and the current bar so the
|
||||
# new pips arrive full, and marks them as bonus so the HUD paints them yellow.
|
||||
func grant_bonus_hp(amount: int) -> void:
|
||||
if amount <= 0:
|
||||
return
|
||||
_bonus_hp += amount
|
||||
_max_hp += amount
|
||||
_hp += amount
|
||||
health_changed.emit(_hp, _max_hp)
|
||||
|
||||
|
||||
# A clean hit — thrown or swung blade — costs the bull one HP pip; the run ends only
|
||||
# when the last pip is gone. `cause` ("gored" / "thrown") tags how it happened for the
|
||||
# death notice. I-frames absorb a flurry so several blades in one pass drop one pip,
|
||||
@@ -1097,6 +835,10 @@ func take_sword_hit(cause: String = "", hit_pos: Vector3 = Vector3.ZERO) -> void
|
||||
health_changed.emit(_hp, _max_hp)
|
||||
hit_taken.emit(cause)
|
||||
camera_pivot.call(&"trigger_hit", 0.7)
|
||||
# Vibrate controller 0 with half weak strength, full strong strength, for 0.5 seconds
|
||||
if DP.b("gamepad_vibration"):
|
||||
Input.start_joy_vibration(0, 0.5, 1, 0.2)
|
||||
|
||||
# A spear gores a spot — blood spurts from the entry wound (where it struck the
|
||||
# bull). Melee gores pass no position and skip the spurt; gore can be turned off.
|
||||
if cause == "thrown" and Settings.gore:
|
||||
@@ -1112,6 +854,21 @@ func take_sword_hit(cause: String = "", hit_pos: Vector3 = Vector3.ZERO) -> void
|
||||
died.emit(cause)
|
||||
|
||||
|
||||
# Pop the bull upward — a smash hitbox knocks it off its feet. Only ever raises the
|
||||
# vertical speed so a downward-moving bull still gets launched.
|
||||
func apply_knock_up(v: float) -> void:
|
||||
velocity.y = maxf(velocity.y, v)
|
||||
|
||||
|
||||
# Typed entry point for the unified hit pipeline: a Hitbox (or any attacker) hands the bull a
|
||||
# DamageEvent instead of calling take_sword_hit directly. A thin adapter over the existing pip /
|
||||
# knock-up logic, so behaviour is unchanged while callers migrate off the stringly path.
|
||||
func receive_damage(event: DamageEvent) -> void:
|
||||
take_sword_hit(event.cause_name(), event.hit_position)
|
||||
if event.knock_up > 0.0:
|
||||
apply_knock_up(event.knock_up)
|
||||
|
||||
|
||||
# Blood spurt from a spear wound: a short-lived spray of dark-red gobs flung out of the
|
||||
# entry point, biased outward from the bull's centre (so it reads as bursting from the
|
||||
# hide) with a touch of lift before gravity drags it down.
|
||||
@@ -1155,7 +912,6 @@ func _spawn_blood(hit_pos: Vector3) -> void:
|
||||
_free_after(p, 1.5)
|
||||
|
||||
|
||||
#hit flashing
|
||||
func _hit_flash() -> void:
|
||||
var meshes := find_children("*", "MeshInstance3D", true, false)
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
[gd_scene format=3 uid="uid://crtm0xtiql3r8"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bn777gguclud5" path="res://player.gd" id="1_xhfnw"]
|
||||
[ext_resource type="Script" uid="uid://cpyq2r5cq653g" path="res://camera_iso.gd" id="2_camera_iso"]
|
||||
[ext_resource type="PackedScene" uid="uid://bun14xmk20pcj" path="res://Assets/bull.fbx" id="4_e80uo"]
|
||||
[ext_resource type="Script" uid="uid://bn777gguclud5" path="res://actors/player/player.gd" id="1_xhfnw"]
|
||||
[ext_resource type="Script" uid="uid://cpyq2r5cq653g" path="res://actors/player/camera_iso.gd" id="2_camera_iso"]
|
||||
[ext_resource type="PackedScene" uid="uid://bun14xmk20pcj" path="res://art/characters/bull.fbx" id="4_e80uo"]
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_kne1u"]
|
||||
radius = 0.7495117
|
||||
height = 1.845398
|
||||
|
||||
[node name="Player" type="CharacterBody3D" unique_id=165510044]
|
||||
collision_layer = 3
|
||||
collision_layer = 2
|
||||
collision_mask = 9
|
||||
script = ExtResource("1_xhfnw")
|
||||
|
||||
[node name="CollisionBull" type="CollisionShape3D" parent="." unique_id=571780801]
|
||||
@@ -22,7 +23,7 @@ script = ExtResource("2_camera_iso")
|
||||
[node name="bull" parent="." unique_id=1386495166 instance=ExtResource("4_e80uo")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.655, 0.23)
|
||||
|
||||
[node name="Skeleton3D" parent="bull/Armature" parent_id_path=PackedInt32Array(1386495166, 1942410469) index="0" unique_id=550138996]
|
||||
[node name="Skeleton3D" parent="bull/Armature" parent_id_path=PackedInt32Array(1386495166, 1057197468) index="0" unique_id=840295370]
|
||||
bones/0/position = Vector3(0, 0, 0.009963839)
|
||||
bones/0/rotation = Quaternion(0.7071068, 0, 0, 0.7071067)
|
||||
bones/0/scale = Vector3(1, 1, 1)
|
||||
@@ -0,0 +1,14 @@
|
||||
extends Node3D
|
||||
|
||||
var player: Node3D = null
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
# If we don't have the player yet, try to find them in the group
|
||||
if not is_instance_valid(player):
|
||||
var players = get_tree().get_nodes_in_group("player")
|
||||
if players.size() > 0:
|
||||
player = players[0] as Node3D
|
||||
|
||||
# If the player exists, follow their position
|
||||
if is_instance_valid(player):
|
||||
global_position = player.global_position
|
||||
@@ -0,0 +1 @@
|
||||
uid://cmapsaj010q40
|
||||
@@ -0,0 +1,14 @@
|
||||
[gd_scene format=3 uid="uid://4v25hb0qu5l0"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://cmapsaj010q40" path="res://actors/player/player_follow_light.gd" id="1_w7wrt"]
|
||||
|
||||
[node name="PlayerFollowLight" type="Node3D" unique_id=1889971494]
|
||||
script = ExtResource("1_w7wrt")
|
||||
|
||||
[node name="SpotLight3D" type="SpotLight3D" parent="." unique_id=1523840927]
|
||||
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 0, 2.6136062, 1.0431258e-07)
|
||||
light_color = Color(0.9999998, 0.8702623, 0.60548127, 1)
|
||||
light_energy = 8.0
|
||||
shadow_enabled = true
|
||||
distance_fade_length = 30.45
|
||||
spot_angle = 72.62192
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -1 +0,0 @@
|
||||
uid://sbf260u32upu
|
||||
@@ -4,12 +4,12 @@ importer="scene"
|
||||
importer_version=1
|
||||
type="PackedScene"
|
||||
uid="uid://cmxqocsdbgufr"
|
||||
path="res://.godot/imported/mees_plaksutab.glb-0ce4e4ee8c8f92069abfac9352f0f4b9.scn"
|
||||
path="res://.godot/imported/crowd_clap.glb-8bb3f532af711440142a67228d380ce0.scn"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://Animations/mees_plaksutab.glb"
|
||||
dest_files=["res://.godot/imported/mees_plaksutab.glb-0ce4e4ee8c8f92069abfac9352f0f4b9.scn"]
|
||||
source_file="res://art/anim/crowd_clap.glb"
|
||||
dest_files=["res://.godot/imported/crowd_clap.glb-8bb3f532af711440142a67228d380ce0.scn"]
|
||||
|
||||
[params]
|
||||
|
||||
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
"""
|
||||
Blender 5.1 script: create walk + idle animations for matador_v02.
|
||||
Run: blender --background matador_v02.blend --python create_matador_anims.py
|
||||
Blender 5.1 script: create walk + idle animations for matador.
|
||||
Run: blender --background matador.blend --python create_matador_anims.py
|
||||
"""
|
||||
import bpy, math, os
|
||||
|
||||
@@ -130,7 +130,7 @@ ti.name = 'idle'
|
||||
si = ti.strips.new('idle', 0, idle)
|
||||
|
||||
# ── export FBX ─────────────────────────────────────────────────────────────────
|
||||
fbx_path = os.path.join(os.path.dirname(bpy.data.filepath), 'matador_v02.fbx')
|
||||
fbx_path = os.path.join(os.path.dirname(bpy.data.filepath), 'matador.fbx')
|
||||
bpy.ops.export_scene.fbx(
|
||||
filepath=fbx_path,
|
||||
use_selection=False,
|
||||
@@ -1,8 +1,8 @@
|
||||
"""
|
||||
Blender 5.1 script: render preview frames of matador animations to PNG.
|
||||
Run: blender --background matador_v02.blend --python render_anim_preview.py
|
||||
Run: blender --background matador.blend --python render_anim_preview.py
|
||||
|
||||
Outputs to Blender/preview/ directory:
|
||||
Outputs to art/blender/preview/ directory:
|
||||
walk_frame_00.png .. walk_frame_09.png
|
||||
idle_frame_00.png
|
||||
|
||||
Binary file not shown.
@@ -3,13 +3,13 @@
|
||||
importer="scene"
|
||||
importer_version=1
|
||||
type="PackedScene"
|
||||
uid="uid://cwmo5vh738x6w"
|
||||
path="res://.godot/imported/Lobby_level.glb-a7e896ecf7377d9530a80d5e63140cc3.scn"
|
||||
uid="uid://cofv5dbavesis"
|
||||
path="res://.godot/imported/bear.glb-75dd9ad1d0d42d959c02624cfc22996d.scn"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://Assets/Lobby_level.glb"
|
||||
dest_files=["res://.godot/imported/Lobby_level.glb-a7e896ecf7377d9530a80d5e63140cc3.scn"]
|
||||
source_file="res://art/characters/bear.glb"
|
||||
dest_files=["res://.godot/imported/bear.glb-75dd9ad1d0d42d959c02624cfc22996d.scn"]
|
||||
|
||||
[params]
|
||||
|
||||
@@ -4,12 +4,12 @@ importer="scene"
|
||||
importer_version=1
|
||||
type="PackedScene"
|
||||
uid="uid://bun14xmk20pcj"
|
||||
path="res://.godot/imported/bull.fbx-50f6d03a92c862695bbeaeba563be0db.scn"
|
||||
path="res://.godot/imported/bull.fbx-ffca5aad6fa2fef7c6aaeff23784274e.scn"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://Assets/bull.fbx"
|
||||
dest_files=["res://.godot/imported/bull.fbx-50f6d03a92c862695bbeaeba563be0db.scn"]
|
||||
source_file="res://art/characters/bull.fbx"
|
||||
dest_files=["res://.godot/imported/bull.fbx-ffca5aad6fa2fef7c6aaeff23784274e.scn"]
|
||||
|
||||
[params]
|
||||
|
||||
Binary file not shown.
@@ -4,12 +4,12 @@ importer="scene"
|
||||
importer_version=1
|
||||
type="PackedScene"
|
||||
uid="uid://eu2lqjhowj4t"
|
||||
path="res://.godot/imported/Matador.glb-84387af784b13addc4467fc8edace9ff.scn"
|
||||
path="res://.godot/imported/matador.glb-d61955edf618ff60a81602b44843dd9c.scn"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://Assets/Matador.glb"
|
||||
dest_files=["res://.godot/imported/Matador.glb-84387af784b13addc4467fc8edace9ff.scn"]
|
||||
source_file="res://art/characters/matador.glb"
|
||||
dest_files=["res://.godot/imported/matador.glb-d61955edf618ff60a81602b44843dd9c.scn"]
|
||||
|
||||
[params]
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
importer="font_data_dynamic"
|
||||
type="FontFile"
|
||||
uid="uid://fu84fchl1im6"
|
||||
path="res://.godot/imported/Cinzel.ttf-b9b78caa7497d2ec78b922cb77f34333.fontdata"
|
||||
path="res://.godot/imported/Cinzel.ttf-95f91c70d492f60cef14788d0547dff4.fontdata"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://fonts/Cinzel.ttf"
|
||||
dest_files=["res://.godot/imported/Cinzel.ttf-b9b78caa7497d2ec78b922cb77f34333.fontdata"]
|
||||
source_file="res://art/fonts/Cinzel.ttf"
|
||||
dest_files=["res://.godot/imported/Cinzel.ttf-95f91c70d492f60cef14788d0547dff4.fontdata"]
|
||||
|
||||
[params]
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
importer="font_data_dynamic"
|
||||
type="FontFile"
|
||||
uid="uid://rcfn6xhs7fur"
|
||||
path="res://.godot/imported/CinzelDecorative-Bold.ttf-fd8dd0c7ef2ceb6acaeb4b78a3c0a47b.fontdata"
|
||||
path="res://.godot/imported/CinzelDecorative-Bold.ttf-71cca077ba75a6c829f53e2548a2580d.fontdata"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://fonts/CinzelDecorative-Bold.ttf"
|
||||
dest_files=["res://.godot/imported/CinzelDecorative-Bold.ttf-fd8dd0c7ef2ceb6acaeb4b78a3c0a47b.fontdata"]
|
||||
source_file="res://art/fonts/CinzelDecorative-Bold.ttf"
|
||||
dest_files=["res://.godot/imported/CinzelDecorative-Bold.ttf-71cca077ba75a6c829f53e2548a2580d.fontdata"]
|
||||
|
||||
[params]
|
||||
|
||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user