Compare commits
93 Commits
0.2.0
...
534b7f45ca
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| c365f76d80 | |||
| 32ac46f840 | |||
| bd10d1789b | |||
| 170d8ff3f1 | |||
| 550bf2c5ad | |||
| 02f3e283de | |||
| 9123574947 | |||
| aadf59d2d0 | |||
| d57c3dc2a2 | |||
| 09ae6bfbf4 | |||
| 40c1b8e680 | |||
| a1c77fed5e | |||
| 1a8af92d88 | |||
| 8166a6c18e | |||
| 46d6dda325 | |||
| fc9348551d | |||
| c4a19c7443 | |||
| 95c4c0ddf9 | |||
| 6a4f104655 | |||
| d4b67888d7 | |||
| b177e0aee3 | |||
| 2a6f4c0f7b | |||
| 32d0c58f5c | |||
| 251002ea7c | |||
| 986496097f | |||
| d976deeb6a | |||
| d632670092 | |||
| e1899e49f4 | |||
| 6f5f9e6bba | |||
| 91cd027a98 | |||
| ef2fb24969 | |||
| 4c75776c70 | |||
| 7688084b7e | |||
| ccc92f201f | |||
| 008650a87e | |||
| b9e3147519 | |||
| cdba320cbc | |||
| ee7b1617c1 | |||
| d749485126 | |||
| 45427b7070 | |||
| d514435dd8 | |||
| 3eb9fa50f3 | |||
| aee900c009 | |||
| 4cf714f516 | |||
| b78736a1f4 | |||
| 212263c858 | |||
| 8c4eeb0861 | |||
| 95def2d527 | |||
| 9a43a9ff8b | |||
| 7238cbf49e | |||
| 44c667f097 | |||
| 3cd347d420 | |||
| 6414b2e7cc |
@@ -2,4 +2,4 @@ disable:
|
|||||||
- class-definitions-order
|
- class-definitions-order
|
||||||
- no-else-return
|
- no-else-return
|
||||||
max-line-length: 120
|
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,6 +2,9 @@
|
|||||||
.godot/
|
.godot/
|
||||||
/android/
|
/android/
|
||||||
|
|
||||||
|
# Godot editor autosave scratch files (e.g. Bear.tscn12015901897.tmp)
|
||||||
|
*.tmp
|
||||||
|
|
||||||
# JetBrains IDE — contains absolute paths (Godot binary, SDK dirs), varies per user
|
# JetBrains IDE — contains absolute paths (Godot binary, SDK dirs), varies per user
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
[gd_resource type="Animation" format=3 uid="uid://bq71flatfddll"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
resource_name = "Bear_fx_smash_hit"
|
||||||
|
length = 0.23333333
|
||||||
|
tracks/0/type = "position_3d"
|
||||||
|
tracks/0/imported = true
|
||||||
|
tracks/0/enabled = true
|
||||||
|
tracks/0/path = NodePath("Bear_fx_smash_hit")
|
||||||
|
tracks/0/interp = 1
|
||||||
|
tracks/0/loop_wrap = true
|
||||||
|
tracks/0/keys = PackedFloat32Array(0, 1, 0, -0.51246816, 7.1560388, 0.033333335, 1, 0, -0.51246816, 7.1560388, 0.06666667, 1, 1.2735307, -1.4730668, 5.139501, 0.1, 1, 1.3610363, 0.27301335, 5.141279, 0.13333334, 1, 1.3735371, 2.1639488, 5.153728, 0.16666667, 1, 1.3573295, -0.04170864, 5.1945124, 0.2, 1, 1.3061407, -0.3338372, 5.1877995, 0.23333333, 1, 1.2735307, -0.37556988, 5.1810856)
|
||||||
|
tracks/1/type = "rotation_3d"
|
||||||
|
tracks/1/imported = true
|
||||||
|
tracks/1/enabled = true
|
||||||
|
tracks/1/path = NodePath("Bear_fx_smash_hit")
|
||||||
|
tracks/1/interp = 1
|
||||||
|
tracks/1/loop_wrap = true
|
||||||
|
tracks/1/keys = PackedFloat32Array(0, 1, 0, 0, 0, 1, 0.033333335, 1, 0, 0, 0, 1, 0.06666667, 1, 0.045591418, -0.036703378, -0.0016762356, 0.9982843, 0.1, 1, 0.06741002, -0.02538766, 0.014408795, 0.99729824, 0.13333334, 1, 0.05603578, -0.017236687, 0.029419938, 0.99784636, 0.16666667, 1, -0.071329094, -0.014488678, 0.036211126, 0.9966901, 0.23333333, 1, -0.039237745, -0.015645763, 0.035726424, 0.99846846)
|
||||||
|
tracks/2/type = "scale_3d"
|
||||||
|
tracks/2/imported = true
|
||||||
|
tracks/2/enabled = true
|
||||||
|
tracks/2/path = NodePath("Bear_fx_smash_hit")
|
||||||
|
tracks/2/interp = 1
|
||||||
|
tracks/2/loop_wrap = true
|
||||||
|
tracks/2/keys = PackedFloat32Array(0, 1, 0, 0, 0, 0.033333335, 1, 0, 0, 0, 0.06666667, 1, 0.55657715, 0.55657715, 0.5565772, 0.23333333, 1, 0.55657715, 0.55657715, 0.55657715)
|
||||||
|
tracks/3/type = "position_3d"
|
||||||
|
tracks/3/imported = true
|
||||||
|
tracks/3/enabled = true
|
||||||
|
tracks/3/path = NodePath("Bear_fx_jump_smash")
|
||||||
|
tracks/3/interp = 1
|
||||||
|
tracks/3/loop_wrap = true
|
||||||
|
tracks/3/keys = PackedFloat32Array(0, 1, 0, -0.51246816, 0)
|
||||||
|
tracks/4/type = "scale_3d"
|
||||||
|
tracks/4/imported = true
|
||||||
|
tracks/4/enabled = true
|
||||||
|
tracks/4/path = NodePath("Bear_fx_jump_smash")
|
||||||
|
tracks/4/interp = 1
|
||||||
|
tracks/4/loop_wrap = true
|
||||||
|
tracks/4/keys = PackedFloat32Array(0, 1, 0, 0, 0)
|
||||||
|
tracks/5/type = "position_3d"
|
||||||
|
tracks/5/imported = true
|
||||||
|
tracks/5/enabled = true
|
||||||
|
tracks/5/path = NodePath("Bear_fx_claw_attack_R")
|
||||||
|
tracks/5/interp = 1
|
||||||
|
tracks/5/loop_wrap = true
|
||||||
|
tracks/5/keys = PackedFloat32Array(0, 1, -3.188013, 1.0970955, 1.7545929)
|
||||||
|
tracks/6/type = "rotation_3d"
|
||||||
|
tracks/6/imported = true
|
||||||
|
tracks/6/enabled = true
|
||||||
|
tracks/6/path = NodePath("Bear_fx_claw_attack_R")
|
||||||
|
tracks/6/interp = 1
|
||||||
|
tracks/6/loop_wrap = true
|
||||||
|
tracks/6/keys = PackedFloat32Array(0, 1, 0, 0, 0, 1)
|
||||||
|
tracks/7/type = "scale_3d"
|
||||||
|
tracks/7/imported = true
|
||||||
|
tracks/7/enabled = true
|
||||||
|
tracks/7/path = NodePath("Bear_fx_claw_attack_R")
|
||||||
|
tracks/7/interp = 1
|
||||||
|
tracks/7/loop_wrap = true
|
||||||
|
tracks/7/keys = PackedFloat32Array(0, 1, 0, 0, 0)
|
||||||
|
tracks/8/type = "position_3d"
|
||||||
|
tracks/8/imported = true
|
||||||
|
tracks/8/enabled = true
|
||||||
|
tracks/8/path = NodePath("Bear_fx_claw_attack_L")
|
||||||
|
tracks/8/interp = 1
|
||||||
|
tracks/8/loop_wrap = true
|
||||||
|
tracks/8/keys = PackedFloat32Array(0, 1, -3.188013, 1.0970955, 1.7545929)
|
||||||
|
tracks/9/type = "rotation_3d"
|
||||||
|
tracks/9/imported = true
|
||||||
|
tracks/9/enabled = true
|
||||||
|
tracks/9/path = NodePath("Bear_fx_claw_attack_L")
|
||||||
|
tracks/9/interp = 1
|
||||||
|
tracks/9/loop_wrap = true
|
||||||
|
tracks/9/keys = PackedFloat32Array(0, 1, 0, 0, 0, 1)
|
||||||
|
tracks/10/type = "scale_3d"
|
||||||
|
tracks/10/imported = true
|
||||||
|
tracks/10/enabled = true
|
||||||
|
tracks/10/path = NodePath("Bear_fx_claw_attack_L")
|
||||||
|
tracks/10/interp = 1
|
||||||
|
tracks/10/loop_wrap = true
|
||||||
|
tracks/10/keys = PackedFloat32Array(0, 1, 0, 0, 0)
|
||||||
|
tracks/11/type = "method"
|
||||||
|
tracks/11/imported = false
|
||||||
|
tracks/11/enabled = true
|
||||||
|
tracks/11/path = NodePath("..")
|
||||||
|
tracks/11/interp = 1
|
||||||
|
tracks/11/loop_wrap = true
|
||||||
|
tracks/11/keys = {
|
||||||
|
"times": PackedFloat32Array(),
|
||||||
|
"transitions": PackedFloat32Array(),
|
||||||
|
"values": []
|
||||||
|
}
|
||||||
@@ -4,8 +4,9 @@ importer="texture"
|
|||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://dk6xuwwa0mfpe"
|
uid="uid://dk6xuwwa0mfpe"
|
||||||
path.s3tc="res://.godot/imported/Arena_sand.png-416de2afe03241df088061e1a5f831af.s3tc.ctex"
|
path.s3tc="res://.godot/imported/Arena_sand.png-416de2afe03241df088061e1a5f831af.s3tc.ctex"
|
||||||
|
path.etc2="res://.godot/imported/Arena_sand.png-416de2afe03241df088061e1a5f831af.etc2.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"imported_formats": ["s3tc_bptc"],
|
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||||
"vram_texture": true
|
"vram_texture": true
|
||||||
}
|
}
|
||||||
generator_parameters={
|
generator_parameters={
|
||||||
@@ -15,7 +16,7 @@ generator_parameters={
|
|||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://Assets/Arena_sand.png"
|
source_file="res://Assets/Arena_sand.png"
|
||||||
dest_files=["res://.godot/imported/Arena_sand.png-416de2afe03241df088061e1a5f831af.s3tc.ctex"]
|
dest_files=["res://.godot/imported/Arena_sand.png-416de2afe03241df088061e1a5f831af.s3tc.ctex", "res://.godot/imported/Arena_sand.png-416de2afe03241df088061e1a5f831af.etc2.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ importer="texture"
|
|||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://b4gkpa7jypcsm"
|
uid="uid://b4gkpa7jypcsm"
|
||||||
path.s3tc="res://.godot/imported/Arena_wall.png-e67bd4bbd2f60583bfc14dd8b7687eef.s3tc.ctex"
|
path.s3tc="res://.godot/imported/Arena_wall.png-e67bd4bbd2f60583bfc14dd8b7687eef.s3tc.ctex"
|
||||||
|
path.etc2="res://.godot/imported/Arena_wall.png-e67bd4bbd2f60583bfc14dd8b7687eef.etc2.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"imported_formats": ["s3tc_bptc"],
|
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||||
"vram_texture": true
|
"vram_texture": true
|
||||||
}
|
}
|
||||||
generator_parameters={
|
generator_parameters={
|
||||||
@@ -15,7 +16,7 @@ generator_parameters={
|
|||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://Assets/Arena_wall.png"
|
source_file="res://Assets/Arena_wall.png"
|
||||||
dest_files=["res://.godot/imported/Arena_wall.png-e67bd4bbd2f60583bfc14dd8b7687eef.s3tc.ctex"]
|
dest_files=["res://.godot/imported/Arena_wall.png-e67bd4bbd2f60583bfc14dd8b7687eef.s3tc.ctex", "res://.godot/imported/Arena_wall.png-e67bd4bbd2f60583bfc14dd8b7687eef.etc2.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ importer="texture"
|
|||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://yercpq3y2o0x"
|
uid="uid://yercpq3y2o0x"
|
||||||
path.s3tc="res://.godot/imported/Arena_wood.png-b4ed20d66b4d375396b0be2f2f863d80.s3tc.ctex"
|
path.s3tc="res://.godot/imported/Arena_wood.png-b4ed20d66b4d375396b0be2f2f863d80.s3tc.ctex"
|
||||||
|
path.etc2="res://.godot/imported/Arena_wood.png-b4ed20d66b4d375396b0be2f2f863d80.etc2.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"imported_formats": ["s3tc_bptc"],
|
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||||
"vram_texture": true
|
"vram_texture": true
|
||||||
}
|
}
|
||||||
generator_parameters={
|
generator_parameters={
|
||||||
@@ -15,7 +16,7 @@ generator_parameters={
|
|||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://Assets/Arena_wood.png"
|
source_file="res://Assets/Arena_wood.png"
|
||||||
dest_files=["res://.godot/imported/Arena_wood.png-b4ed20d66b4d375396b0be2f2f863d80.s3tc.ctex"]
|
dest_files=["res://.godot/imported/Arena_wood.png-b4ed20d66b4d375396b0be2f2f863d80.s3tc.ctex", "res://.godot/imported/Arena_wood.png-b4ed20d66b4d375396b0be2f2f863d80.etc2.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 1.3 MiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[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
|
||||||
@@ -3,13 +3,13 @@
|
|||||||
importer="scene"
|
importer="scene"
|
||||||
importer_version=1
|
importer_version=1
|
||||||
type="PackedScene"
|
type="PackedScene"
|
||||||
uid="uid://dxe72ig8gfbcg"
|
uid="uid://cwmo5vh738x6w"
|
||||||
path="res://.godot/imported/arena model2026.glb-299db01cae891e5005f8ac1e6c83d0a6.scn"
|
path="res://.godot/imported/Lobby_level.glb-a7e896ecf7377d9530a80d5e63140cc3.scn"
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://Assets/arena model2026.glb"
|
source_file="res://Assets/Lobby_level.glb"
|
||||||
dest_files=["res://.godot/imported/arena model2026.glb-299db01cae891e5005f8ac1e6c83d0a6.scn"]
|
dest_files=["res://.godot/imported/Lobby_level.glb-a7e896ecf7377d9530a80d5e63140cc3.scn"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 598 KiB |
@@ -0,0 +1,45 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://xtv6u00w4nmy"
|
||||||
|
path.s3tc="res://.godot/imported/Lobby_level_sand.png-bb70b0169c6b5159e020ce143afe9786.s3tc.ctex"
|
||||||
|
path.etc2="res://.godot/imported/Lobby_level_sand.png-bb70b0169c6b5159e020ce143afe9786.etc2.ctex"
|
||||||
|
metadata={
|
||||||
|
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||||
|
"vram_texture": true
|
||||||
|
}
|
||||||
|
generator_parameters={
|
||||||
|
"md5": "c8b9b7d6205ce80dfcfaa38152b996fe"
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://Assets/Lobby_level_sand.png"
|
||||||
|
dest_files=["res://.godot/imported/Lobby_level_sand.png-bb70b0169c6b5159e020ce143afe9786.s3tc.ctex", "res://.godot/imported/Lobby_level_sand.png-bb70b0169c6b5159e020ce143afe9786.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
|
||||||
|
After Width: | Height: | Size: 412 KiB |
@@ -0,0 +1,45 @@
|
|||||||
|
[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
|
||||||
|
After Width: | Height: | Size: 180 KiB |
@@ -0,0 +1,45 @@
|
|||||||
|
[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
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://dctmlh4hnuux1"
|
||||||
|
path="res://.godot/imported/Lobby_underground.glb-55ed49331d7074ef560bfa0a498b89a7.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://Assets/Lobby_underground.glb"
|
||||||
|
dest_files=["res://.godot/imported/Lobby_underground.glb-55ed49331d7074ef560bfa0a498b89a7.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=1
|
||||||
|
After Width: | Height: | Size: 166 KiB |
@@ -0,0 +1,45 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dc4gf36fgsabp"
|
||||||
|
path.s3tc="res://.godot/imported/Lobby_underground_rock.png-76733dc5212e6c79a91e71ce008ebf6f.s3tc.ctex"
|
||||||
|
path.etc2="res://.godot/imported/Lobby_underground_rock.png-76733dc5212e6c79a91e71ce008ebf6f.etc2.ctex"
|
||||||
|
metadata={
|
||||||
|
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||||
|
"vram_texture": true
|
||||||
|
}
|
||||||
|
generator_parameters={
|
||||||
|
"md5": "de3796fe2c1123ae5ba27a0532fda6bd"
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://Assets/Lobby_underground_rock.png"
|
||||||
|
dest_files=["res://.godot/imported/Lobby_underground_rock.png-76733dc5212e6c79a91e71ce008ebf6f.s3tc.ctex", "res://.godot/imported/Lobby_underground_rock.png-76733dc5212e6c79a91e71ce008ebf6f.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
|
||||||
|
After Width: | Height: | Size: 47 KiB |
@@ -0,0 +1,45 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://v0hdo12xule3"
|
||||||
|
path.s3tc="res://.godot/imported/Lobby_underground_rock_B.jpg-c5e86461201f99688c8146fff239a62b.s3tc.ctex"
|
||||||
|
path.etc2="res://.godot/imported/Lobby_underground_rock_B.jpg-c5e86461201f99688c8146fff239a62b.etc2.ctex"
|
||||||
|
metadata={
|
||||||
|
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||||
|
"vram_texture": true
|
||||||
|
}
|
||||||
|
generator_parameters={
|
||||||
|
"md5": "335b739e743244a64c47e2564a9a3652"
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://Assets/Lobby_underground_rock_B.jpg"
|
||||||
|
dest_files=["res://.godot/imported/Lobby_underground_rock_B.jpg-c5e86461201f99688c8146fff239a62b.s3tc.ctex", "res://.godot/imported/Lobby_underground_rock_B.jpg-c5e86461201f99688c8146fff239a62b.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
|
||||||
|
After Width: | Height: | Size: 183 KiB |
@@ -0,0 +1,45 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dcy7fdio5k84j"
|
||||||
|
path.s3tc="res://.godot/imported/Lobby_underground_rock_B.png-3133a2540f4e1417fb5588bad79e38c8.s3tc.ctex"
|
||||||
|
path.etc2="res://.godot/imported/Lobby_underground_rock_B.png-3133a2540f4e1417fb5588bad79e38c8.etc2.ctex"
|
||||||
|
metadata={
|
||||||
|
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||||
|
"vram_texture": true
|
||||||
|
}
|
||||||
|
generator_parameters={
|
||||||
|
"md5": "16ea5e4b01e9b3952b950ff65535e5d5"
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://Assets/Lobby_underground_rock_B.png"
|
||||||
|
dest_files=["res://.godot/imported/Lobby_underground_rock_B.png-3133a2540f4e1417fb5588bad79e38c8.s3tc.ctex", "res://.godot/imported/Lobby_underground_rock_B.png-3133a2540f4e1417fb5588bad79e38c8.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
|
||||||
@@ -28,7 +28,7 @@ meshes/generate_lods=true
|
|||||||
meshes/create_shadow_meshes=true
|
meshes/create_shadow_meshes=true
|
||||||
meshes/light_baking=1
|
meshes/light_baking=1
|
||||||
meshes/lightmap_texel_size=0.2
|
meshes/lightmap_texel_size=0.2
|
||||||
meshes/force_disable_compression=false
|
meshes/force_disable_compression=true
|
||||||
skins/use_named_skins=true
|
skins/use_named_skins=true
|
||||||
animation/import=true
|
animation/import=true
|
||||||
animation/fps=30
|
animation/fps=30
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://bm6rmt3xprhjs"
|
||||||
|
path="res://.godot/imported/bear_level_arena.glb-b6a9df3b338c8b0345fe2642d8b715eb.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://Assets/bear_level_arena.glb"
|
||||||
|
dest_files=["res://.godot/imported/bear_level_arena.glb-b6a9df3b338c8b0345fe2642d8b715eb.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=1
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://dnltsqmuqcxh1"
|
||||||
|
path="res://.godot/imported/Barrel.glb-8809615fbcbd5dc5b64a229316967555.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://Assets/breakables/Barrel.glb"
|
||||||
|
dest_files=["res://.godot/imported/Barrel.glb-8809615fbcbd5dc5b64a229316967555.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=1
|
||||||
@@ -28,7 +28,7 @@ meshes/generate_lods=true
|
|||||||
meshes/create_shadow_meshes=true
|
meshes/create_shadow_meshes=true
|
||||||
meshes/light_baking=1
|
meshes/light_baking=1
|
||||||
meshes/lightmap_texel_size=0.2
|
meshes/lightmap_texel_size=0.2
|
||||||
meshes/force_disable_compression=false
|
meshes/force_disable_compression=true
|
||||||
skins/use_named_skins=true
|
skins/use_named_skins=true
|
||||||
animation/import=true
|
animation/import=true
|
||||||
animation/fps=30
|
animation/fps=30
|
||||||
|
|||||||
|
After Width: | Height: | Size: 552 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dct1d88g015el"
|
||||||
|
path="res://.godot/imported/kleeps_v2.png-55aa0dd25620611263a8b3000c5e2a49.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://Assets/kleeps_v2.png"
|
||||||
|
dest_files=["res://.godot/imported/kleeps_v2.png-55aa0dd25620611263a8b3000c5e2a49.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: 12 MiB |
|
Before Width: | Height: | Size: 12 MiB After Width: | Height: | Size: 3.5 MiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://bh5nuyudh7x44"
|
||||||
|
path="res://.godot/imported/loading_screen.png-253743b71eb7ab3f4f9e2b29cbabf407.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://Assets/loading_screen.png"
|
||||||
|
dest_files=["res://.godot/imported/loading_screen.png-253743b71eb7ab3f4f9e2b29cbabf407.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
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
extends Node3D
|
||||||
|
## Lobby geometry — watches the run state and raises the bear-gate grid once the arena has
|
||||||
|
## been cleared. The DP flag stays as a designer override for testing the animation.
|
||||||
|
|
||||||
|
var bear_gate_open := false
|
||||||
|
|
||||||
|
|
||||||
|
func _process(_delta: float) -> void:
|
||||||
|
if bear_gate_open:
|
||||||
|
return
|
||||||
|
if Run.bear_gate_open() or DP.b("gate_unlocked_bear"):
|
||||||
|
bear_gate_open = true
|
||||||
|
$AnimationPlayer.play("Gate_opening")
|
||||||
|
if DP.b("gamepad_vibration"):
|
||||||
|
Input.start_joy_vibration(0, 0.5, .5, 7)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cwbf7jfkr8j10
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
extends Node3D
|
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
|
||||||
$AnimationPlayer.play("Taunt")
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://bn3xwnlktwdlf
|
|
||||||
@@ -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)
|
|
||||||
@@ -10,16 +10,54 @@ I am a **Godot 4.6+ expert**. I follow current best practices for GDScript, scen
|
|||||||
|
|
||||||
## Project structure
|
## 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
|
||||||
|
Assets/ Animations/ fonts/ HUD/ raw art (not yet consolidated under art/)
|
||||||
|
```
|
||||||
|
|
||||||
| Path | Purpose |
|
| Path | Purpose |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `scene.tscn` | Main scene (arena + player) |
|
| `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 |
|
||||||
| `Player.tscn` | Player scene root (`CharacterBody3D`) |
|
| `levels/` | The level system: per-level module scenes plus the code that defines, loads and sequences them (see rows below) |
|
||||||
| `player.gd` | Movement, charge, turning logic |
|
| `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` |
|
||||||
|
| `actors/player/player.tscn` | Player scene root (`CharacterBody3D`) |
|
||||||
|
| `actors/player/player.gd` | Movement, charge, turning logic |
|
||||||
| `camera_spring_arm.gd` | Mouse-look pivot (`Node3D` + `SpringArm3D`) |
|
| `camera_spring_arm.gd` | Mouse-look pivot (`Node3D` + `SpringArm3D`) |
|
||||||
| `camera_follow.gd` | Smooth camera follow (`Camera3D`) |
|
| `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) |
|
||||||
| `matador_spawn.gd` | Spawns N matadors at random arena positions |
|
| `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 |
|
||||||
| `debug_params.gd` | Runtime-tunable parameter registry (autoload `DP`) |
|
| `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` |
|
||||||
|
| `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`) — 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 |
|
||||||
|
| `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 |
|
||||||
| `Assets/` | Raw 3D assets (`.glb`, `.fbx`) |
|
| `Assets/` | Raw 3D assets (`.glb`, `.fbx`) |
|
||||||
| `Blender/` | Blender source files, animation scripts, FBX exports |
|
| `Blender/` | Blender source files, animation scripts, FBX exports |
|
||||||
| `Blender/create_matador_anims.py` | Creates walk + idle animations and exports FBX |
|
| `Blender/create_matador_anims.py` | Creates walk + idle animations and exports FBX |
|
||||||
@@ -88,10 +126,26 @@ After FBX import, animations appear as `Armature|walk` and `Armature|idle` in An
|
|||||||
bash run_tests.sh
|
bash run_tests.sh
|
||||||
|
|
||||||
# Individual tests:
|
# Individual tests:
|
||||||
gdlint *.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/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/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/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):
|
# Gameplay test with real rendering (inspect frame strip visually):
|
||||||
godot --script tests/gameplay_test.gd # opens a window, saves real screenshots
|
godot --script tests/gameplay_test.gd # opens a window, saves real screenshots
|
||||||
|
|||||||
@@ -4,15 +4,16 @@ importer="texture"
|
|||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://bxm7c6ldrdiq5"
|
uid="uid://bxm7c6ldrdiq5"
|
||||||
path.bptc="res://.godot/imported/citrus_orchard_road_puresky_4k.hdr-262868e8787abf05f7044da72beefce7.bptc.ctex"
|
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"
|
||||||
metadata={
|
metadata={
|
||||||
"imported_formats": ["s3tc_bptc"],
|
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||||
"vram_texture": true
|
"vram_texture": true
|
||||||
}
|
}
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://HDRI/citrus_orchard_road_puresky_4k.hdr"
|
source_file="res://HDRI/citrus_orchard_road_puresky_4k.hdr"
|
||||||
dest_files=["res://.godot/imported/citrus_orchard_road_puresky_4k.hdr-262868e8787abf05f7044da72beefce7.bptc.ctex"]
|
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"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
|
|||||||
@@ -4,15 +4,16 @@ importer="texture"
|
|||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://3syhacswepyj"
|
uid="uid://3syhacswepyj"
|
||||||
path.bptc="res://.godot/imported/kloppenheim_06_puresky_4k.hdr-026f0eabdfce50035e2f8417ce3e0060.bptc.ctex"
|
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"
|
||||||
metadata={
|
metadata={
|
||||||
"imported_formats": ["s3tc_bptc"],
|
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||||
"vram_texture": true
|
"vram_texture": true
|
||||||
}
|
}
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://HDRI/kloppenheim_06_puresky_4k.hdr"
|
source_file="res://HDRI/kloppenheim_06_puresky_4k.hdr"
|
||||||
dest_files=["res://.godot/imported/kloppenheim_06_puresky_4k.hdr-026f0eabdfce50035e2f8417ce3e0060.bptc.ctex"]
|
dest_files=["res://.godot/imported/kloppenheim_06_puresky_4k.hdr-026f0eabdfce50035e2f8417ce3e0060.bptc.ctex", "res://.godot/imported/kloppenheim_06_puresky_4k.hdr-026f0eabdfce50035e2f8417ce3e0060.astc.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
|
|||||||
@@ -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, 1269629432) index="0" unique_id=978366810]
|
|
||||||
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=256196413]
|
|
||||||
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=988244930]
|
|
||||||
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"]
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[gd_resource type="StandardMaterial3D" format=3 uid="uid://b73nb47y8ppt"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
albedo_color = Color(0.26098922, 0.27367952, 0.27059656, 1)
|
||||||
|
metallic = 1.0
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
shader_type spatial;
|
||||||
|
|
||||||
|
void vertex() {
|
||||||
|
// Called for every vertex the material is visible on.
|
||||||
|
}
|
||||||
|
|
||||||
|
void fragment() {
|
||||||
|
// Called for every pixel the material is visible on.
|
||||||
|
}
|
||||||
|
|
||||||
|
//void light() {
|
||||||
|
// // Called for every pixel for every light affecting the material.
|
||||||
|
// // Uncomment to replace the default light processing function with this one.
|
||||||
|
//}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bme1rkldtyrhu
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[gd_resource type="StandardMaterial3D" format=3 uid="uid://isrp2y47xs5x"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
albedo_color = Color(0.49425018, 0.31969637, 0.21259204, 1)
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[gd_resource type="StandardMaterial3D" format=3 uid="uid://blpkmyikjnhbo"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
emission_enabled = true
|
||||||
|
emission = Color(0.4627451, 0.8117647, 0.9490196, 1)
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[gd_resource type="StandardMaterial3D" format=3 uid="uid://dgmk764033feq"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
emission_enabled = true
|
||||||
|
emission = Color(0.7096375, 0.3347874, 0.09943161, 1)
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
[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"]
|
|
||||||
|
|
||||||
[sub_resource type="SphereShape3D" id="SphereShape3D_chest"]
|
|
||||||
radius = 0.35
|
|
||||||
|
|
||||||
[sub_resource type="SphereShape3D" id="SphereShape3D_belly"]
|
|
||||||
radius = 0.4
|
|
||||||
|
|
||||||
[sub_resource type="SphereShape3D" id="SphereShape3D_hips"]
|
|
||||||
radius = 0.3
|
|
||||||
|
|
||||||
[node name="Player" type="CharacterBody3D" unique_id=165510044]
|
|
||||||
collision_layer = 3
|
|
||||||
script = ExtResource("1_xhfnw")
|
|
||||||
|
|
||||||
[node name="CollisionChest" type="CollisionShape3D" parent="." unique_id=447032296]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.505, -0.2)
|
|
||||||
shape = SubResource("SphereShape3D_chest")
|
|
||||||
|
|
||||||
[node name="CollisionBelly" type="CollisionShape3D" parent="." unique_id=571780801]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.505, 0.1)
|
|
||||||
shape = SubResource("SphereShape3D_belly")
|
|
||||||
|
|
||||||
[node name="CollisionHips" type="CollisionShape3D" parent="." unique_id=717874116]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.505, 0.4)
|
|
||||||
shape = SubResource("SphereShape3D_hips")
|
|
||||||
|
|
||||||
[node name="Camera3D" type="Camera3D" parent="." unique_id=1120161087]
|
|
||||||
script = ExtResource("2_camera_iso")
|
|
||||||
|
|
||||||
[node name="bull" parent="." unique_id=1386495166 instance=ExtResource("4_e80uo")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.655, 0.23)
|
|
||||||
|
|
||||||
[editable path="bull"]
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cn7fhjkro7637
|
||||||
@@ -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://Assets/Bear.glb" id="2_bearglb"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://bdbx0wwa73rhp" path="res://Assets/fx/Bear_FX.glb" id="3_bearfx"]
|
||||||
|
[ext_resource type="Material" uid="uid://blpkmyikjnhbo" path="res://Materials/M_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
|
extends CharacterBody3D
|
||||||
|
|
||||||
# WANDER → bull outside flee range, roaming freely
|
# WANDER → roam/taunt outside flee range. FLEE → bull close: back away (raycast-steered).
|
||||||
# FLEE → bull within range, matador backs away (raycast-steered around walls)
|
# BRACE → charge detected: plant + face (cite). SIDESTEP → bull commits: lateral pass (pase).
|
||||||
# BRACE → charge detected: plant feet, face the bull (cite phase)
|
# ATTACK → chase and strike. ROLL → hit but rolled: survive. THROW → wind up + hurl the spear.
|
||||||
# SIDESTEP→ bull commits: sharp lateral step to let it pass (pase phase)
|
# RAGDOLL → hit, no roll.
|
||||||
# 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
|
|
||||||
enum State { WANDER, FLEE, BRACE, SIDESTEP, ATTACK, ROLL, THROW, RAGDOLL }
|
enum State { WANDER, FLEE, BRACE, SIDESTEP, ATTACK, ROLL, THROW, RAGDOLL }
|
||||||
|
|
||||||
# The matador's high-level *want*, chosen by a weighted utility vote (see
|
# The matador's high-level *want*, chosen by a weighted utility vote (see
|
||||||
@@ -32,9 +28,12 @@ var _dodge_cd: float = 0.0
|
|||||||
var _will_dodge: bool = true
|
var _will_dodge: bool = true
|
||||||
var _attack_timer: float = 0.0
|
var _attack_timer: float = 0.0
|
||||||
var _swing_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_timer: float = 0.0
|
||||||
var _roll_dir: Vector3 = Vector3.ZERO
|
var _roll_dir: Vector3 = Vector3.ZERO
|
||||||
|
var _is_spawning: bool = false
|
||||||
var _throw_timer: float = 0.0
|
var _throw_timer: float = 0.0
|
||||||
var _throw_aim_cd: float = 0.0
|
var _throw_aim_cd: float = 0.0
|
||||||
var _throw_dir: Vector3 = Vector3.ZERO
|
var _throw_dir: Vector3 = Vector3.ZERO
|
||||||
@@ -51,7 +50,7 @@ var _drawing: bool = false
|
|||||||
var _sheathing: bool = false
|
var _sheathing: bool = false
|
||||||
var _draw_timer: float = 0.0
|
var _draw_timer: float = 0.0
|
||||||
var _draw_len: 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 _spear_node: Node3D = null
|
||||||
var _blade_hit_cd: float = 0.0
|
var _blade_hit_cd: float = 0.0
|
||||||
var _death_player: AudioStreamPlayer = null
|
var _death_player: AudioStreamPlayer = null
|
||||||
@@ -60,6 +59,7 @@ var _blood_burst: CPUParticles3D = null
|
|||||||
@onready var _mesh: Node3D = $matador2
|
@onready var _mesh: Node3D = $matador2
|
||||||
@onready var _hit_area: Area3D = $HitArea
|
@onready var _hit_area: Area3D = $HitArea
|
||||||
|
|
||||||
|
|
||||||
const _ANIM_RUN: StringName = &"Run"
|
const _ANIM_RUN: StringName = &"Run"
|
||||||
const _ANIM_IDLE: StringName = &"Taunt"
|
const _ANIM_IDLE: StringName = &"Taunt"
|
||||||
const _ANIM_ATTACK: StringName = &"Attack"
|
const _ANIM_ATTACK: StringName = &"Attack"
|
||||||
@@ -74,8 +74,9 @@ const _ANIM_TAUNTS: Array[StringName] = [&"Taunt", &"Taunt_B", &"Taunt_C"]
|
|||||||
# reparents from holster to hand at this point in the animation.
|
# reparents from holster to hand at this point in the animation.
|
||||||
const _DRAW_GRAB_AT: float = 0.55
|
const _DRAW_GRAB_AT: float = 0.55
|
||||||
|
|
||||||
const _SWORD_SCENE: PackedScene = preload("res://Assets/sword.glb")
|
# Wrapper scenes (glb + an editor-tweakable hitbox / collider) — see Sword.tscn / Spear.tscn.
|
||||||
const _SPEAR_SCENE: PackedScene = preload("res://Assets/spear.glb")
|
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
|
const _GRAVITY: float = 9.8 # arena gravity, for the spear's ballistic arc
|
||||||
|
|
||||||
@@ -92,12 +93,20 @@ const _TURN_FACE: float = 16.0 # face the bull while engaging
|
|||||||
const _TURN_SHARP: float = 24.0 # snap onto a sidestep / roll direction
|
const _TURN_SHARP: float = 24.0 # snap onto a sidestep / roll direction
|
||||||
|
|
||||||
|
|
||||||
|
const RigidSkin = preload("res://systems/render/rigid_skin.gd")
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
add_to_group(&"matador")
|
add_to_group(&"matador")
|
||||||
_skeleton = _find_skeleton(_mesh)
|
_skeleton = _find_skeleton(_mesh)
|
||||||
_anim_player = _find_anim_player(_mesh)
|
_anim_player = _find_anim_player(_mesh)
|
||||||
if _skeleton:
|
if _skeleton:
|
||||||
_sim = MatadorRagdoll.build(_skeleton)
|
_sim = MatadorRagdoll.build(_skeleton)
|
||||||
|
# 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:
|
if _anim_player:
|
||||||
for anim in [_ANIM_RUN, _ANIM_ATTACK] + _ANIM_TAUNTS:
|
for anim in [_ANIM_RUN, _ANIM_ATTACK] + _ANIM_TAUNTS:
|
||||||
_ensure_loop(anim)
|
_ensure_loop(anim)
|
||||||
@@ -112,17 +121,19 @@ func _ready() -> void:
|
|||||||
_throw_aim_cd = randf_range(2.0, 5.0)
|
_throw_aim_cd = randf_range(2.0, 5.0)
|
||||||
_hit_area.body_entered.connect(_on_body_entered)
|
_hit_area.body_entered.connect(_on_body_entered)
|
||||||
_pick_wander_target()
|
_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)
|
add_child(_blood_burst)
|
||||||
if ResourceLoader.exists("res://sounds/ole.ogg"):
|
if ResourceLoader.exists("res://sounds/ole.ogg"):
|
||||||
_ole_player = AudioStreamPlayer.new()
|
_ole_player = AudioStreamPlayer.new()
|
||||||
_ole_player.stream = load("res://sounds/ole.ogg")
|
_ole_player.stream = load("res://sounds/ole.ogg")
|
||||||
_ole_player.volume_db = 0.0
|
_ole_player.volume_db = 0.0
|
||||||
|
_ole_player.bus = &"Effects"
|
||||||
add_child(_ole_player)
|
add_child(_ole_player)
|
||||||
if ResourceLoader.exists("res://sounds/matador_death.ogg"):
|
if ResourceLoader.exists("res://sounds/matador_death.ogg"):
|
||||||
_death_player = AudioStreamPlayer.new()
|
_death_player = AudioStreamPlayer.new()
|
||||||
_death_player.stream = load("res://sounds/matador_death.ogg")
|
_death_player.stream = load("res://sounds/matador_death.ogg")
|
||||||
_death_player.volume_db = 2.0
|
_death_player.volume_db = 2.0
|
||||||
|
_death_player.bus = &"Effects"
|
||||||
add_child(_death_player)
|
add_child(_death_player)
|
||||||
|
|
||||||
|
|
||||||
@@ -143,6 +154,10 @@ func _acquire_bull() -> void:
|
|||||||
func _physics_process(delta: float) -> void:
|
func _physics_process(delta: float) -> void:
|
||||||
_acquire_bull()
|
_acquire_bull()
|
||||||
|
|
||||||
|
if _is_spawning and is_on_floor():
|
||||||
|
_is_spawning = false
|
||||||
|
collision_layer = 1
|
||||||
|
|
||||||
if DP.b("show_raycasts") and _steer_dir != Vector3.ZERO:
|
if DP.b("show_raycasts") and _steer_dir != Vector3.ZERO:
|
||||||
var o := global_position + Vector3.UP * 0.6
|
var o := global_position + Vector3.UP * 0.6
|
||||||
DebugDraw.ray(o, o + _steer_dir * _STEER_LOOKAHEAD, Color(0.3, 1.0, 0.9))
|
DebugDraw.ray(o, o + _steer_dir * _STEER_LOOKAHEAD, Color(0.3, 1.0, 0.9))
|
||||||
@@ -153,6 +168,9 @@ func _physics_process(delta: float) -> void:
|
|||||||
_dodge_cd = maxf(0.0, _dodge_cd - delta)
|
_dodge_cd = maxf(0.0, _dodge_cd - delta)
|
||||||
_blade_hit_cd = maxf(0.0, _blade_hit_cd - delta)
|
_blade_hit_cd = maxf(0.0, _blade_hit_cd - delta)
|
||||||
_throw_aim_cd = maxf(0.0, _throw_aim_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:
|
if _state != State.RAGDOLL and _bull != null:
|
||||||
_update_ai_state(delta)
|
_update_ai_state(delta)
|
||||||
|
|
||||||
@@ -170,9 +188,8 @@ func _physics_process(delta: float) -> void:
|
|||||||
_advance_draw(delta)
|
_advance_draw(delta)
|
||||||
|
|
||||||
|
|
||||||
# Utility-driven brain. The physically committed states (a brace, a pass, a roll, a
|
# Utility-driven brain. Committed states (brace / pass / roll / throw wind-up) run to completion on
|
||||||
# throw wind-up) run to completion on their own timers; from the free states the
|
# their own timers; from the free states the matador re-scores every mat_decide_interval.
|
||||||
# matador re-scores its options every mat_decide_interval and acts on the winner.
|
|
||||||
func _update_ai_state(delta: float) -> void:
|
func _update_ai_state(delta: float) -> void:
|
||||||
if _state == State.BRACE or _state == State.SIDESTEP \
|
if _state == State.BRACE or _state == State.SIDESTEP \
|
||||||
or _state == State.ROLL or _state == State.THROW:
|
or _state == State.ROLL or _state == State.THROW:
|
||||||
@@ -189,9 +206,8 @@ func _update_ai_state(delta: float) -> void:
|
|||||||
_enter_intent(intent)
|
_enter_intent(intent)
|
||||||
|
|
||||||
|
|
||||||
# Score each intent from weighted heuristics and return the winner. Weights (the
|
# Score each intent from weighted mat_w_* heuristics and return the winner; a small commit bonus on
|
||||||
# mat_w_* params) shape the personality; a small commit bonus on the current intent
|
# the current intent stops it flip-flopping between two near-tied options.
|
||||||
# stops it flip-flopping between two near-tied options.
|
|
||||||
func _choose_intent() -> Intent:
|
func _choose_intent() -> Intent:
|
||||||
var dist := global_position.distance_to(_bull.global_position)
|
var dist := global_position.distance_to(_bull.global_position)
|
||||||
var bull_flat := Vector3(_bull.velocity.x, 0.0, _bull.velocity.z)
|
var bull_flat := Vector3(_bull.velocity.x, 0.0, _bull.velocity.z)
|
||||||
@@ -212,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
|
# 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.
|
# cooldown makes attacking (and dodging outright) less attractive.
|
||||||
var dodge_ready := 1.0 if _dodge_cd <= 0.0 else 0.35
|
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) \
|
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")
|
and dist >= DP.f("mat_throw_min_dist") and dist <= DP.f("mat_throw_range")
|
||||||
|
|
||||||
@@ -232,7 +251,7 @@ func _choose_intent() -> Intent:
|
|||||||
# Press the attack; ease off a touch when a charge is barrelling in. Supporting
|
# 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.
|
# matadors barely press — they yield the pass to whoever's engaging.
|
||||||
Intent.ATTACK: DP.f("mat_w_aggression") * (0.45 + 0.55 * close) \
|
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.
|
# Sidestep / plant against an incoming charge.
|
||||||
Intent.DODGE: DP.f("mat_w_caution") * danger * (1.0 if _dodge_cd <= 0.0 else 0.0),
|
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
|
# Hurl the blade — an occasional ranged flourish from an engaging matador when
|
||||||
@@ -240,7 +259,10 @@ func _choose_intent() -> Intent:
|
|||||||
Intent.THROW: (DP.f("mat_w_throw") * (0.35 + 0.65 * (1.0 - close))) \
|
Intent.THROW: (DP.f("mat_w_throw") * (0.35 + 0.65 * (1.0 - close))) \
|
||||||
if (throw_ok and is_engager) else -1.0,
|
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: Intent = Intent.WANDER
|
||||||
var best_score := -INF
|
var best_score := -INF
|
||||||
@@ -286,9 +308,8 @@ func _enter_intent(intent: Intent) -> void:
|
|||||||
_start_throw()
|
_start_throw()
|
||||||
|
|
||||||
|
|
||||||
# How many other live matadors are closer to the bull than this one. 0 = the
|
# How many other live matadors are closer to the bull than this one (0 = the lead engager); the
|
||||||
# closest (the natural lead engager); the utility brain gives the nearest
|
# brain lets the nearest mat_engage_slots press while the rest spread out and taunt.
|
||||||
# mat_engage_slots the go-ahead to press while the rest spread out and taunt.
|
|
||||||
func _engagement_rank() -> int:
|
func _engagement_rank() -> int:
|
||||||
if _bull == null:
|
if _bull == null:
|
||||||
return 0
|
return 0
|
||||||
@@ -332,6 +353,19 @@ func _is_charge_incoming() -> bool:
|
|||||||
return bull_flat.normalized().dot(to_me.normalized()) > 0.85
|
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.
|
# Returns a wall-clear direction, recomputed at most every _STEER_INTERVAL seconds.
|
||||||
# Randomises among available clear angles so juke direction is unpredictable.
|
# Randomises among available clear angles so juke direction is unpredictable.
|
||||||
func _steer_clear(desired_dir: Vector3, delta: float) -> Vector3:
|
func _steer_clear(desired_dir: Vector3, delta: float) -> Vector3:
|
||||||
@@ -392,6 +426,19 @@ func _face_point(point: Vector3, delta: float, rate: float) -> void:
|
|||||||
func _tick_wander(delta: float) -> void:
|
func _tick_wander(delta: float) -> void:
|
||||||
# Plant the feet while drawing / sheathing instead of sliding through the clip.
|
# Plant the feet while drawing / sheathing instead of sliding through the clip.
|
||||||
if _drawing or _sheathing or _idle_timer > 0.0:
|
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)
|
_idle_timer = maxf(0.0, _idle_timer - delta)
|
||||||
_decelerate(10.0, delta)
|
_decelerate(10.0, delta)
|
||||||
# Taunt toward the bull so the posturing plays to it, not off into space.
|
# Taunt toward the bull so the posturing plays to it, not off into space.
|
||||||
@@ -516,6 +563,7 @@ func _start_attack() -> void:
|
|||||||
|
|
||||||
func _tick_attack(delta: float) -> void:
|
func _tick_attack(delta: float) -> void:
|
||||||
_attack_timer -= delta
|
_attack_timer -= delta
|
||||||
|
var was_swinging := _swing_timer > 0.0
|
||||||
_swing_timer = maxf(0.0, _swing_timer - delta)
|
_swing_timer = maxf(0.0, _swing_timer - delta)
|
||||||
# Plant the feet while drawing instead of sliding toward the bull mid-clip.
|
# Plant the feet while drawing instead of sliding toward the bull mid-clip.
|
||||||
if _drawing or _sheathing:
|
if _drawing or _sheathing:
|
||||||
@@ -528,22 +576,41 @@ func _tick_attack(delta: float) -> void:
|
|||||||
to_bull.y = 0.0
|
to_bull.y = 0.0
|
||||||
var dist := to_bull.length()
|
var dist := to_bull.length()
|
||||||
|
|
||||||
# Commit to a full swing once in range instead of flipping Run/Attack at the
|
# A finished stab enforces a recovery beat (no re-commit for mat_stab_recovery), then decides
|
||||||
# distance boundary every frame (the old foot-slide jitter). When the swing
|
# what's next: a landed hit very likely breaks into a taunt/retreat, otherwise the usual roll
|
||||||
# ends the matador re-closes if the bull slipped away, or stabs again if not.
|
# to give ground / circle / throw — anything but instantly drilling the same thrust again.
|
||||||
if _swing_timer <= 0.0 and dist <= DP.f("mat_attack_min_dist") and _sword_in_hand:
|
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)
|
_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:
|
if _swing_timer > 0.0:
|
||||||
_lunge_timer = maxf(0.0, _lunge_timer - delta)
|
_decelerate(22.0, delta)
|
||||||
if _lunge_timer > 0.0 and dist > 0.1:
|
|
||||||
_accelerate(to_bull.normalized(), DP.f("mat_lunge_speed"), delta)
|
|
||||||
else:
|
|
||||||
_decelerate(22.0, delta)
|
|
||||||
_play_anim(_ANIM_ATTACK)
|
_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:
|
else:
|
||||||
|
if _sword_blade_area != null:
|
||||||
|
_sword_blade_area.deactivate()
|
||||||
var dir := _steer_clear(to_bull.normalized(), delta)
|
var dir := _steer_clear(to_bull.normalized(), delta)
|
||||||
_accelerate(dir, DP.f("mat_attack_speed"), delta)
|
_accelerate(dir, DP.f("mat_attack_speed"), delta)
|
||||||
_play_anim(_ANIM_RUN)
|
_play_anim(_ANIM_RUN)
|
||||||
@@ -551,6 +618,50 @@ func _tick_attack(delta: float) -> void:
|
|||||||
move_and_slide()
|
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:
|
func _enter_roll(hit_dir: Vector3) -> void:
|
||||||
_state = State.ROLL
|
_state = State.ROLL
|
||||||
# Match the state to the actual clip so the roll plays through once instead of
|
# Match the state to the actual clip so the roll plays through once instead of
|
||||||
@@ -564,6 +675,22 @@ func _enter_roll(hit_dir: Vector3) -> void:
|
|||||||
_anim_player.seek(0.0, true)
|
_anim_player.seek(0.0, true)
|
||||||
|
|
||||||
|
|
||||||
|
# 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
|
||||||
|
_roll_dir = dir.normalized() if dir.length() > 0.1 else -_mesh.global_transform.basis.z
|
||||||
|
_state = State.ROLL
|
||||||
|
_roll_timer = _anim_length(_ANIM_ROLL, DP.f("mat_roll_duration"))
|
||||||
|
_is_spawning = true
|
||||||
|
collision_layer = 0
|
||||||
|
velocity.y = 4.0
|
||||||
|
if _anim_player and _anim_player.has_animation(_ANIM_ROLL):
|
||||||
|
_anim_player.play(_ANIM_ROLL)
|
||||||
|
_anim_player.seek(0.0, true)
|
||||||
|
|
||||||
|
|
||||||
func _tick_roll(delta: float) -> void:
|
func _tick_roll(delta: float) -> void:
|
||||||
_roll_timer -= delta
|
_roll_timer -= delta
|
||||||
if _roll_timer <= 0.0:
|
if _roll_timer <= 0.0:
|
||||||
@@ -738,34 +865,84 @@ func _on_body_entered(body: Node3D) -> void:
|
|||||||
if not body.is_in_group(&"player"):
|
if not body.is_in_group(&"player"):
|
||||||
return
|
return
|
||||||
var player := body as CharacterBody3D
|
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
|
return
|
||||||
var flat_vel := Vector3(player.velocity.x, 0.0, player.velocity.z)
|
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 \
|
var hit_dir := flat_vel.normalized() if flat_vel.length() > 0.5 else \
|
||||||
(global_position - player.global_position).normalized()
|
(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)
|
_enter_roll(hit_dir)
|
||||||
else:
|
else:
|
||||||
_enter_ragdoll(hit_dir, player.velocity.length())
|
_enter_ragdoll(hit_dir, bull_speed)
|
||||||
|
|
||||||
|
|
||||||
func _on_blade_hit(body: Node3D) -> void:
|
# Reach-based hit for the sidestep PASS (fast lateral sweep — the thin blade Area tunnels, so a
|
||||||
if _state != State.ATTACK and _state != State.SIDESTEP and _state != State.BRACE:
|
# distance+facing check is the reliable gore; the ATTACK stab uses the blade collider instead).
|
||||||
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.
|
|
||||||
func _try_melee_hit(reach: float) -> void:
|
func _try_melee_hit(reach: float) -> void:
|
||||||
if _blade_hit_cd > 0.0 or not _sword_in_hand or _bull == null:
|
if _blade_hit_cd > 0.0 or not _sword_in_hand or _bull == null:
|
||||||
return
|
return
|
||||||
@@ -774,19 +951,17 @@ func _try_melee_hit(reach: float) -> void:
|
|||||||
var dist := to_bull.length()
|
var dist := to_bull.length()
|
||||||
if dist > reach or dist < 0.01:
|
if dist > reach or dist < 0.01:
|
||||||
return
|
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))
|
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"))):
|
if facing.dot(to_bull / dist) < cos(deg_to_rad(DP.f("mat_stab_arc"))):
|
||||||
return
|
return
|
||||||
_blade_hit_cd = DP.f("mat_stab_cooldown")
|
_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:
|
func _enter_ragdoll(hit_dir: Vector3, bull_speed: float, up_boost: float = 0.0) -> void:
|
||||||
_state = State.RAGDOLL
|
_state = State.RAGDOLL
|
||||||
if _sword_blade_area:
|
if _sword_blade_area != null:
|
||||||
_sword_blade_area.monitoring = false
|
_sword_blade_area.deactivate()
|
||||||
_clear_spear() # a spear mid-wind-up dies with the matador, not in the air
|
_clear_spear() # a spear mid-wind-up dies with the matador, not in the air
|
||||||
killed.emit()
|
killed.emit()
|
||||||
var cleanup_timer := get_tree().create_timer(4.0)
|
var cleanup_timer := get_tree().create_timer(4.0)
|
||||||
@@ -979,10 +1154,9 @@ func _pick_wander_target() -> void:
|
|||||||
|
|
||||||
# ── Sword attachment ──────────────────────────────────────────────────────────
|
# ── Sword attachment ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
# The rig (matador_v03) carries two purpose-built bones: weapon_bone (right-hand
|
# The rig (matador_v03) carries two bones: weapon_bone (right-hand grip) and weapon_rest_bone (hip
|
||||||
# grip) and weapon_rest_bone (hip holster). The sword rides in the holster and is
|
# holster). The sword rides in the holster, carried to the hand in combat (_carry_sword); each bone
|
||||||
# carried to the hand in combat (see _carry_sword); each bone has its own DP-tunable
|
# has its own DP-tunable offset/rotation (sword_* / sword_rest_*) so the poses seat independently.
|
||||||
# local offset/rotation (sword_* / sword_rest_*) so the poses seat independently.
|
|
||||||
func _setup_sword() -> void:
|
func _setup_sword() -> void:
|
||||||
if not _skeleton:
|
if not _skeleton:
|
||||||
return
|
return
|
||||||
@@ -1015,28 +1189,15 @@ func _spawn_sword() -> void:
|
|||||||
_drawing = false
|
_drawing = false
|
||||||
_apply_sword_grip()
|
_apply_sword_grip()
|
||||||
|
|
||||||
# Blade collider along +Z (hilt at origin), tipped from the capsule's default Y.
|
# The blade hitbox (Sword.tscn, hitbox.gd) is armed only during the ATTACK thrust (see
|
||||||
# Only monitors while the sword is in hand (see _carry_sword).
|
# _tick_attack); off while merely carried, so brushing a drawn matador never gores the bull.
|
||||||
var blade_cap := CapsuleShape3D.new()
|
_sword_blade_area = _sword_node.get_node_or_null("BladeHitbox") as Hitbox
|
||||||
blade_cap.radius = 0.18
|
if _sword_blade_area != null:
|
||||||
blade_cap.height = 1.30
|
_sword_blade_area.deactivate()
|
||||||
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 sword is MELEE-only: drawn while fighting up close (ATTACK) or guarding
|
# The sword is MELEE-only: drawn for ATTACK / BRACE / SIDESTEP, holstered otherwise. Ranged throws
|
||||||
# (BRACE / SIDESTEP), holstered otherwise. Ranged throws use a spear instead
|
# use a spear (_spawn_spear_in_hand), so THROW keeps the sword holstered.
|
||||||
# (see _spawn_spear_in_hand), so THROW keeps the sword holstered.
|
|
||||||
func _wants_sword_drawn() -> bool:
|
func _wants_sword_drawn() -> bool:
|
||||||
return _state == State.ATTACK \
|
return _state == State.ATTACK \
|
||||||
or _state == State.BRACE or _state == State.SIDESTEP
|
or _state == State.BRACE or _state == State.SIDESTEP
|
||||||
@@ -1045,6 +1206,9 @@ func _wants_sword_drawn() -> bool:
|
|||||||
func _update_sword_carry() -> void:
|
func _update_sword_carry() -> void:
|
||||||
if not is_instance_valid(_sword_node) or _state == State.RAGDOLL:
|
if not is_instance_valid(_sword_node) or _state == State.RAGDOLL:
|
||||||
return
|
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 _wants_sword_drawn():
|
||||||
if not _sword_in_hand and not _drawing:
|
if not _sword_in_hand and not _drawing:
|
||||||
_begin_draw()
|
_begin_draw()
|
||||||
@@ -1124,8 +1288,9 @@ func _carry_sword(in_hand: bool) -> void:
|
|||||||
_sword_in_hand = in_hand
|
_sword_in_hand = in_hand
|
||||||
_sword_node.reparent(target, false)
|
_sword_node.reparent(target, false)
|
||||||
_apply_sword_grip()
|
_apply_sword_grip()
|
||||||
if _sword_blade_area:
|
# Carrying never arms the blade — only the ATTACK thrust does (see _tick_attack).
|
||||||
_sword_blade_area.monitoring = in_hand
|
if _sword_blade_area != null:
|
||||||
|
_sword_blade_area.deactivate()
|
||||||
|
|
||||||
|
|
||||||
func _apply_sword_grip() -> void:
|
func _apply_sword_grip() -> void:
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[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="Script" uid="uid://bvfa7xi0luysk" path="res://actors/matador/matador.gd" id="1_matgd"]
|
||||||
[ext_resource type="PackedScene" uid="uid://eu2lqjhowj4t" path="res://Assets/Matador.glb" id="2_matfbx"]
|
[ext_resource type="PackedScene" uid="uid://eu2lqjhowj4t" path="res://Assets/Matador.glb" id="2_matfbx"]
|
||||||
|
|
||||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_body"]
|
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_body"]
|
||||||
@@ -11,19 +11,18 @@ height = 1.2
|
|||||||
radius = 0.45
|
radius = 0.45
|
||||||
height = 1.4
|
height = 1.4
|
||||||
|
|
||||||
[node name="Matador" type="CharacterBody3D"]
|
[node name="Matador" type="CharacterBody3D" unique_id=1781857630]
|
||||||
script = ExtResource("1_matgd")
|
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)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.85, 0)
|
||||||
shape = SubResource("CapsuleShape3D_body")
|
shape = SubResource("CapsuleShape3D_body")
|
||||||
|
|
||||||
[node name="HitArea" type="Area3D" parent="."]
|
[node name="HitArea" type="Area3D" parent="." unique_id=745037545]
|
||||||
monitoring = true
|
|
||||||
monitorable = false
|
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)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.85, 0)
|
||||||
shape = SubResource("CapsuleShape3D_hit")
|
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
|
## 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.
|
## 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.
|
# [bone, radius] → sphere collider; [bone, radius, height] → capsule.
|
||||||
const _BONES: Array = [
|
const _BONES: Array = [
|
||||||
["matador", 0.18],
|
["matador", 0.18],
|
||||||
@@ -53,10 +55,15 @@ static func build(skeleton: Skeleton3D) -> PhysicalBoneSimulator3D:
|
|||||||
pb.bone_name = bname
|
pb.bone_name = bname
|
||||||
pb.mass = 0.4
|
pb.mass = 0.4
|
||||||
pb.linear_damp = 0.1
|
pb.linear_damp = 0.1
|
||||||
pb.collision_layer = 1
|
# The ragdoll only simulates once the matador is dead, so it lives on the CORPSE
|
||||||
pb.collision_mask = 1
|
# 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"):
|
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.joint_type = PhysicalBone3D.JOINT_TYPE_PIN
|
||||||
pb.mass = 0.15
|
pb.mass = 0.15
|
||||||
pb.linear_damp = 0.5
|
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://Assets/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,11 +10,18 @@ extends RigidBody3D
|
|||||||
# (see shed_all, which the bull's slam calls to fling them all off); other hits pin
|
# (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.
|
# 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 _DESPAWN_SEC: float = 8.0
|
||||||
const _STUCK_GROUP: StringName = &"stuck_spear"
|
const _STUCK_GROUP: StringName = &"stuck_spear"
|
||||||
# Shaft geometry in spear.glb local space: tip at +Z, butt behind the origin.
|
# Shaft geometry in spear.glb local space: tip at +Z, butt behind the origin.
|
||||||
const _TIP_Z: float = 1.915
|
const _TIP_Z: float = 1.915
|
||||||
const _EMBED_DEPTH: float = 0.45 # how far past the surface the tip buries
|
const _EMBED_DEPTH: float = 0.72 # how far past the surface the tip buries
|
||||||
|
# Bone the bull's spears ride on. The collision spheres sit outside the hide, so a
|
||||||
|
# spear stuck to the body root reads as floating; anchoring to COG also lets it bob
|
||||||
|
# and rotate with the animated body instead of only the character root.
|
||||||
|
const _COG_BONE: String = "COG"
|
||||||
|
const _ANCHOR_NAME: String = "SpearAnchor"
|
||||||
|
|
||||||
var _mesh: Node3D = null
|
var _mesh: Node3D = null
|
||||||
var _stopped: bool = false
|
var _stopped: bool = false
|
||||||
@@ -29,16 +36,25 @@ static func launch(parent: Node, mesh: Node3D, origin: Vector3, velocity: Vector
|
|||||||
var s := SpearProjectile.new()
|
var s := SpearProjectile.new()
|
||||||
s.gravity_scale = gravity
|
s.gravity_scale = gravity
|
||||||
s.collision_layer = 0
|
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.contact_monitor = true
|
||||||
s.max_contacts_reported = 6
|
s.max_contacts_reported = 6
|
||||||
parent.add_child(s)
|
parent.add_child(s)
|
||||||
# Seat the body so its local +Z (the shaft, see spear.glb) points along the throw.
|
# Seat the body so its local +Z (the shaft, see spear.glb) points along the throw.
|
||||||
s.global_transform = Transform3D(_aim_basis(velocity), origin)
|
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.reparent(s, false)
|
||||||
mesh.transform = Transform3D.IDENTITY
|
mesh.transform = Transform3D.IDENTITY
|
||||||
s._mesh = mesh
|
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
|
s.linear_velocity = velocity
|
||||||
_despawn(s)
|
_despawn(s)
|
||||||
return s
|
return s
|
||||||
@@ -53,10 +69,12 @@ static func shed_all(bull: Node) -> void:
|
|||||||
if scene == null:
|
if scene == null:
|
||||||
scene = bull
|
scene = bull
|
||||||
var center: Vector3 = (bull as Node3D).global_position
|
var center: Vector3 = (bull as Node3D).global_position
|
||||||
for child in bull.get_children():
|
# Stuck spears now live under a BoneAttachment3D deep in the skeleton, so walk the
|
||||||
if not child.is_in_group(_STUCK_GROUP):
|
# whole subtree by group rather than only the bull's direct children.
|
||||||
|
for node in bull.get_tree().get_nodes_in_group(_STUCK_GROUP):
|
||||||
|
if not (bull as Node).is_ancestor_of(node):
|
||||||
continue
|
continue
|
||||||
var mesh := child as Node3D
|
var mesh := node as Node3D
|
||||||
var gx := mesh.global_transform
|
var gx := mesh.global_transform
|
||||||
var rb := RigidBody3D.new()
|
var rb := RigidBody3D.new()
|
||||||
rb.collision_layer = 0
|
rb.collision_layer = 0
|
||||||
@@ -77,6 +95,18 @@ static func shed_all(bull: Node) -> void:
|
|||||||
_despawn(rb)
|
_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:
|
static func _shaft_collider() -> CollisionShape3D:
|
||||||
# Collider along local +Z (mesh runs z≈-0.34 … 1.92), fattened so a fast throw
|
# 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.
|
# can't tunnel past the bull's collision spheres.
|
||||||
@@ -193,9 +223,9 @@ func _embed(other: Object, contact: Vector3, flight: Vector3) -> void:
|
|||||||
var pose := Transform3D(_aim_basis(f), contact + f * (_EMBED_DEPTH - _TIP_Z))
|
var pose := Transform3D(_aim_basis(f), contact + f * (_EMBED_DEPTH - _TIP_Z))
|
||||||
var bull := other as Node
|
var bull := other as Node
|
||||||
if bull != null and bull.is_in_group(&"player"):
|
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):
|
if is_instance_valid(_mesh) and is_instance_valid(bull):
|
||||||
_mesh.reparent(bull, false)
|
_mesh.reparent(_spear_anchor(bull), false)
|
||||||
_mesh.global_transform = pose
|
_mesh.global_transform = pose
|
||||||
_mesh.add_to_group(_STUCK_GROUP)
|
_mesh.add_to_group(_STUCK_GROUP)
|
||||||
if Settings.gore:
|
if Settings.gore:
|
||||||
@@ -204,3 +234,20 @@ func _embed(other: Object, contact: Vector3, flight: Vector3) -> void:
|
|||||||
return
|
return
|
||||||
freeze = true
|
freeze = true
|
||||||
global_transform = pose
|
global_transform = pose
|
||||||
|
|
||||||
|
|
||||||
|
# Node embedded spears ride on the bull: a BoneAttachment3D pinned to the COG bone so
|
||||||
|
# they follow the animated body (bob, turn) rather than only the character root. One
|
||||||
|
# anchor is shared by every spear; falls back to the bull root if there's no skeleton.
|
||||||
|
static func _spear_anchor(bull: Node) -> Node3D:
|
||||||
|
var existing := bull.find_child(_ANCHOR_NAME, true, false)
|
||||||
|
if existing is Node3D:
|
||||||
|
return existing as Node3D
|
||||||
|
var skel := bull.find_child("Skeleton3D", true, false) as Skeleton3D
|
||||||
|
if skel == null:
|
||||||
|
return bull as Node3D
|
||||||
|
var ba := BoneAttachment3D.new()
|
||||||
|
ba.name = _ANCHOR_NAME
|
||||||
|
skel.add_child(ba)
|
||||||
|
ba.bone_name = _COG_BONE
|
||||||
|
return ba
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
[gd_scene format=3 uid="uid://c8swordwrap01"]
|
||||||
|
|
||||||
|
[ext_resource type="PackedScene" uid="uid://xyvank512l5n" path="res://Assets/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")
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
class_name IsoCamera
|
||||||
extends Camera3D
|
extends Camera3D
|
||||||
|
|
||||||
@export var height: float = 25.0
|
@export var height: float = 25.0
|
||||||
@@ -16,6 +17,11 @@ var _shake_amount: float = 0.0
|
|||||||
var _shake_decay: float = 0.0
|
var _shake_decay: float = 0.0
|
||||||
var _user_fov: float = 0.0
|
var _user_fov: float = 0.0
|
||||||
|
|
||||||
|
# Per-level framing multiplier: 1.0 = the designer's base shot, >1 dollies the camera
|
||||||
|
# back for a wider view (the lobby uses this). Set by game_shell when a level loads;
|
||||||
|
# kept separate from the scroll-wheel FOV zoom so the two don't fight.
|
||||||
|
var _framing_scale: float = 1.0
|
||||||
|
|
||||||
# Trailing-camera state: _focus is the smoothed point the camera frames (it lags
|
# Trailing-camera state: _focus is the smoothed point the camera frames (it lags
|
||||||
# the bull), _drag_offset is the velocity-driven trail that slides it behind.
|
# the bull), _drag_offset is the velocity-driven trail that slides it behind.
|
||||||
var _focus: Vector3 = Vector3.ZERO
|
var _focus: Vector3 = Vector3.ZERO
|
||||||
@@ -42,15 +48,30 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||||||
_user_fov = clamp(_user_fov * 1.1, fov_min, fov_max)
|
_user_fov = clamp(_user_fov * 1.1, fov_min, fov_max)
|
||||||
|
|
||||||
|
|
||||||
|
# Pinch-to-zoom hook for the touch UI. `ratio` is the two-finger spread this frame over
|
||||||
|
# last frame: >1 (fingers parting) narrows the FOV to zoom in, <1 widens it to pull back,
|
||||||
|
# mirroring the scroll-wheel zoom_in/zoom_out feel.
|
||||||
|
func adjust_zoom(ratio: float) -> void:
|
||||||
|
if ratio <= 0.0:
|
||||||
|
return
|
||||||
|
_user_fov = clampf(_user_fov / ratio, fov_min, fov_max)
|
||||||
|
|
||||||
|
|
||||||
|
## Dolly the camera in/out for the level being loaded. 1.0 is the base framing; >1 pulls
|
||||||
|
## back for a wider shot (the lobby hub uses this). Independent of the scroll-wheel zoom.
|
||||||
|
func set_framing_scale(scale: float) -> void:
|
||||||
|
_framing_scale = maxf(scale, 0.1)
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
fov = lerpf(fov, _user_fov, delta * 8.0)
|
fov = lerpf(fov, _user_fov, delta * 8.0)
|
||||||
|
|
||||||
var angle_rad := deg_to_rad(horizontal_angle_deg)
|
var angle_rad := deg_to_rad(horizontal_angle_deg)
|
||||||
|
|
||||||
var offset := Vector3(
|
var offset := Vector3(
|
||||||
sin(angle_rad) * horizontal_distance,
|
sin(angle_rad) * horizontal_distance * _framing_scale,
|
||||||
height,
|
height * _framing_scale,
|
||||||
cos(angle_rad) * horizontal_distance
|
cos(angle_rad) * horizontal_distance * _framing_scale
|
||||||
)
|
)
|
||||||
|
|
||||||
var target := get_parent() as Node3D
|
var target := get_parent() as Node3D
|
||||||
@@ -14,7 +14,9 @@ const _ANIM_IDLE: StringName = &"Armature|IDLE"
|
|||||||
const _ANIM_KICK: StringName = &"Armature|KICK"
|
const _ANIM_KICK: StringName = &"Armature|KICK"
|
||||||
const _ANIM_SLAM: StringName = &"Armature|SLAM"
|
const _ANIM_SLAM: StringName = &"Armature|SLAM"
|
||||||
const _ANIM_DASH: StringName = &"Armature|DASH"
|
const _ANIM_DASH: StringName = &"Armature|DASH"
|
||||||
|
const _ANIM_WALK: StringName = &"Armature|WALK"
|
||||||
const _ANIM_ROLL: StringName = &"Armature|ROLL"
|
const _ANIM_ROLL: StringName = &"Armature|ROLL"
|
||||||
|
const _ANIM_DEATH: StringName = &"Armature|DEATH"
|
||||||
|
|
||||||
@onready var camera_pivot: Node3D = $Camera3D
|
@onready var camera_pivot: Node3D = $Camera3D
|
||||||
@onready var cube_guy: Node3D = $bull
|
@onready var cube_guy: Node3D = $bull
|
||||||
@@ -26,6 +28,11 @@ var _was_on_floor: bool = false
|
|||||||
var _pre_slide_vel_y: float = 0.0
|
var _pre_slide_vel_y: float = 0.0
|
||||||
|
|
||||||
var _slam_ring_mesh: ArrayMesh = null
|
var _slam_ring_mesh: ArrayMesh = null
|
||||||
|
# Attack-FX resources are identical every swing, so build them once and reuse. Recreating a fresh
|
||||||
|
# StandardMaterial3D / SphereMesh per attack makes a mobile WebGL driver recompile the shader and
|
||||||
|
# re-upload the mesh each time — a main-thread hitch "when attacking" that never shows on desktop.
|
||||||
|
var _unshaded_mat: StandardMaterial3D = null
|
||||||
|
var _particle_sphere_cache: Dictionary = {} # "radius:seg:ring" -> SphereMesh (shared)
|
||||||
|
|
||||||
# Ability system — kick=0, dash=1, slam=2, roll=3
|
# Ability system — kick=0, dash=1, slam=2, roll=3
|
||||||
var ability_cd: Array[float] = [0.0, 0.0, 0.0, 0.0]
|
var ability_cd: Array[float] = [0.0, 0.0, 0.0, 0.0]
|
||||||
@@ -65,6 +72,9 @@ signal health_changed(current: int, max_hp: int)
|
|||||||
signal hit_taken(cause: String)
|
signal hit_taken(cause: String)
|
||||||
var _max_hp: int = 10
|
var _max_hp: int = 10
|
||||||
var _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 _hit_iframes: float = 0.0
|
||||||
|
|
||||||
var _huff_player: AudioStreamPlayer = null
|
var _huff_player: AudioStreamPlayer = null
|
||||||
@@ -77,10 +87,24 @@ var _walk_dust_ramp: Gradient = null
|
|||||||
var _charge_dust_ramp: Gradient = null
|
var _charge_dust_ramp: Gradient = null
|
||||||
|
|
||||||
|
|
||||||
|
const RigidSkin = preload("res://systems/render/rigid_skin.gd")
|
||||||
|
|
||||||
|
var _hit_flash_tween: Tween
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
add_to_group(&"player")
|
add_to_group(&"player")
|
||||||
|
# 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")))
|
_max_hp = maxi(1, int(DP.f("bull_max_hp")))
|
||||||
_hp = _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
|
_cube_scale = cube_guy.scale
|
||||||
_setup_hoof_dust()
|
_setup_hoof_dust()
|
||||||
_setup_legs()
|
_setup_legs()
|
||||||
@@ -94,22 +118,22 @@ func _ready() -> void:
|
|||||||
|
|
||||||
func _setup_legs() -> void:
|
func _setup_legs() -> void:
|
||||||
_legs = Node.new()
|
_legs = Node.new()
|
||||||
_legs.set_script(load("res://bull_legs.gd"))
|
_legs.set_script(load("res://actors/player/bull_legs.gd"))
|
||||||
add_child(_legs)
|
add_child(_legs)
|
||||||
_legs.call(&"setup", self, cube_guy)
|
_legs.call(&"setup", self, cube_guy)
|
||||||
|
|
||||||
|
|
||||||
func _setup_hoof_dust() -> void:
|
func _setup_hoof_dust() -> void:
|
||||||
var sphere := _particle_sphere(0.10, 4, 2, _unshaded_material())
|
var sphere := _particle_sphere(0.07, 4, 2, _unshaded_material())
|
||||||
|
|
||||||
_walk_dust_ramp = Gradient.new()
|
_walk_dust_ramp = Gradient.new()
|
||||||
_walk_dust_ramp.set_color(0, Color(0.80, 0.69, 0.46, 1.0))
|
_walk_dust_ramp.set_color(0, Color(0.94, 0.90, 0.83, 1.0))
|
||||||
_walk_dust_ramp.set_color(1, Color(0.80, 0.69, 0.46, 0.0))
|
_walk_dust_ramp.set_color(1, Color(0.94, 0.90, 0.83, 0.0))
|
||||||
|
|
||||||
# Light blue dust while charging
|
# White-beige dust while running
|
||||||
_charge_dust_ramp = Gradient.new()
|
_charge_dust_ramp = Gradient.new()
|
||||||
_charge_dust_ramp.set_color(0, Color(0.20, 0.40, 0.85, 1.0))
|
_charge_dust_ramp.set_color(0, Color(0.94, 0.90, 0.83, 1.0))
|
||||||
_charge_dust_ramp.set_color(1, Color(0.20, 0.40, 0.85, 0.0))
|
_charge_dust_ramp.set_color(1, Color(0.94, 0.90, 0.83, 0.0))
|
||||||
|
|
||||||
var scale_curve := Curve.new()
|
var scale_curve := Curve.new()
|
||||||
scale_curve.add_point(Vector2(0.0, 0.2))
|
scale_curve.add_point(Vector2(0.0, 0.2))
|
||||||
@@ -213,6 +237,7 @@ func _setup_audio() -> void:
|
|||||||
_huff_player = AudioStreamPlayer.new()
|
_huff_player = AudioStreamPlayer.new()
|
||||||
_huff_player.stream = load("res://sounds/bull_huff.ogg")
|
_huff_player.stream = load("res://sounds/bull_huff.ogg")
|
||||||
_huff_player.volume_db = 0.0
|
_huff_player.volume_db = 0.0
|
||||||
|
_huff_player.bus = &"Effects"
|
||||||
add_child(_huff_player)
|
add_child(_huff_player)
|
||||||
if ResourceLoader.exists("res://sounds/crowd_ambient.ogg"):
|
if ResourceLoader.exists("res://sounds/crowd_ambient.ogg"):
|
||||||
_crowd_player = AudioStreamPlayer.new()
|
_crowd_player = AudioStreamPlayer.new()
|
||||||
@@ -222,6 +247,7 @@ func _setup_audio() -> void:
|
|||||||
_crowd_player.stream = s
|
_crowd_player.stream = s
|
||||||
_crowd_player.volume_db = -14.0
|
_crowd_player.volume_db = -14.0
|
||||||
_crowd_player.autoplay = true
|
_crowd_player.autoplay = true
|
||||||
|
_crowd_player.bus = &"Ambient"
|
||||||
add_child(_crowd_player)
|
add_child(_crowd_player)
|
||||||
|
|
||||||
|
|
||||||
@@ -251,8 +277,8 @@ func _set_dust_state(new_state: DustState) -> void:
|
|||||||
p.initial_velocity_min = DP.f("charge_vel_min")
|
p.initial_velocity_min = DP.f("charge_vel_min")
|
||||||
p.initial_velocity_max = DP.f("charge_vel_max")
|
p.initial_velocity_max = DP.f("charge_vel_max")
|
||||||
p.emitting = true
|
p.emitting = true
|
||||||
if _charge_trail_emitter:
|
# Blue energy trail belongs to the roll/dash ability only, not to fast WASD
|
||||||
_charge_trail_emitter.emitting = _dust_state == DustState.CHARGE
|
# running — driven explicitly in the roll loop / _end_roll, not by dust state.
|
||||||
|
|
||||||
|
|
||||||
func _on_dp_changed(_key: String, _val: Variant) -> void:
|
func _on_dp_changed(_key: String, _val: Variant) -> void:
|
||||||
@@ -294,7 +320,7 @@ func _play_idle() -> void:
|
|||||||
return
|
return
|
||||||
_bull_anim.speed_scale = 1.0
|
_bull_anim.speed_scale = 1.0
|
||||||
if _bull_anim.has_animation(_ANIM_IDLE):
|
if _bull_anim.has_animation(_ANIM_IDLE):
|
||||||
_bull_anim.play(_ANIM_IDLE, 0.2)
|
_bull_anim.play(_ANIM_IDLE, 0.05)
|
||||||
|
|
||||||
|
|
||||||
# Drive the WASD locomotion clip: the looping DASH doubles as the run cycle while a
|
# Drive the WASD locomotion clip: the looping DASH doubles as the run cycle while a
|
||||||
@@ -306,9 +332,11 @@ func _update_locomotion_anim(moving: bool) -> void:
|
|||||||
return
|
return
|
||||||
_locomotion_moving = moving
|
_locomotion_moving = moving
|
||||||
if moving:
|
if moving:
|
||||||
if _bull_anim.has_animation(_ANIM_DASH):
|
if _bull_anim.has_animation(_ANIM_WALK):
|
||||||
_bull_anim.speed_scale = 1.0
|
_bull_anim.speed_scale = 2.0
|
||||||
_bull_anim.play(_ANIM_DASH, 0.25)
|
_bull_anim.play(_ANIM_WALK, 1)
|
||||||
|
var anim = _bull_anim.get_animation(_ANIM_WALK)
|
||||||
|
anim.loop_mode = Animation.LOOP_LINEAR
|
||||||
else:
|
else:
|
||||||
_play_idle()
|
_play_idle()
|
||||||
|
|
||||||
@@ -490,10 +518,12 @@ func _tick_roll(delta: float, _steer: Vector3) -> void:
|
|||||||
|
|
||||||
_set_dust_state(DustState.CHARGE)
|
_set_dust_state(DustState.CHARGE)
|
||||||
if _charge_trail_emitter:
|
if _charge_trail_emitter:
|
||||||
|
_charge_trail_emitter.emitting = true
|
||||||
_charge_trail_emitter.direction = (-_roll_dir + Vector3(0.0, 0.25, 0.0)).normalized()
|
_charge_trail_emitter.direction = (-_roll_dir + Vector3(0.0, 0.25, 0.0)).normalized()
|
||||||
|
|
||||||
move_and_slide()
|
move_and_slide()
|
||||||
_was_on_floor = is_on_floor()
|
_was_on_floor = is_on_floor()
|
||||||
|
_smash_barrels_in_radius(DP.f("roll_hit_radius"))
|
||||||
_roll_try_pop(speed)
|
_roll_try_pop(speed)
|
||||||
|
|
||||||
|
|
||||||
@@ -532,22 +562,37 @@ func _roll_try_pop(speed: float) -> bool:
|
|||||||
return false
|
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:
|
func _roll_pick_target() -> Node3D:
|
||||||
var best: Node3D = null
|
var best: Node3D = null
|
||||||
var best_d := INF
|
var best_d := INF
|
||||||
for mat: Node in get_tree().get_nodes_in_group(&"matador"):
|
for cand: Node3D in _roll_targets():
|
||||||
if _roll_hit.has(mat) or not _mat_active(mat):
|
var d: float = cand.global_position.distance_squared_to(global_position)
|
||||||
continue
|
|
||||||
var d: float = (mat as Node3D).global_position.distance_squared_to(global_position)
|
|
||||||
if d < best_d:
|
if d < best_d:
|
||||||
best_d = d
|
best_d = d
|
||||||
best = mat as Node3D
|
best = cand
|
||||||
return best
|
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:
|
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
|
# A matador still in play — valid, not ragdolled/consumed. Ragdolled bodies report
|
||||||
@@ -556,8 +601,39 @@ func _mat_active(mat: Node) -> bool:
|
|||||||
return is_instance_valid(mat) and mat.has_method(&"is_active") and mat.call(&"is_active")
|
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
|
# A lobby barrel still standing — valid and not already queued for removal by an earlier hit.
|
||||||
# the normal locomotion path next frame.
|
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:
|
func _end_roll() -> void:
|
||||||
_ability_active = false
|
_ability_active = false
|
||||||
_active_ability = -1
|
_active_ability = -1
|
||||||
@@ -571,6 +647,8 @@ func _end_roll() -> void:
|
|||||||
_legs.set(&"external_rotation", false)
|
_legs.set(&"external_rotation", false)
|
||||||
if _roll_spark_emitter:
|
if _roll_spark_emitter:
|
||||||
_roll_spark_emitter.emitting = false
|
_roll_spark_emitter.emitting = false
|
||||||
|
if _charge_trail_emitter:
|
||||||
|
_charge_trail_emitter.emitting = false
|
||||||
velocity.x *= 0.15
|
velocity.x *= 0.15
|
||||||
velocity.z *= 0.15
|
velocity.z *= 0.15
|
||||||
_play_idle()
|
_play_idle()
|
||||||
@@ -607,23 +685,33 @@ func _hit_matadors_radius(range_m: float, strength: float, up_boost: float = 0.0
|
|||||||
# All FX in this script use unshaded, vertex-coloured, alpha-blended particles;
|
# All FX in this script use unshaded, vertex-coloured, alpha-blended particles;
|
||||||
# these three helpers remove the boilerplate each spawn function used to repeat.
|
# these three helpers remove the boilerplate each spawn function used to repeat.
|
||||||
|
|
||||||
|
# One shared unshaded material for every particle FX (per-particle colour comes from the
|
||||||
|
# CPUParticles3D colour_ramp via vertex colour, not the material) — built once so the mobile
|
||||||
|
# driver compiles this shader a single time instead of on every attack.
|
||||||
func _unshaded_material() -> StandardMaterial3D:
|
func _unshaded_material() -> StandardMaterial3D:
|
||||||
var mat := StandardMaterial3D.new()
|
if _unshaded_mat == null:
|
||||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
_unshaded_mat = StandardMaterial3D.new()
|
||||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
_unshaded_mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||||
mat.vertex_color_use_as_albedo = true
|
_unshaded_mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||||
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
_unshaded_mat.vertex_color_use_as_albedo = true
|
||||||
return mat
|
_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 sphere sized for a particle mesh (height is always the diameter). Cached per size so
|
||||||
|
# repeat attacks reuse the same mesh (one GPU upload) instead of generating + uploading a new one.
|
||||||
func _particle_sphere(radius: float, segments: int, rings: int, mat: Material) -> SphereMesh:
|
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()
|
var sphere := SphereMesh.new()
|
||||||
sphere.radius = radius
|
sphere.radius = radius
|
||||||
sphere.height = radius * 2.0
|
sphere.height = radius * 2.0
|
||||||
sphere.radial_segments = segments
|
sphere.radial_segments = segments
|
||||||
sphere.rings = rings
|
sphere.rings = rings
|
||||||
sphere.material = mat
|
sphere.material = mat
|
||||||
|
_particle_sphere_cache[key] = sphere
|
||||||
return sphere
|
return sphere
|
||||||
|
|
||||||
|
|
||||||
@@ -1000,13 +1088,15 @@ func _physics_process(delta: float) -> void:
|
|||||||
cube_guy.rotation.y = lerp_angle(
|
cube_guy.rotation.y = lerp_angle(
|
||||||
cube_guy.rotation.y, target_angle, delta * DP.f("visual_turn_speed"))
|
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)
|
_update_locomotion_anim(on_floor and flat_speed > 1.0)
|
||||||
|
|
||||||
_pre_slide_vel_y = velocity.y
|
_pre_slide_vel_y = velocity.y
|
||||||
var pre_wall_vel := Vector3(velocity.x, 0.0, velocity.z)
|
var pre_wall_vel := Vector3(velocity.x, 0.0, velocity.z)
|
||||||
move_and_slide()
|
move_and_slide()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ── Wall bounce — no sliding, guaranteed exit ────────────────────────────
|
# ── Wall bounce — no sliding, guaranteed exit ────────────────────────────
|
||||||
# pre_wall_vel is captured before move_and_slide strips the into-wall component,
|
# 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.
|
# so into_spd correctly reflects how hard the bull hit the wall even when stationary.
|
||||||
@@ -1022,11 +1112,23 @@ func _physics_process(delta: float) -> void:
|
|||||||
velocity.z = flat_n.z * exit_spd
|
velocity.z = flat_n.z * exit_spd
|
||||||
pre_wall_vel = Vector3(velocity.x, 0.0, velocity.z)
|
pre_wall_vel = Vector3(velocity.x, 0.0, velocity.z)
|
||||||
|
|
||||||
# ── Bounce on landing ─────────────────────────────────────────────────────
|
# --- ADD THIS BREAKABLE BARREL DETECTION CODE ---
|
||||||
|
var collision = get_slide_collision(i)
|
||||||
|
var collider = collision.get_collider()
|
||||||
|
|
||||||
|
# Check if the object we hit has the "destroy_barrel" function
|
||||||
|
if collider and collider.has_method("destroy_barrel"):
|
||||||
|
# Optional: Only break if moving fast (e.g., sprinting or dashing)
|
||||||
|
# if velocity.length() > 5.0:
|
||||||
|
collider.destroy_barrel()
|
||||||
|
|
||||||
|
# ── Landing. Don't bounce ─────────────────────────────────────────────────────
|
||||||
if is_on_floor() and not _was_on_floor:
|
if is_on_floor() and not _was_on_floor:
|
||||||
var impact := -_pre_slide_vel_y
|
velocity.y = 0.0
|
||||||
if impact > DP.f("bounce_min_impact") and not _slam_pending:
|
#Bouncing landing below
|
||||||
velocity.y = impact * DP.f("bounce_restitution")
|
#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 ──────────────────────────────────────────────────────────
|
# ── Slam landing ──────────────────────────────────────────────────────────
|
||||||
if _slam_pending and is_on_floor() and not _was_on_floor:
|
if _slam_pending and is_on_floor() and not _was_on_floor:
|
||||||
@@ -1039,6 +1141,7 @@ func _physics_process(delta: float) -> void:
|
|||||||
_play_idle()
|
_play_idle()
|
||||||
_spawn_slam_fx()
|
_spawn_slam_fx()
|
||||||
_hit_matadors_radius(DP.f("slam_range"), DP.f("slam_strength"), DP.f("slam_launch_up"))
|
_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 ──────────────────────────────────────────────────────────────────
|
# ── Dust ──────────────────────────────────────────────────────────────────
|
||||||
if flat_speed > DP.f("dust_charge_spd") and on_floor:
|
if flat_speed > DP.f("dust_charge_spd") and on_floor:
|
||||||
@@ -1065,6 +1168,22 @@ func get_max_hp() -> int:
|
|||||||
return _max_hp
|
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
|
# 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
|
# 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,
|
# death notice. I-frames absorb a flurry so several blades in one pass drop one pip,
|
||||||
@@ -1073,10 +1192,15 @@ func take_sword_hit(cause: String = "", hit_pos: Vector3 = Vector3.ZERO) -> void
|
|||||||
if _dead or _hit_iframes > 0.0:
|
if _dead or _hit_iframes > 0.0:
|
||||||
return
|
return
|
||||||
_hp -= 1
|
_hp -= 1
|
||||||
|
_hit_flash()
|
||||||
_hit_iframes = DP.f("bull_hit_iframes")
|
_hit_iframes = DP.f("bull_hit_iframes")
|
||||||
health_changed.emit(_hp, _max_hp)
|
health_changed.emit(_hp, _max_hp)
|
||||||
hit_taken.emit(cause)
|
hit_taken.emit(cause)
|
||||||
camera_pivot.call(&"trigger_hit", 0.7)
|
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
|
# 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.
|
# bull). Melee gores pass no position and skip the spurt; gore can be turned off.
|
||||||
if cause == "thrown" and Settings.gore:
|
if cause == "thrown" and Settings.gore:
|
||||||
@@ -1085,10 +1209,28 @@ func take_sword_hit(cause: String = "", hit_pos: Vector3 = Vector3.ZERO) -> void
|
|||||||
_huff_player.pitch_scale = randf_range(0.7, 0.9)
|
_huff_player.pitch_scale = randf_range(0.7, 0.9)
|
||||||
_huff_player.play()
|
_huff_player.play()
|
||||||
if _hp <= 0:
|
if _hp <= 0:
|
||||||
|
if _bull_anim.current_animation != _ANIM_DEATH:
|
||||||
|
_bull_anim.speed_scale = 0.8
|
||||||
|
_bull_anim.play(_ANIM_DEATH)
|
||||||
_dead = true
|
_dead = true
|
||||||
died.emit(cause)
|
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
|
# 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
|
# 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.
|
# hide) with a touch of lift before gravity drags it down.
|
||||||
@@ -1130,3 +1272,36 @@ func _spawn_blood(hit_pos: Vector3) -> void:
|
|||||||
p.global_position = hit_pos
|
p.global_position = hit_pos
|
||||||
p.restart()
|
p.restart()
|
||||||
_free_after(p, 1.5)
|
_free_after(p, 1.5)
|
||||||
|
|
||||||
|
|
||||||
|
#hit flashing
|
||||||
|
func _hit_flash() -> void:
|
||||||
|
var meshes := find_children("*", "MeshInstance3D", true, false)
|
||||||
|
|
||||||
|
for node in meshes:
|
||||||
|
var mesh := node as MeshInstance3D
|
||||||
|
if mesh:
|
||||||
|
mesh.material_overlay = _make_hit_overlay()
|
||||||
|
|
||||||
|
if _hit_flash_tween:
|
||||||
|
_hit_flash_tween.kill()
|
||||||
|
|
||||||
|
_hit_flash_tween = create_tween()
|
||||||
|
_hit_flash_tween.tween_interval(0.08)
|
||||||
|
_hit_flash_tween.tween_callback(_clear_hit_flash)
|
||||||
|
|
||||||
|
|
||||||
|
func _make_hit_overlay() -> StandardMaterial3D:
|
||||||
|
var mat := StandardMaterial3D.new()
|
||||||
|
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||||
|
mat.albedo_color = Color(1.0, 0.03, 0.03, 1.0)
|
||||||
|
return mat
|
||||||
|
|
||||||
|
|
||||||
|
func _clear_hit_flash() -> void:
|
||||||
|
var meshes := find_children("*", "MeshInstance3D", true, false)
|
||||||
|
|
||||||
|
for node in meshes:
|
||||||
|
var mesh := node as MeshInstance3D
|
||||||
|
if mesh:
|
||||||
|
mesh.material_overlay = null
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
[gd_scene format=3 uid="uid://crtm0xtiql3r8"]
|
||||||
|
|
||||||
|
[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://Assets/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 = 2
|
||||||
|
collision_mask = 9
|
||||||
|
script = ExtResource("1_xhfnw")
|
||||||
|
|
||||||
|
[node name="CollisionBull" type="CollisionShape3D" parent="." unique_id=571780801]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.29183745, 0.3197594)
|
||||||
|
shape = SubResource("CapsuleShape3D_kne1u")
|
||||||
|
|
||||||
|
[node name="Camera3D" type="Camera3D" parent="." unique_id=1120161087]
|
||||||
|
script = ExtResource("2_camera_iso")
|
||||||
|
|
||||||
|
[node name="bull" parent="." unique_id=1386495166 instance=ExtResource("4_e80uo")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.655, 0.23)
|
||||||
|
|
||||||
|
[node name="Skeleton3D" parent="bull/Armature" parent_id_path=PackedInt32Array(1386495166, 1942410469) index="0" unique_id=550138996]
|
||||||
|
bones/0/position = Vector3(0, 0, 0.009963839)
|
||||||
|
bones/0/rotation = Quaternion(0.7071068, 0, 0, 0.7071067)
|
||||||
|
bones/0/scale = Vector3(1, 1, 1)
|
||||||
|
bones/1/position = Vector3(0.003526625, -0.0043296535, 0.0040827994)
|
||||||
|
bones/1/rotation = Quaternion(-0.00038647873, 0.06909673, 0.9868542, -0.14609687)
|
||||||
|
bones/2/rotation = Quaternion(-0.06929122, 3.2175163e-07, -0.112670615, 0.99121344)
|
||||||
|
bones/4/position = Vector3(0.0045358227, -0.002137695, -0.004238624)
|
||||||
|
bones/4/rotation = Quaternion(-0.24948503, 0.19590352, 0.9416978, -0.11217985)
|
||||||
|
bones/4/scale = Vector3(1.0000001, 0.9999999, 1.0000001)
|
||||||
|
bones/5/rotation = Quaternion(-0.58943665, -0.3660725, -0.22564478, 0.683842)
|
||||||
|
bones/5/scale = Vector3(1, 1.0000001, 0.99999994)
|
||||||
|
bones/6/rotation = Quaternion(0.52910197, 0.15997827, 0.120245, 0.8246207)
|
||||||
|
bones/6/scale = Vector3(1, 1, 1.0000001)
|
||||||
|
bones/17/position = Vector3(0, 0.0009373814, 0.007552617)
|
||||||
|
bones/17/rotation = Quaternion(-9.4156974e-08, 0.7071068, 0.7071067, -9.4156974e-08)
|
||||||
|
bones/19/position = Vector3(-0.003526625, -0.0043296535, 0.0040827994)
|
||||||
|
bones/19/rotation = Quaternion(0.00038647873, 0.06909673, 0.9868542, 0.14609687)
|
||||||
|
bones/20/rotation = Quaternion(-0.06929122, -3.2227587e-07, 0.11267061, 0.99121344)
|
||||||
|
bones/22/position = Vector3(-0.0045358227, -0.002137695, -0.004238624)
|
||||||
|
bones/22/rotation = Quaternion(0.24948503, 0.19590352, 0.9416978, 0.11217985)
|
||||||
|
bones/23/rotation = Quaternion(-0.58943665, 0.3660725, 0.22564478, 0.683842)
|
||||||
|
bones/23/scale = Vector3(1, 1.0000001, 1)
|
||||||
|
bones/24/rotation = Quaternion(0.529102, -0.15997836, -0.120245025, 0.82462066)
|
||||||
|
bones/24/scale = Vector3(1, 1.0000001, 1)
|
||||||
|
|
||||||
|
[editable path="bull"]
|
||||||
@@ -8,5 +8,5 @@ script="rider-plugin.gd"
|
|||||||
|
|
||||||
[presets]
|
[presets]
|
||||||
|
|
||||||
active="off"
|
active="on"
|
||||||
presets="presets.json"
|
presets="presets.json"
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
uid://sbf260u32upu
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
extends Camera3D
|
|
||||||
|
|
||||||
|
|
||||||
@export var spring_arm: Node3D
|
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
|
||||||
var lerp_power: float = DP.f("cam_lerp_power")
|
|
||||||
position = position.lerp(spring_arm.position, delta * lerp_power)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://b720l6m0ehrj3
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
extends Node3D
|
|
||||||
|
|
||||||
@onready var spring_arm: SpringArm3D = $SpringArm3D
|
|
||||||
var _desired_length: float = 7.0
|
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
|
||||||
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
|
|
||||||
_desired_length = DP.f("cam_spring_length")
|
|
||||||
spring_arm.spring_length = _desired_length
|
|
||||||
|
|
||||||
|
|
||||||
func _unhandled_input(event: InputEvent) -> void:
|
|
||||||
var sensitivity: float = DP.f("cam_sensitivity")
|
|
||||||
var min_vert: float = deg_to_rad(DP.f("cam_min_vert_angle"))
|
|
||||||
var max_vert: float = deg_to_rad(DP.f("cam_max_vert_angle"))
|
|
||||||
var min_zoom: float = DP.f("cam_min_zoom")
|
|
||||||
var max_zoom: float = DP.f("cam_max_zoom")
|
|
||||||
|
|
||||||
if event is InputEventMouseMotion:
|
|
||||||
rotation.y -= event.relative.x * sensitivity
|
|
||||||
rotation.y = wrapf(rotation.y, 0.0, TAU)
|
|
||||||
rotation.x -= event.relative.y * sensitivity
|
|
||||||
rotation.x = clamp(rotation.x, min_vert, max_vert)
|
|
||||||
|
|
||||||
if event.is_action_pressed("zoom_in"):
|
|
||||||
_desired_length = clamp(_desired_length / 1.2, min_zoom, max_zoom)
|
|
||||||
if event.is_action_pressed("zoom_out"):
|
|
||||||
_desired_length = clamp(_desired_length * 1.2, min_zoom, max_zoom)
|
|
||||||
|
|
||||||
if event.is_action_pressed("toggle_mouse_capture"):
|
|
||||||
if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
|
|
||||||
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
|
||||||
else:
|
|
||||||
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
|
|
||||||
|
|
||||||
|
|
||||||
func _process(_delta: float) -> void:
|
|
||||||
spring_arm.spring_length = _desired_length
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://b4td4k8yo5y78
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
extends Node
|
|
||||||
|
|
||||||
## Global keybinding manager (autoload `Controls`).
|
|
||||||
## Loads saved keyboard bindings on startup and applies them to InputMap so
|
|
||||||
## rebinds made in the intro menu persist into the game scene. Only the
|
|
||||||
## keyboard event of each action is overridden; joypad/mouse events are kept.
|
|
||||||
|
|
||||||
const SAVE_PATH := "user://controls.cfg"
|
|
||||||
const SECTION := "keybinds"
|
|
||||||
|
|
||||||
## Actions the options menu lets the player rebind (keyboard only).
|
|
||||||
const REBINDABLE_ACTIONS: PackedStringArray = [
|
|
||||||
&"move_forward",
|
|
||||||
&"move_back",
|
|
||||||
&"move_left",
|
|
||||||
&"move_right",
|
|
||||||
&"ability_kick",
|
|
||||||
&"ability_dash",
|
|
||||||
&"ability_slam",
|
|
||||||
&"ability_roll",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
|
||||||
load_saved()
|
|
||||||
|
|
||||||
|
|
||||||
## Whether to drive the game with the on-screen touch UI (joystick + buttons)
|
|
||||||
## instead of keyboard hints. DisplayServer.is_touchscreen_available() can't be
|
|
||||||
## used here: `emulate_touch_from_mouse` (on for desktop testing) makes it report
|
|
||||||
## true on every desktop. Gate on the actual runtime platform instead — real
|
|
||||||
## mobile/native and mobile web browsers — plus the DP force flag for testing.
|
|
||||||
func use_touch_ui() -> bool:
|
|
||||||
if DP.b("force_touch_controls"):
|
|
||||||
return true
|
|
||||||
return (
|
|
||||||
OS.has_feature("mobile")
|
|
||||||
or OS.has_feature("web_android")
|
|
||||||
or OS.has_feature("web_ios")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
## Returns the first keyboard event bound to an action, or null.
|
|
||||||
func get_key_event(action: StringName) -> InputEventKey:
|
|
||||||
for event: InputEvent in InputMap.action_get_events(action):
|
|
||||||
if event is InputEventKey:
|
|
||||||
return event
|
|
||||||
return null
|
|
||||||
|
|
||||||
|
|
||||||
## Human-readable label for the action's current key (e.g. "W", "Space").
|
|
||||||
func get_key_label(action: StringName) -> String:
|
|
||||||
var event := get_key_event(action)
|
|
||||||
if event == null:
|
|
||||||
return "—"
|
|
||||||
return event.as_text_physical_keycode()
|
|
||||||
|
|
||||||
|
|
||||||
## Replaces the primary keyboard event of `action` with `event`, keeping
|
|
||||||
## every other event (e.g. arrow-key alternates, joypad) in its place.
|
|
||||||
func rebind(action: StringName, event: InputEventKey) -> void:
|
|
||||||
rebind_no_save(action, event)
|
|
||||||
save()
|
|
||||||
|
|
||||||
|
|
||||||
func save() -> void:
|
|
||||||
var cfg := ConfigFile.new()
|
|
||||||
for action: StringName in REBINDABLE_ACTIONS:
|
|
||||||
var event := get_key_event(action)
|
|
||||||
if event != null:
|
|
||||||
cfg.set_value(SECTION, String(action), event.physical_keycode)
|
|
||||||
cfg.save(SAVE_PATH)
|
|
||||||
|
|
||||||
|
|
||||||
func load_saved() -> void:
|
|
||||||
var cfg := ConfigFile.new()
|
|
||||||
if cfg.load(SAVE_PATH) != OK:
|
|
||||||
return
|
|
||||||
for action: StringName in REBINDABLE_ACTIONS:
|
|
||||||
var key := String(action)
|
|
||||||
if not cfg.has_section_key(SECTION, key):
|
|
||||||
continue
|
|
||||||
var keycode: int = cfg.get_value(SECTION, key)
|
|
||||||
var event := InputEventKey.new()
|
|
||||||
event.physical_keycode = keycode as Key
|
|
||||||
rebind_no_save(action, event)
|
|
||||||
|
|
||||||
|
|
||||||
## Rebuilds the action's event list, swapping the first keyboard event for
|
|
||||||
## `event` in place so ordering (and thus the "primary" key) is preserved.
|
|
||||||
func rebind_no_save(action: StringName, event: InputEventKey) -> void:
|
|
||||||
var events := InputMap.action_get_events(action)
|
|
||||||
InputMap.action_erase_events(action)
|
|
||||||
var replaced := false
|
|
||||||
for existing: InputEvent in events:
|
|
||||||
if not replaced and existing is InputEventKey:
|
|
||||||
InputMap.action_add_event(action, event)
|
|
||||||
replaced = true
|
|
||||||
else:
|
|
||||||
InputMap.action_add_event(action, existing)
|
|
||||||
if not replaced:
|
|
||||||
InputMap.action_add_event(action, event)
|
|
||||||
|
|
||||||
|
|
||||||
func reset_defaults() -> void:
|
|
||||||
InputMap.load_from_project_settings()
|
|
||||||
if FileAccess.file_exists(SAVE_PATH):
|
|
||||||
DirAccess.remove_absolute(SAVE_PATH)
|
|
||||||
@@ -34,10 +34,28 @@ commit=${GIT_COMMIT}
|
|||||||
built=${BUILD_DATE}
|
built=${BUILD_DATE}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
# Cache-bust: give wasm/pck unique names so browsers never serve a stale build.
|
||||||
|
# index.js gets a ?v= query string; wasm/pck get renamed because the engine
|
||||||
|
# fetches them internally using GODOT_CONFIG.executable as the base name.
|
||||||
|
STAMP="$GIT_COMMIT"
|
||||||
|
cp "$BUILD_DIR/index.wasm" "$BUILD_DIR/index.${STAMP}.wasm"
|
||||||
|
cp "$BUILD_DIR/index.wasm.gz" "$BUILD_DIR/index.${STAMP}.wasm.gz"
|
||||||
|
cp "$BUILD_DIR/index.pck" "$BUILD_DIR/index.${STAMP}.pck"
|
||||||
|
|
||||||
|
sed -i \
|
||||||
|
-e "s|src=\"index\.js\"|src=\"index.js?v=${STAMP}\"|" \
|
||||||
|
-e "s|\"executable\":\"index\"|\"executable\":\"index.${STAMP}\"|" \
|
||||||
|
-e "s|\"index\.pck\"|\"index.${STAMP}.pck\"|g" \
|
||||||
|
-e "s|\"index\.wasm\"|\"index.${STAMP}.wasm\"|g" \
|
||||||
|
"$BUILD_DIR/index.html"
|
||||||
|
|
||||||
|
# Remove previous build's versioned assets from the server before uploading.
|
||||||
|
ssh "$SERVER_USER@$SERVER_HOST" "find $SERVER_PATH -maxdepth 1 -name 'index.*.wasm' -o -name 'index.*.wasm.gz' -o -name 'index.*.pck' | xargs -r rm -f"
|
||||||
|
|
||||||
scp "$BUILD_DIR/index.html" \
|
scp "$BUILD_DIR/index.html" \
|
||||||
"$BUILD_DIR/index.js" \
|
"$BUILD_DIR/index.js" \
|
||||||
"$BUILD_DIR/index.wasm.gz" \
|
"$BUILD_DIR/index.${STAMP}.wasm.gz" \
|
||||||
"$BUILD_DIR/index.pck" \
|
"$BUILD_DIR/index.${STAMP}.pck" \
|
||||||
"$BUILD_DIR/index.audio.worklet.js" \
|
"$BUILD_DIR/index.audio.worklet.js" \
|
||||||
"$BUILD_DIR/index.audio.position.worklet.js" \
|
"$BUILD_DIR/index.audio.position.worklet.js" \
|
||||||
"$BUILD_DIR/index.icon.png" \
|
"$BUILD_DIR/index.icon.png" \
|
||||||
@@ -46,4 +64,4 @@ scp "$BUILD_DIR/index.html" \
|
|||||||
"$BUILD_DIR/version.txt" \
|
"$BUILD_DIR/version.txt" \
|
||||||
"$SERVER_USER@$SERVER_HOST:$SERVER_PATH/"
|
"$SERVER_USER@$SERVER_HOST:$SERVER_PATH/"
|
||||||
|
|
||||||
echo "deployed ${VERSION} to $SERVER_HOST:$SERVER_PATH"
|
echo "deployed ${VERSION} (${STAMP}) to $SERVER_HOST:$SERVER_PATH"
|
||||||
|
|||||||
@@ -1,18 +1,30 @@
|
|||||||
|
[runnable_presets]
|
||||||
|
|
||||||
|
Web="Web"
|
||||||
|
"Windows Desktop"="Windows Desktop"
|
||||||
|
|
||||||
[preset.0]
|
[preset.0]
|
||||||
|
|
||||||
name="Web"
|
name="Web"
|
||||||
platform="Web"
|
platform="Web"
|
||||||
runnable=true
|
|
||||||
dedicated_server=false
|
dedicated_server=false
|
||||||
custom_features=""
|
custom_features=""
|
||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter=""
|
include_filter=""
|
||||||
exclude_filter=""
|
exclude_filter=""
|
||||||
export_path="build/web/index.html"
|
export_path="build/web/index.html"
|
||||||
|
patches=PackedStringArray()
|
||||||
|
patch_delta_encoding=false
|
||||||
|
patch_delta_compression_level_zstd=19
|
||||||
|
patch_delta_min_reduction=0.1
|
||||||
|
patch_delta_include_filters="*"
|
||||||
|
patch_delta_exclude_filters=""
|
||||||
encryption_include_filters=""
|
encryption_include_filters=""
|
||||||
encryption_exclude_filters=""
|
encryption_exclude_filters=""
|
||||||
|
seed=0
|
||||||
encrypt_pck=false
|
encrypt_pck=false
|
||||||
encrypt_directory=false
|
encrypt_directory=false
|
||||||
|
script_export_mode=2
|
||||||
|
|
||||||
[preset.0.options]
|
[preset.0.options]
|
||||||
|
|
||||||
@@ -21,7 +33,7 @@ custom_template/release=""
|
|||||||
variant/extensions_support=false
|
variant/extensions_support=false
|
||||||
variant/thread_support=true
|
variant/thread_support=true
|
||||||
vram_texture_compression/for_desktop=true
|
vram_texture_compression/for_desktop=true
|
||||||
vram_texture_compression/for_mobile=false
|
vram_texture_compression/for_mobile=true
|
||||||
html/export_icon=true
|
html/export_icon=true
|
||||||
html/custom_html_shell=""
|
html/custom_html_shell=""
|
||||||
html/head_include=""
|
html/head_include=""
|
||||||
@@ -29,6 +41,7 @@ html/canvas_resize_policy=2
|
|||||||
html/focus_canvas_on_start=true
|
html/focus_canvas_on_start=true
|
||||||
html/experimental_virtual_keyboard=false
|
html/experimental_virtual_keyboard=false
|
||||||
progressive_web_app/enabled=false
|
progressive_web_app/enabled=false
|
||||||
|
progressive_web_app/ensure_cross_origin_isolation_headers=true
|
||||||
progressive_web_app/offline_page=""
|
progressive_web_app/offline_page=""
|
||||||
progressive_web_app/display=1
|
progressive_web_app/display=1
|
||||||
progressive_web_app/orientation=0
|
progressive_web_app/orientation=0
|
||||||
@@ -36,3 +49,76 @@ progressive_web_app/icon_144x144=""
|
|||||||
progressive_web_app/icon_180x180=""
|
progressive_web_app/icon_180x180=""
|
||||||
progressive_web_app/icon_512x512=""
|
progressive_web_app/icon_512x512=""
|
||||||
progressive_web_app/background_color=Color(0, 0, 0, 1)
|
progressive_web_app/background_color=Color(0, 0, 0, 1)
|
||||||
|
threads/emscripten_pool_size=8
|
||||||
|
threads/godot_pool_size=4
|
||||||
|
|
||||||
|
[preset.1]
|
||||||
|
|
||||||
|
name="Windows Desktop"
|
||||||
|
platform="Windows Desktop"
|
||||||
|
dedicated_server=false
|
||||||
|
custom_features=""
|
||||||
|
export_filter="all_resources"
|
||||||
|
include_filter=""
|
||||||
|
exclude_filter=""
|
||||||
|
export_path="../Export/Bullosseum_020.exe"
|
||||||
|
patches=PackedStringArray()
|
||||||
|
patch_delta_encoding=false
|
||||||
|
patch_delta_compression_level_zstd=19
|
||||||
|
patch_delta_min_reduction=0.1
|
||||||
|
patch_delta_include_filters="*"
|
||||||
|
patch_delta_exclude_filters=""
|
||||||
|
encryption_include_filters=""
|
||||||
|
encryption_exclude_filters=""
|
||||||
|
seed=0
|
||||||
|
encrypt_pck=false
|
||||||
|
encrypt_directory=false
|
||||||
|
script_export_mode=2
|
||||||
|
|
||||||
|
[preset.1.options]
|
||||||
|
|
||||||
|
custom_template/debug=""
|
||||||
|
custom_template/release=""
|
||||||
|
debug/export_console_wrapper=1
|
||||||
|
binary_format/embed_pck=false
|
||||||
|
texture_format/s3tc_bptc=true
|
||||||
|
texture_format/etc2_astc=false
|
||||||
|
shader_baker/enabled=false
|
||||||
|
binary_format/architecture="x86_64"
|
||||||
|
codesign/enable=false
|
||||||
|
codesign/timestamp=true
|
||||||
|
codesign/timestamp_server_url=""
|
||||||
|
codesign/digest_algorithm=1
|
||||||
|
codesign/description=""
|
||||||
|
codesign/custom_options=PackedStringArray()
|
||||||
|
application/modify_resources=true
|
||||||
|
application/icon=""
|
||||||
|
application/console_wrapper_icon=""
|
||||||
|
application/icon_interpolation=4
|
||||||
|
application/file_version=""
|
||||||
|
application/product_version=""
|
||||||
|
application/company_name=""
|
||||||
|
application/product_name=""
|
||||||
|
application/file_description=""
|
||||||
|
application/copyright=""
|
||||||
|
application/trademarks=""
|
||||||
|
application/export_angle=0
|
||||||
|
application/export_d3d12=0
|
||||||
|
application/d3d12_agility_sdk_multiarch=true
|
||||||
|
ssh_remote_deploy/enabled=false
|
||||||
|
ssh_remote_deploy/host="user@host_ip"
|
||||||
|
ssh_remote_deploy/port="22"
|
||||||
|
ssh_remote_deploy/extra_args_ssh=""
|
||||||
|
ssh_remote_deploy/extra_args_scp=""
|
||||||
|
ssh_remote_deploy/run_script="Expand-Archive -LiteralPath '{temp_dir}\\{archive_name}' -DestinationPath '{temp_dir}'
|
||||||
|
$action = New-ScheduledTaskAction -Execute '{temp_dir}\\{exe_name}' -Argument '{cmd_args}'
|
||||||
|
$trigger = New-ScheduledTaskTrigger -Once -At 00:00
|
||||||
|
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
|
||||||
|
$task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings
|
||||||
|
Register-ScheduledTask godot_remote_debug -InputObject $task -Force:$true
|
||||||
|
Start-ScheduledTask -TaskName godot_remote_debug
|
||||||
|
while (Get-ScheduledTask -TaskName godot_remote_debug | ? State -eq running) { Start-Sleep -Milliseconds 100 }
|
||||||
|
Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue"
|
||||||
|
ssh_remote_deploy/cleanup_script="Stop-ScheduledTask -TaskName godot_remote_debug -ErrorAction:SilentlyContinue
|
||||||
|
Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue
|
||||||
|
Remove-Item -Recurse -Force '{temp_dir}'"
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[gd_scene format=3 uid="uid://map_screen_v1"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://levels/map_screen.gd" id="1_map"]
|
||||||
|
|
||||||
|
[node name="MapScreen" type="Control"]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
script = ExtResource("1_map")
|
||||||