Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a4d18e4928 | |||
| c742c4ae0e | |||
| 5caccd318c | |||
| 78efca445a | |||
| 5078502228 | |||
| 7bc241bbe6 | |||
| 534b7f45ca | |||
| 524a222b6c | |||
| 9ca6f282f5 | |||
| 64a8a9863d | |||
| c5ef68e82a | |||
| 4569d3f5d4 | |||
| 7b6a141767 | |||
| b839252fe0 | |||
| ac0da2a6fc | |||
| c3f92d8758 | |||
| 9c4563a973 | |||
| ffe3052468 | |||
| f02185e859 | |||
| 70274f1773 | |||
| 973db7d8b8 | |||
| 1ea7f7a744 | |||
| 64fabb688f | |||
| 9122a601d0 | |||
| a6f938a0da | |||
| 994ce3be73 | |||
| 39aacc3c87 | |||
| 400ae24f92 | |||
| 79ea5bb2be | |||
| 981ebf1910 | |||
| 01d6ecf475 | |||
| 931ddf0eeb | |||
| 50073b2574 | |||
| 07f8a71f2c | |||
| 34dc5b026e | |||
| 5c1e881a53 | |||
| 4efc6763f0 | |||
| 76d4fee33c | |||
| 271be0b26b | |||
| 98c6f1285a | |||
| 4d66df8c7e | |||
| 41f18cb5d6 | |||
| 6b6a926715 | |||
| dae7ec06da | |||
| 1eb06876e9 | |||
| 1021a07b04 |
@@ -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
|
||||
@@ -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/
|
||||
|
||||
@@ -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
|
||||
|
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
|
||||
|
Before Width: | Height: | Size: 598 KiB |
|
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
|
||||
|
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
|
||||
@@ -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
|
||||
|
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"]
|
||||
@@ -1 +0,0 @@
|
||||
uid://ymaeifakqovf
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -141,7 +164,7 @@ func _hit_flash() -> void:
|
||||
_hit_flash_tween = create_tween()
|
||||
_hit_flash_tween.tween_interval(0.10)
|
||||
_hit_flash_tween.tween_callback(_clear_hit_flash)
|
||||
|
||||
|
||||
func _make_hit_overlay() -> StandardMaterial3D:
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.albedo_color = Color(1.0, 0.05, 0.05, 1.0)
|
||||
@@ -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
|
||||
@@ -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,13 +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)
|
||||
|
||||
# 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:
|
||||
velocity.y = 0.0
|
||||
#Bouncing landing below
|
||||
#var impact := -_pre_slide_vel_y
|
||||
#if impact > DP.f("bounce_min_impact") and not _slam_pending:
|
||||
#velocity.y = impact * DP.f("bounce_restitution")
|
||||
|
||||
# ── Slam landing ──────────────────────────────────────────────────────────
|
||||
if _slam_pending and is_on_floor() and not _was_on_floor:
|
||||
@@ -1060,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:
|
||||
@@ -1075,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
|
||||
@@ -1086,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,
|
||||
@@ -1099,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:
|
||||
@@ -1114,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,9 +910,8 @@ func _spawn_blood(hit_pos: Vector3) -> void:
|
||||
p.global_position = hit_pos
|
||||
p.restart()
|
||||
_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]
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -4,12 +4,12 @@ importer="scene"
|
||||
importer_version=1
|
||||
type="PackedScene"
|
||||
uid="uid://bdbx0wwa73rhp"
|
||||
path="res://.godot/imported/Bear_FX.glb-5bac655e61b5e140f6cd145298d00ca8.scn"
|
||||
path="res://.godot/imported/bear_fx.glb-ec36223896f7444a4dbe06fe70380d22.scn"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://Assets/fx/Bear_FX.glb"
|
||||
dest_files=["res://.godot/imported/Bear_FX.glb-5bac655e61b5e140f6cd145298d00ca8.scn"]
|
||||
source_file="res://art/fx/bear_fx.glb"
|
||||
dest_files=["res://.godot/imported/bear_fx.glb-ec36223896f7444a4dbe06fe70380d22.scn"]
|
||||
|
||||
[params]
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bxm7c6ldrdiq5"
|
||||
path.bptc="res://.godot/imported/citrus_orchard_road_puresky_4k.hdr-262868e8787abf05f7044da72beefce7.bptc.ctex"
|
||||
path.astc="res://.godot/imported/citrus_orchard_road_puresky_4k.hdr-262868e8787abf05f7044da72beefce7.astc.ctex"
|
||||
path.bptc="res://.godot/imported/citrus_orchard_road_puresky_4k.hdr-227da4c15c620e5b14eb76c27dcd326f.bptc.ctex"
|
||||
path.astc="res://.godot/imported/citrus_orchard_road_puresky_4k.hdr-227da4c15c620e5b14eb76c27dcd326f.astc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||
"vram_texture": true
|
||||
@@ -12,8 +12,8 @@ metadata={
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://HDRI/citrus_orchard_road_puresky_4k.hdr"
|
||||
dest_files=["res://.godot/imported/citrus_orchard_road_puresky_4k.hdr-262868e8787abf05f7044da72beefce7.bptc.ctex", "res://.godot/imported/citrus_orchard_road_puresky_4k.hdr-262868e8787abf05f7044da72beefce7.astc.ctex"]
|
||||
source_file="res://art/hdri/citrus_orchard_road_puresky_4k.hdr"
|
||||
dest_files=["res://.godot/imported/citrus_orchard_road_puresky_4k.hdr-227da4c15c620e5b14eb76c27dcd326f.bptc.ctex", "res://.godot/imported/citrus_orchard_road_puresky_4k.hdr-227da4c15c620e5b14eb76c27dcd326f.astc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://3syhacswepyj"
|
||||
path.bptc="res://.godot/imported/kloppenheim_06_puresky_4k.hdr-026f0eabdfce50035e2f8417ce3e0060.bptc.ctex"
|
||||
path.astc="res://.godot/imported/kloppenheim_06_puresky_4k.hdr-026f0eabdfce50035e2f8417ce3e0060.astc.ctex"
|
||||
path.bptc="res://.godot/imported/kloppenheim_06_puresky_4k.hdr-8cc5d2aeb82da4bd42ed9b64393cbf3a.bptc.ctex"
|
||||
path.astc="res://.godot/imported/kloppenheim_06_puresky_4k.hdr-8cc5d2aeb82da4bd42ed9b64393cbf3a.astc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||
"vram_texture": true
|
||||
@@ -12,8 +12,8 @@ metadata={
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://HDRI/kloppenheim_06_puresky_4k.hdr"
|
||||
dest_files=["res://.godot/imported/kloppenheim_06_puresky_4k.hdr-026f0eabdfce50035e2f8417ce3e0060.bptc.ctex", "res://.godot/imported/kloppenheim_06_puresky_4k.hdr-026f0eabdfce50035e2f8417ce3e0060.astc.ctex"]
|
||||
source_file="res://art/hdri/kloppenheim_06_puresky_4k.hdr"
|
||||
dest_files=["res://.godot/imported/kloppenheim_06_puresky_4k.hdr-8cc5d2aeb82da4bd42ed9b64393cbf3a.bptc.ctex", "res://.godot/imported/kloppenheim_06_puresky_4k.hdr-8cc5d2aeb82da4bd42ed9b64393cbf3a.astc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 144 KiB |