Debug menu F1 (use ctrl to lock cam). Design.md doc

This commit is contained in:
2026-05-10 18:40:14 +03:00
parent dc40057fe1
commit fe5d143047
37 changed files with 1237 additions and 45 deletions
+3
View File
@@ -1,3 +1,6 @@
# Godot 4+ specific ignores
.godot/
/android/
# JetBrains IDE — contains absolute paths (Godot binary, SDK dirs), varies per user
.idea/
+59
View File
@@ -0,0 +1,59 @@
# Bullosseum — CLAUDE.md
## About this project
A 3D arena game built in Godot 4.7 (Forward Plus renderer, Jolt Physics) where the player controls a bull. Currently features third-person camera, bull movement with charge mechanics, and a placeholder arena.
## AI role
I am a **Godot 4.6+ expert**. I follow current best practices for GDScript, scene architecture, physics, and performance. If a question suggests a suboptimal approach — wrong node type, unnecessary complexity, a pattern that fights the engine — I will say so and explain the better alternative before implementing anything.
## Project structure
| Path | Purpose |
|---|---|
| `scene.tscn` | Main scene (arena + player) |
| `Player.tscn` | Player scene root (`CharacterBody3D`) |
| `player.gd` | Movement, charge, turning logic |
| `camera_spring_arm.gd` | Mouse-look pivot (`Node3D` + `SpringArm3D`) |
| `camera_follow.gd` | Smooth camera follow (`Camera3D`) |
| `Assets/` | Raw 3D assets (`.glb`, `.fbx`) |
| `addons/rider-plugin/` | JetBrains Rider IDE integration |
## Tech choices
- **Physics:** Jolt Physics (not the default Godot Physics)
- **Renderer:** Forward Plus
- **Input map:** WASD + arrow keys, Shift = charge, Space = jump, scroll = zoom, middle-click = toggle mouse capture
- **IDE:** JetBrains Rider via the rider-plugin addon
## GDScript conventions
- Typed GDScript everywhere (`var x: float`, return types on functions)
- `@onready` for node references; never `get_node()` strings when avoidable
- Constants in `SCREAMING_SNAKE_CASE`, variables in `snake_case`
- One script per scene root — keep scripts focused
- Signal names in `snake_case`; connect via `signal.connect()` not the legacy string form
- Prefer `_physics_process` for physics/movement, `_process` for visuals/camera, `_unhandled_input` for input that shouldn't bubble
- No comments that restate what the code already says; only comment non-obvious constraints or workarounds
## Best practices to enforce
- Use `CharacterBody3D` for player-controlled characters, not `RigidBody3D` (unless the design specifically needs physics simulation)
- Use `SpringArm3D` for third-person cameras to get free collision avoidance
- Prefer `move_and_slide()` with `velocity` over manual collision queries
- Export variables (`@export`) for any value a designer might tune; keep magic numbers out of logic
- Scene composition over inheritance — build behaviour from small focused scenes
- Use `autoload` (singletons) sparingly: only for truly global state (e.g. GameManager, AudioBus); not as a shortcut for passing data
- Keep `_physics_process` deterministic and frame-rate independent (always multiply by `delta`)
- Prefer signals over direct node references for decoupling
## Godot 4.x specifics
- `wrapf` / `wrap` instead of manual modulo for angles
- `lerp_angle` for smooth rotation interpolation (handles wrap-around correctly)
- `move_toward` for speed ramps without overshooting
- `PackedStringArray`, `PackedVector3Array`, etc. for performance-sensitive arrays
- `@tool` scripts for editor helpers only — don't use in gameplay scripts
- Resource (`extends Resource`) for shared data/config; no plain `Dictionary` for structured data
- `StringName` (`&"action_name"`) for input action lookups in hot paths
+205
View File
@@ -0,0 +1,205 @@
# Bullosseum — Game Design Document
> Living document. `?` marks open questions.
---
## Elevator Pitch
You are the bull. Waves of cowboys, matadors, and gladiators pour into your arena — each with their own tricks to dodge or counter your charge. Survive, wreck things, pick upgrades. Every run is different. Goofy low-poly mayhem.
---
## Art Direction
- **Style:** Low-poly, chunky geometry, flat or minimal shading
- **Tone:** Goofy and physical — think *Gang Beasts* meets *Vampire Survivors* energy
- **Camera:** Third-person, spring-arm collision avoidance
- **Palette:** Warm sand/terracotta arena, vivid faction colours (red/gold for matadors, brown/blue for cowboys, iron/purple for gladiators)
- **Particles:** Stylised cartoon puffs — dust, impact sparks, ragdoll stars
- **Audio direction:** ?
---
## Confirmed Mechanics
- Third-person bull locomotion with momentum (walk, charge)
- Wide committed turning arc during charge — charges feel like commitments
- Visual turn lag (body leans)
- Spring-arm camera with zoom
- Sand dust particles at hooves (walk = small, charge = big)
- Jolt Physics backend
---
## Run Structure (Vampire Survivors-inspired)
```
Start run → Wave spawns → Kill enemies → Wave cleared → Pick upgrade → Next wave → ... → Die or achieve target
```
- **No hard time limit per wave** — pressure comes from enemy density escalating
- **Upgrade picks between waves** — choose 1 of 3 options (stat, ability, passive synergy)
- **Fail state is soft** — getting overwhelmed is a death spiral, not a timeout. You stop being able to charge effectively and get cornered
- **Run length:** ? (1525 minutes for a full run feels right for web)
- **Meta progression:** ? Unlock new starting abilities or bull cosmetics across runs
### Wave Escalation — Faction Ramp
Factions are introduced one at a time so each mechanic gets space to teach itself before mixing begins.
| Waves | Faction makeup | Boss |
|---|---|---|
| 13 | Matadors only | — |
| 4 | Matadors only | El Presidente |
| 57 | Matadors + Cowboys introduced | — |
| 8 | Mixed (matadors + cowboys) | Sheriff |
| 911 | All three factions introduced | — |
| 12 | Mixed (all three) | Centurion |
| 13+ | Full chaos — all factions, escalating density | Repeat bosses + ? final boss |
- Each new faction's first wave is a "pure" wave so the player can learn the pattern before it mixes
- Bosses appear solo — the arena clears before a boss spawns, making it feel like a duel
- ? After wave 12, does a loop begin (harder repeat) or does a final boss end the run?
---
## Enemy Factions
### Matadors (the classics)
The trickiest faction — they're designed around baiting your charge.
| Enemy | Behaviour |
|---|---|
| Matador | Waves red cape to draw your charge, sidesteps at last moment. Must catch them off-guard or predict the dodge |
| Banderillero | Plants slow-flags in the ground — charges into one stuns the bull briefly |
| Picador | Mounted on horse, tanky, slow. Horse must be hit first before the rider is vulnerable |
| ? El Presidente (boss) | Summons matadors and teleports around the arena in bursts |
### Cowboys
The herd faction — individually easy, dangerous in groups.
| Enemy | Behaviour |
|---|---|
| Cowboy | Throws lasso to root you for 12s, then runs. Easy to hit, just annoying |
| Wrangler | Herds other enemies toward you — a force multiplier |
| Mounted Cowboy | Circles you on horseback, throwing dynamite. Must cut off the riding arc |
| ? Sheriff (boss) | Shotgun blast that staggers the bull; spawns deputies on death |
### Gladiators
The armour faction — charge angles matter.
| Enemy | Behaviour |
|---|---|
| Legionary | Front shield absorbs charges completely. Must hit from the side or behind |
| Net-thrower | Long-range root that holds you in place for 3s |
| Archer | Stationary on elevated platform (arena wall edge), fires slowing arrows |
| ? Centurion (boss) | Buffs nearby gladiators, has a charge-reflect shield that bounces you back |
---
## Ability System
Three slots total — always the same structure, filled differently every run.
```
[Charge ──────────] [Medium □] [Ultimate □]
always on 1 slot 1 slot
```
### Slot 1 — Charge (always on)
| | Description |
|---|---|
| **Charge** | Hold to build, release to sprint. Committed arc, hard to steer. The foundation everything else is built around |
### Slot 2 — Medium Ability
Cooldown ~815s. Tactical, reactive, fills gaps that charge can't. One equipped at a time.
| Ability | Description | Good against |
|---|---|---|
| **Stomp** | Slam front hooves — short-range AoE stun | Matador clusters, anything close |
| **Gore** | Upward headbutt — launches a single target | Shielded gladiators (bypasses front shield) |
| **Tail Whip** | 360° sweep behind and to the sides | Cowboys lassooing from behind |
| **Bull Roar** | Knockback shockwave — no damage, pure space creation | Net-throwers, tight groups |
| **Ground Pound** | From a jump, landing creates a shockwave crater | Mounted enemies (dismounts them) |
| **Counter** | Narrow window — if you charge as an attack lands, explosive ricochet | Picadors, centurion reflect |
### Slot 3 — Ultimate Ability
Cooldown ~3550s, or requires a full charge meter. High impact. One equipped at a time.
| Ability | Description | Good against |
|---|---|---|
| **Stampede** | ~5s of zero deceleration — can't stop or steer well, flattens everything in the path | Boss phases, dense waves |
| **Enrage** | ~8s berserk: charge speed +60%, hitbox wider, ignores all slows and roots | Lasso/net-heavy waves |
| **Red Mist** | Every enemy on screen gets a red target marker and is pulled slightly toward you — brief guaranteed hits | Matador dodge phase |
| **Thunder Horn** | Next charge is electrified — chains damage to up to 4 nearby enemies on contact | Gladiator formations |
| **Call of the Herd** | Summon 3 ghost bulls that charge in parallel alongside you for 6s | Any — pure chaos |
### Card Pick Screen (between every wave)
Game pauses. Three cards shown — pick one.
| Card type | Examples |
|---|---|
| **New medium** | Swap current medium for a different one |
| **New ultimate** | Swap current ultimate for a different one |
| **Stat boost** | +charge speed, +turn rate, medium cooldown, ultimate cooldown |
| **Passive modifier** | Changes how your *current* ability behaves (see below) |
| **Synergy** | Unlocks a cross-ability combo effect |
**Passive modifier examples:**
- Stomp → "Dust Blind" — stun cloud also blocks enemy vision for 2s
- Gore → "Pinball" — launched enemy bounces off walls and damages others on landing
- Stampede → "Aftershock" — leaves a trembling ground trail that slows enemies for 5s
- Enrage → "Bloodlust" — kills during enrage reduce remaining cooldown
**Synergy examples:**
- Charge + Stomp = "Seismic Run" — charging for 2s+ turns your next stomp into a ground wave
- Gore + Thunder Horn = "Lightning Rod" — electrified gore sends a chain to the whole group
- ? Others to discover during design
---
## Arena Design
Each faction maps to an arena environment. Mixed-faction waves could transition the arena or blend themes.
| Faction | Arena | Hazards |
|---|---|---|
| Matadors | Sand bullring, wooden barrier ring | Flags slow movement, open centre is the danger zone |
| Cowboys | Frontier rodeo grounds, hay bales, fences | Lasso posts (rope traps), horse stampede lanes |
| Gladiators | Roman colosseum, stone floor, iron gates | Archer towers on walls, spike traps, wet stone (slide) |
- ? Does the arena change mid-run or is one arena selected at run start?
- ? Destructible fence/barrier sections throughout?
---
## Core Loop Revisited
```
Move → Spot threat pattern → Position approach angle → Charge/Ability → Chain hits → Wave clear
↑ ↓
Dodge/react to debuffs (lasso, net, slow-flag) Pick upgrade (1 of 3)
```
The **positioning** step is where skill expression lives: which enemy to hit first, what angle to approach a shielded gladiator, whether to charge the matador or fake him out.
---
## Open Questions
1. **Meta progression** — cosmetic unlocks only, or do you unlock new starting medium/ultimate options across runs?
2. **Arena change** — single arena per run, or does the arena theme shift when a new faction is introduced mid-run?
3. **After wave 12** — infinite loop with scaling difficulty, or a final boss that ends the run cleanly?
4. **Ability replacement** — when you pick a new medium/ultimate card, does it always replace the slot, or can you upgrade an existing ability through repeated picks (level 1→2→3)?
5. **Web scope** — vertical slice (matadors only, 4 waves, one ability tree) or full 12-wave game?
---
## References
- *Vampire Survivors* — wave escalation, upgrade pick loop, run length
- *Gang Beasts* — ragdoll slapstick commitment
- *Katamari* / *Donut County* — simple core verb scaled through variety
- *Superhot* — charge-as-commitment, every move mattering
- *Cult of the Lamb* — faction variety with distinct visual languages
+33
View File
@@ -0,0 +1,33 @@
# Contribution
Hello, thanks for taking time and helping out with the addon!
Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. Please sign the CLA before sending the PR: https://www.jetbrains.com/agreements/cla/.
#### Local setup
Open the `godot-editor-plugin/CMakeLists.txt` in Rider 2026.1+, select `rider-gdextension` target in the run configuration selector.
There are some more fixes coming in 2026.2, which will allow working on gdscript and cpp files within the same workspace.
#### Signing the binaries
Plugin binaries need to be signed to comply with modern operating system security requirements. Unsigned dynamic libraries may fail to load or trigger security warnings:
- **macOS**: Requires code signing (and often notarization) for `.dylib` files. Unsigned libraries may simply fail to load.
- **Windows**: SmartScreen may block unsigned binaries, and corporate environments often enforce signed code only.
- **Linux**: Generally allows unsigned `.so` files, but some sandboxed environments (Flatpak, Snap) impose restrictions.
**How signing is implemented:**
1. **TeamCity Configuration**: [ijplatform_master_Net_Deploy_Plugins_Public_Godot](https://buildserver.labs.intellij.net/buildConfiguration/ijplatform_master_Net_Deploy_Plugins_Public_Godot) (internal JetBrains link)
- Note: The automatic trigger on new tags is currently not working, so manual triggering is required.
2. **Release Process**:
- First, use your GitHub Action to prepare a **pre-release** with a tag
- Manually trigger the TeamCity configuration on the necessary branch/tag
- The configuration will download assets, sign them, and upload them back (removing the pre-release flag)
- The pre-release flag is required at the start; otherwise, the signing configuration will skip the release (protection against double signing)
3. **Signing existing releases**:
- You can run signing on already existing old releases/tags
- Mark them as pre-release first, then run the configuration on the necessary tag
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 JetBrains
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+51
View File
@@ -0,0 +1,51 @@
# JetBrains Rider Integration Godot addon
This addon currently provides two features:
1. Finds all Rider installations in the system and provides a selector on the `Text Editor` -> `External` tab in the settings to select one.
2. Provides the "Use Rider" toggle in the Godot toolbar and, when enabled, applies a set of editor settings recommended for working with JetBrains Rider. The goal is to make it trivial to switch between Rideroptimized settings and stock Godot settings with a single click.
## Quick start
Requirements:
- Godot 4.2.2+
Install:
1. Inside the Godot editor, it can be installed from the AssetLib view or [downloaded](https://godotengine.org/asset-library/asset/4576)
2. [Optional] Change the initial value of `active` in the plugin.cfg
3. [Optional] Change the initial values in the presets.json file.
4. Enable "JetBrains Rider External Editor" plugin in the Project → Project Settings… → Plugins tab.
Use:
- A toolbar toggle named "Use Rider" will appear. Click it to turn the preset On/Off.
Screenshot:
![Toolbar toggle](screenshots/Toolbar.png)
## What the toggle changes
The preset values live in `presets.json`.
When ON:
- Write the values from the "on" preset into the Editor Settings.
When OFF:
- Write the values from the "off" preset into the Editor Settings.
Note: The plugin does not currently autoset Riders executable path or flags. See Plans below.
## Setting Rider to be the external editor
The plugin automatically detects installed Rider versions on your system and provides a convenient dropdown menu to
select which installation to use as your external editor.
- The plugin scans common installation locations for Rider on Windows, macOS, and Linux.
- Detected installations appear in the "Select Rider" dropdown in the toolbar.
- When you select a Rider installation, the plugin automatically updates the `dotnet/editor/external_editor_path` editor
setting.
## License
See `addons/rider-plugin/LICENCE`.
## Acknowledgements
Created by JetBrains to streamline using Rider with Godot.
Initial idea https://github.com/sszigeti/toggle_external_editor
@@ -0,0 +1,16 @@
[configuration]
entry_symbol = "rider_library_init"
compatibility_minimum = "4.2.2"
[libraries]
macos.single.debug = "./macos/librider-gdextension.macos.editor.dylib"
windows.arm64.single.debug = "./windows/rider-gdextension.windows.editor.arm64.dll"
windows.x86_64.single.debug = "./windows/rider-gdextension.windows.editor.x86_64.dll"
linux.x86_64.single.debug = "./linux/librider-gdextension.linux.editor.x86_64.so"
linux.arm64.single.debug = "./linux/librider-gdextension.linux.editor.arm64.so"
+144
View File
@@ -0,0 +1,144 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.0"
id="katman_1"
x="0px"
y="0px"
viewBox="0 0 512 512"
xml:space="preserve"
sodipodi:docname="JetBrains Rider Icon.svg"
width="512"
height="512"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs43" /><sodipodi:namedview
id="namedview41"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
showgrid="false"
inkscape:zoom="1.0980553"
inkscape:cx="256.36233"
inkscape:cy="212.19333"
inkscape:window-width="1920"
inkscape:window-height="1027"
inkscape:window-x="-8"
inkscape:window-y="22"
inkscape:window-maximized="1"
inkscape:current-layer="katman_1" />
<style
type="text/css"
id="style2">
.st0{fill:url(#SVGID_1_);}
.st1{fill:url(#SVGID_00000130622934180993703410000017420799098261276343_);}
.st2{fill:url(#SVGID_00000060739771362873723200000017991140373209755022_);}
.st3{fill:#FFFFFF;}
</style>
<symbol
id="rider"
viewBox="-35 -35 70 70">
<linearGradient
id="SVGID_1_"
gradientUnits="userSpaceOnUse"
x1="30.4897"
y1="5.1188998"
x2="-23.4683"
y2="-25.8451">
<stop
offset="0"
style="stop-color:#DD1265"
id="stop4" />
<stop
offset="0.483"
style="stop-color:#DD1265"
id="stop6" />
<stop
offset="0.942"
style="stop-color:#FDB60D"
id="stop8" />
</linearGradient>
<path
class="st0"
d="M 35,-7.7 -14.1,-35 18.8,13.9 25.5,9.5 Z"
id="path11" />
<linearGradient
id="SVGID_00000169517474296634577950000016895268102491994795_"
gradientUnits="userSpaceOnUse"
x1="-1.5839"
y1="-28.888"
x2="19.805099"
y2="30.174999">
<stop
offset="0.139"
style="stop-color:#087CFA"
id="stop13" />
<stop
offset="0.476"
style="stop-color:#DD1265"
id="stop15" />
<stop
offset="0.958"
style="stop-color:#087CFA"
id="stop17" />
</linearGradient>
<path
style="fill:url(#SVGID_00000169517474296634577950000016895268102491994795_)"
d="M 15.5,-18.9 9.3,-33.9 -4.3,-20.5 1.2,28.1 14.4,35 35,23 Z"
id="path20" />
<linearGradient
id="SVGID_00000135668350575375336630000014638141720088491653_"
gradientUnits="userSpaceOnUse"
x1="-17.5865"
y1="-27.071199"
x2="-1.7875"
y2="29.073799">
<stop
offset="0.278"
style="stop-color:#DD1265"
id="stop22" />
<stop
offset="0.968"
style="stop-color:#FDB60D"
id="stop24" />
</linearGradient>
<path
style="fill:url(#SVGID_00000135668350575375336630000014638141720088491653_)"
d="m -14.1,-35 -20.9,14.1 7.8,48.1 20.1,7.7 26,-21 z"
id="path27" />
<path
d="M 21,-21 H -21 V 21 H 21 Z"
id="path29" />
<path
class="st3"
d="m -0.6,13.6 h -15.8 v 2.7 h 15.8 z"
id="path31" />
<path
class="st3"
d="m 0.5,-15.8 h 6.2 c 5,0 8.4,3.4 8.4,7.7 0,4.4 -3.4,7.9 -8.4,7.9 L 0.5,0 Z m 3.5,3.2 v 9.5 h 2.7 c 2.8,0 4.8,-1.9 4.8,-4.6 0,-2.8 -1.9,-4.7 -4.8,-4.7 z"
id="path33" />
<path
class="st3"
d="m -15.7,-15.8 h 7.2 c 2,0 3.5,0.6 4.6,1.6 0.9,0.9 1.3,2.1 1.3,3.6 v 0.1 c 0,1.3 -0.3,2.3 -0.9,3.1 -0.6,0.8 -1.4,1.4 -2.4,1.8 L -2,0 h -4.1 l -3.3,-4.8 h -2.9 V 0 h -3.5 z m 7,7.7 c 0.8,0 1.5,-0.2 2,-0.6 0.5,-0.4 0.7,-1 0.7,-1.6 v -0.1 c 0,-0.8 -0.2,-1.3 -0.7,-1.7 -0.5,-0.3 -1.1,-0.6 -2,-0.6 h -3.4 v 4.5 h 3.4 z"
id="path35" />
</symbol>
<use
xlink:href="#rider"
width="70"
height="70"
x="-35"
y="-35"
transform="matrix(7.0682519,0,0,7.2481031,256,256)"
style="overflow:visible"
id="use38" />
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

+43
View File
@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://wrdrj6wednn8"
path="res://.godot/imported/icon.svg-45c914cff7482ba9564963fe65b548e4.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/rider-plugin/icons/icon.svg"
dest_files=["res://.godot/imported/icon.svg-45c914cff7482ba9564963fe65b548e4.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
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
+12
View File
@@ -0,0 +1,12 @@
[plugin]
name="JetBrains Rider External Editor"
description="Provides a toolbar toggle, to switch a set of settings on and off. Default set of settings helps to enable/disable the following settings recommended by the Rider documentation https://www.jetbrains.com/help/rider/Godot.html#optimize-godot-editor-for-rider"
author="JetBrains"
version="1.0.0"
script="rider-plugin.gd"
[presets]
active="off"
presets="presets.json"
+15
View File
@@ -0,0 +1,15 @@
{
"on": {
"text_editor/external/use_external_editor": true,
"interface/editor/import_resources_when_unfocused": true,
"interface/editor/save_on_focus_loss": true,
"text_editor/behavior/files/auto_reload_scripts_on_external_change": true,
"run/window_placement/game_embed_mode": -1
},
"off": {
"text_editor/external/use_external_editor": false,
"interface/editor/import_resources_when_unfocused": false,
"interface/editor/save_on_focus_loss": false,
"text_editor/behavior/files/auto_reload_scripts_on_external_change": false
}
}
+66
View File
@@ -0,0 +1,66 @@
@tool
extends EditorPlugin
var editor_settings: EditorSettings
var checkbutton: CheckButton
var _preset_applier: PresetApplier
var _settings_service: EditorSettingsService
var _locator_service: RiderLocatorService
var _plugin_cfg_path: String
var _presets_json_path: String
func _enter_tree() -> void:
editor_settings = EditorInterface.get_editor_settings()
var script_path := (get_script() as Script).resource_path
var plugin_dir := script_path.get_base_dir()
_plugin_cfg_path = plugin_dir + "/plugin.cfg"
var cfg := ConfigFile.new()
var err := cfg.load(_plugin_cfg_path)
if err != OK:
push_warning("Failed to load plugin.cfg: %s" % [err])
return
var active_str := str(cfg.get_value("presets", "active", "on"))
var is_active := active_str == "on"
var presets_rel_path := str(cfg.get_value("presets", "presets", "presets.json"))
_presets_json_path = plugin_dir + "/" + presets_rel_path
# Build UI
checkbutton = CheckButton.new()
checkbutton.text = "Use Rider"
checkbutton.tooltip_text = "Shortcut for setting recommended settings"
checkbutton.button_pressed = is_active
checkbutton.pressed.connect(_on_checkbutton_pressed)
add_control_to_container(EditorPlugin.CONTAINER_TOOLBAR, checkbutton)
# Initialize services and panel
_settings_service = EditorSettingsService.new()
_locator_service = RiderLocatorService.new()
_preset_applier = PresetApplier.new(_presets_json_path)
_locator_service.add_selector_in_editor_interface(_settings_service)
# Ensure settings reflect current state on startup
_preset_applier.apply_preset(editor_settings, is_active)
func _on_checkbutton_pressed() -> void:
var cfg := ConfigFile.new()
if cfg.load(_plugin_cfg_path) != OK:
push_warning("Failed to load plugin.cfg to update state")
return
var is_active := checkbutton.button_pressed
var key := _preset_applier.get_preset_key(is_active)
cfg.set_value("presets", "active", key)
var save_err := cfg.save(_plugin_cfg_path)
if save_err != OK:
push_warning("Failed to save plugin.cfg: %s" % [save_err])
# Apply selected preset to editor settings
_preset_applier.apply_preset(editor_settings, is_active)
func _exit_tree() -> void:
if checkbutton != null:
remove_control_from_container(EditorPlugin.CONTAINER_TOOLBAR, checkbutton)
checkbutton.queue_free()
var args = OS.get_cmdline_args()
if "--rider-addon-tests" in args:
print("==== rider-addon-tests finished ====")
+1
View File
@@ -0,0 +1 @@
uid://c1x58xm2w1n20
Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bro2w3ghx4xt7"
path="res://.godot/imported/Toolbar.png-a521e2493bd3c08a829245b3129bb58f.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/rider-plugin/screenshots/Toolbar.png"
dest_files=["res://.godot/imported/Toolbar.png-a521e2493bd3c08a829245b3129bb58f.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
+23
View File
@@ -0,0 +1,23 @@
@tool
## Simple JSON helpers for editor/runtime use.
## Keeps file and JSON parsing concerns out of feature code.
class_name JsonUtils
static func load_from_file(path: String) -> Variant:
# Returns parsed JSON value (Dictionary/Array/etc.) or null on error.
var file := FileAccess.open(path, FileAccess.READ)
if file == null:
push_warning("JsonUtils: Failed to open file: %s" % path)
return null
var text := file.get_as_text()
file.close()
var data: Variant = JSON.parse_string(text)
if data == null:
push_warning("JsonUtils: Invalid JSON in file: %s" % path)
return null
return data
static func load_dict_from_file(path: String) -> Dictionary:
# Returns Dictionary or empty {} on error.
var data := load_from_file(path) as Dictionary
return data
@@ -0,0 +1 @@
uid://bci4kmk7h4j6a
@@ -0,0 +1,112 @@
@tool
extends RefCounted
class_name RiderLocatorService
var _installations_found: Array = []
var _thread: Thread = null
func get_installations() -> Array:
var result: Array = RiderLocator.new().get_installations() # from the gdextension
return result
# todo
func fix_external_editor_if_supplied_in_commandline(_settings_service: EditorSettingsService, editor_settings: EditorSettings) -> bool:
# When Godot is started from Rider (or vice versa), we may receive the Rider path
# via command-line so we can keep Godot's external editor setting in sync.
# Supported form (only this one):
# --my_rider_path="/absolute/path/to/rider with possible spaces"
var args : Array = OS.get_cmdline_args()
var provided_rider_path := ""
for a_raw in args:
var a: String = str(a_raw)
if a.begins_with("--my_rider_path="):
provided_rider_path = a.substr("--my_rider_path=".length())
break
if provided_rider_path.is_empty():
return false
provided_rider_path = trim_quotes(provided_rider_path)
# Validate existence (file or dir)
var looks_existing := FileAccess.file_exists(provided_rider_path) || DirAccess.dir_exists_absolute(provided_rider_path)
if looks_existing:
_settings_service.set_external_editor_path(editor_settings, provided_rider_path)
print("Rider path provided via CLI (my_rider_path): ", provided_rider_path)
return true
else:
push_warning("my_rider_path was provided but does not exist: %s" % [provided_rider_path])
return false
func trim_quotes(s: String) -> String:
if s.begins_with('"') and s.ends_with('"') and s.length() >= 2:
return s.substr(1, s.length() - 2)
return s
func add_selector_in_editor_interface(_settings_service: EditorSettingsService):
_update_selector(_installations_found)
if _installations_found.is_empty() and _thread == null:
_thread = Thread.new()
_thread.start(_load_installations)
func _load_installations() -> void:
var array: Array = get_installations()
call_deferred("_on_installations_loaded", array)
func _on_installations_loaded(array: Array):
_installations_found = array
if _thread:
_thread.wait_to_finish()
_thread = null
_update_selector(_installations_found)
func _notification(what: int) -> void:
if what == NOTIFICATION_PREDELETE and _thread != null:
_thread.wait_to_finish()
func _update_selector(array: Array):
var name := "text_editor/external/editor"
var settings := EditorInterface.get_editor_settings()
if !(settings.has_setting(name)):
settings.set(name, 0)
var installations: Array = ["Custom"]
for element in array:
var display_name: String = element.get("display", "")
# Replace special characters that break PROPERTY_HINT_ENUM format
# Comma is the enum delimiter, colon is used for explicit value assignment
display_name = display_name.replace(",", "").replace(":", " -")
installations.append(display_name)
var options :String = ",".join(installations)
settings.add_property_info({
"name": name,
"type":TYPE_INT,
"hint":PROPERTY_HINT_ENUM,
"hint_string": options
})
# Connect to settings changes to update external editor path when selection changes
if not settings.settings_changed.is_connected(_on_selection_changed):
settings.settings_changed.connect(_on_selection_changed.bind())
func _on_selection_changed() -> void:
var name := "text_editor/external/editor"
var settings := EditorInterface.get_editor_settings()
var selected_index: int = settings.get_setting(name)
# Index 0 is "Custom", so user manages the path manually
if selected_index == 0:
return
# Map to actual installation (offset by 1 because of "Custom" at index 0)
var installation_index := selected_index - 1
var installations_array = _installations_found
if installation_index >= 0 and installation_index < installations_array.size():
var installation = installations_array[installation_index]
var new_path: String = installation.get("path", "")
if not new_path.is_empty():
EditorSettingsService.new().set_external_editor_path(settings, new_path)
@@ -0,0 +1 @@
uid://cfsu1rbg0ypem
@@ -0,0 +1,35 @@
@tool
extends RefCounted
class_name PresetApplier
var presets_path: String
func _init(p_path: String) -> void:
presets_path = p_path
func get_preset_key(is_active: bool) -> String:
return "on" if is_active else "off"
func apply_preset(editor_settings: EditorSettings, is_active: bool) -> void:
var data: Dictionary = JsonUtils.load_dict_from_file(presets_path)
if data.is_empty():
push_warning("Failed to load presets: %s" % presets_path)
return
var new_preset_key := get_preset_key(is_active)
var previous_preset_key := get_preset_key(not is_active)
if not data.has(new_preset_key):
push_warning("Preset '%s' not found in presets.json" % new_preset_key)
return
var new_preset := data[new_preset_key] as Dictionary
# Reset keys from previous preset that are missing in the new preset
if data.has(previous_preset_key):
var previous_preset := data[previous_preset_key] as Dictionary
for key in previous_preset:
if not new_preset.has(key):
editor_settings.set_setting(str(key), editor_settings.property_get_revert(str(key)))
# Apply the new preset
for key in new_preset:
editor_settings.set_setting(str(key), new_preset[key])
@@ -0,0 +1 @@
uid://bjiaycxso8h8u
@@ -0,0 +1,17 @@
@tool
extends RefCounted
class_name EditorSettingsService
func set_external_editor_path(editor_settings: EditorSettings, path: String) -> void:
editor_settings.set_setting("text_editor/external/exec_path", path)
func has_valid_external_editor_path(editor_settings: EditorSettings) -> bool:
var has_setting: bool = editor_settings.has_setting("text_editor/external/exec_path")
if not has_setting:
return false
var path : String = editor_settings.get_setting("text_editor/external/exec_path")
var exists := not path.is_empty() && (FileAccess.file_exists(path) || DirAccess.dir_exists_absolute(path))
return exists
func set_use_external_editor(editor_settings: EditorSettings, enabled: bool) -> void:
editor_settings.set_setting("text_editor/external/use_external_editor", enabled)
@@ -0,0 +1 @@
uid://bdiu78ot0rrkc
+192
View File
@@ -0,0 +1,192 @@
extends Node
## Runtime debug panel. Toggle with F1.
## Reads DP.get_all() to build controls — adding a param to DP is all that's needed to show it here.
var _panel: Control
var _status_label: Label
var _prev_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_CAPTURED
func _ready() -> void:
_build_ui()
DP.any_changed.connect(_on_dp_changed)
func _build_ui() -> void:
var canvas := CanvasLayer.new()
canvas.layer = 128
add_child(canvas)
_panel = PanelContainer.new()
_panel.anchor_left = 1.0
_panel.anchor_right = 1.0
_panel.anchor_top = 0.0
_panel.anchor_bottom = 1.0
_panel.offset_left = -390.0
_panel.offset_right = 0.0
_panel.offset_top = 0.0
_panel.offset_bottom = 0.0
_panel.mouse_filter = Control.MOUSE_FILTER_STOP
_panel.visible = false
var bg := StyleBoxFlat.new()
bg.bg_color = Color(0.08, 0.08, 0.10, 0.95)
_panel.add_theme_stylebox_override("panel", bg)
canvas.add_child(_panel)
var root_vbox := VBoxContainer.new()
root_vbox.size_flags_horizontal = Control.SIZE_FILL
_panel.add_child(root_vbox)
# ── Toolbar ───────────────────────────────────────────────────────────────
var toolbar_margin := MarginContainer.new()
toolbar_margin.add_theme_constant_override("margin_left", 8)
toolbar_margin.add_theme_constant_override("margin_right", 8)
toolbar_margin.add_theme_constant_override("margin_top", 6)
toolbar_margin.add_theme_constant_override("margin_bottom", 4)
root_vbox.add_child(toolbar_margin)
var toolbar := HBoxContainer.new()
toolbar.add_theme_constant_override("separation", 6)
toolbar_margin.add_child(toolbar)
var title := Label.new()
title.text = "DEBUG [F1]"
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
toolbar.add_child(title)
_status_label = Label.new()
_status_label.modulate = Color(0.4, 1.0, 0.5)
toolbar.add_child(_status_label)
var save_btn := Button.new()
save_btn.text = "Save"
save_btn.pressed.connect(_on_save)
toolbar.add_child(save_btn)
var reset_btn := Button.new()
reset_btn.text = "Reset All"
reset_btn.pressed.connect(DP.reset_all)
toolbar.add_child(reset_btn)
root_vbox.add_child(HSeparator.new())
# ── Scrollable content ────────────────────────────────────────────────────
var scroll := ScrollContainer.new()
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
scroll.size_flags_horizontal = Control.SIZE_FILL
root_vbox.add_child(scroll)
var content_margin := MarginContainer.new()
content_margin.size_flags_horizontal = Control.SIZE_EXPAND_FILL
content_margin.add_theme_constant_override("margin_left", 8)
content_margin.add_theme_constant_override("margin_right", 8)
content_margin.add_theme_constant_override("margin_top", 4)
content_margin.add_theme_constant_override("margin_bottom", 8)
scroll.add_child(content_margin)
var content := VBoxContainer.new()
content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
content_margin.add_child(content)
_build_params(content)
func _build_params(content: VBoxContainer) -> void:
# Group keys by section preserving registration order
var sections: Dictionary = {}
for key: String in DP.get_all():
var sec: String = DP.get_all()[key]["section"]
if not sections.has(sec):
sections[sec] = []
sections[sec].append(key)
for section: String in sections:
content.add_child(HSeparator.new())
var header := Label.new()
header.text = section.to_upper()
header.modulate = Color(0.75, 0.85, 1.0)
content.add_child(header)
for key: String in sections[section]:
_add_float_row(content, key, DP.get_all()[key])
func _add_float_row(parent: VBoxContainer, key: String, meta: Dictionary) -> void:
var row := HBoxContainer.new()
row.size_flags_horizontal = Control.SIZE_FILL
parent.add_child(row)
var lbl := Label.new()
lbl.text = key.replace("_", " ")
lbl.custom_minimum_size.x = 140
lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
row.add_child(lbl)
var slider := HSlider.new()
slider.min_value = meta["min"]
slider.max_value = meta["max"]
slider.step = meta["step"]
slider.value = meta["value"]
slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL
slider.custom_minimum_size.x = 60
row.add_child(slider)
var spin := SpinBox.new()
spin.min_value = meta["min"]
spin.max_value = meta["max"]
spin.step = meta["step"]
spin.value = meta["value"]
spin.custom_minimum_size.x = 85
row.add_child(spin)
# Slider → SpinBox + DP (use no_signal to avoid echo)
slider.value_changed.connect(func(v: float) -> void:
spin.set_value_no_signal(v)
DP.set_value(key, v)
)
# SpinBox → Slider + DP
spin.value_changed.connect(func(v: float) -> void:
slider.set_value_no_signal(v)
DP.set_value(key, v)
)
# External changes (reset, load) → sync both controls
DP.any_changed.connect(func(changed_key: String, _val: Variant) -> void:
if changed_key != key and changed_key != "__all__":
return
var v := DP.f(key)
slider.set_value_no_signal(v)
spin.set_value_no_signal(v)
)
func _on_save() -> void:
DP.save()
_status_label.text = "Saved!"
get_tree().create_timer(1.5).timeout.connect(func() -> void:
_status_label.text = ""
)
func _on_dp_changed(_key: String, _value: Variant) -> void:
pass # individual rows handle their own updates via signal
func _input(event: InputEvent) -> void:
if _panel.visible and Input.is_key_pressed(KEY_CTRL) and event is InputEventMouseMotion:
get_viewport().set_input_as_handled()
func _unhandled_input(event: InputEvent) -> void:
if not (event is InputEventKey and event.pressed and not event.echo):
return
if (event as InputEventKey).keycode != KEY_F1:
return
_panel.visible = not _panel.visible
if _panel.visible:
_prev_mouse_mode = Input.mouse_mode
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
else:
Input.mouse_mode = _prev_mouse_mode
get_viewport().set_input_as_handled()
+98
View File
@@ -0,0 +1,98 @@
extends Node
## Central registry for all runtime-tunable parameters.
## Add a new param with one _reg_f() call in _register_all() — the menu picks it up automatically.
## Access: DP.f("key") • Persist: DP.save() / DP.load_saved() • Reset: DP.reset_all()
signal any_changed(key: String, value: Variant)
const SAVE_PATH := "user://debug_params.cfg"
var _params: Dictionary = {}
func _ready() -> void:
_register_all()
load_saved()
func _register_all() -> void:
# ── Movement ──────────────────────────────────────────────────────────────
_reg_f("Movement", "walk_speed", 6.0, 1.0, 40.0)
_reg_f("Movement", "charge_speed", 18.0, 5.0, 60.0)
_reg_f("Movement", "walk_accel", 14.0, 1.0, 80.0)
_reg_f("Movement", "charge_accel", 40.0, 5.0, 120.0)
_reg_f("Movement", "deceleration", 12.0, 1.0, 50.0)
_reg_f("Movement", "charge_decel", 3.5, 0.5, 20.0)
_reg_f("Movement", "jump_velocity", 4.5, 1.0, 20.0)
# ── Turning ───────────────────────────────────────────────────────────────
_reg_f("Turning", "walk_turn_fast", 8.0, 0.5, 25.0)
_reg_f("Turning", "walk_turn_slow", 3.5, 0.5, 15.0)
_reg_f("Turning", "charge_turn", 0.55, 0.05, 3.0, 0.01)
_reg_f("Turning", "visual_turn_speed", 12.0, 1.0, 40.0)
# ── Dust ──────────────────────────────────────────────────────────────────
_reg_f("Dust", "dust_lifetime", 0.9, 0.1, 4.0)
_reg_f("Dust", "dust_explosiveness", 0.35, 0.0, 1.0)
_reg_f("Dust", "dust_spread", 45.0, 5.0, 90.0)
_reg_f("Dust", "dust_gravity_y", -1.5,-15.0, 0.0)
_reg_f("Dust", "walk_scale_min", 0.35, 0.05, 3.0)
_reg_f("Dust", "walk_scale_max", 0.65, 0.05, 3.0)
_reg_f("Dust", "walk_vel_min", 0.8, 0.1, 12.0)
_reg_f("Dust", "walk_vel_max", 1.8, 0.1, 12.0)
_reg_f("Dust", "charge_scale_min", 1.0, 0.05, 6.0)
_reg_f("Dust", "charge_scale_max", 2.2, 0.05, 6.0)
_reg_f("Dust", "charge_vel_min", 2.8, 0.1, 20.0)
_reg_f("Dust", "charge_vel_max", 5.5, 0.1, 20.0)
func _reg_f(section: String, key: String, default: float,
min_val: float, max_val: float, step: float = 0.05) -> void:
_params[key] = {
"value": default,
"default": default,
"section": section,
"type": TYPE_FLOAT,
"min": min_val,
"max": max_val,
"step": step,
}
## Read a float param. Panics on unknown key — intentional: typos should be loud.
func f(key: String) -> float:
return _params[key]["value"] as float
func set_value(key: String, value: Variant) -> void:
if not _params.has(key):
return
_params[key]["value"] = value
any_changed.emit(key, value)
## Returns the full metadata dict — used by DebugMenu to build UI.
func get_all() -> Dictionary:
return _params
func save() -> void:
var cfg := ConfigFile.new()
for key: String in _params:
var p: Dictionary = _params[key]
cfg.set_value(p["section"], key, p["value"])
cfg.save(SAVE_PATH)
func load_saved() -> void:
var cfg := ConfigFile.new()
if cfg.load(SAVE_PATH) != OK:
return
for section: String in cfg.get_sections():
for key: String in cfg.get_section_keys(section):
if _params.has(key):
_params[key]["value"] = cfg.get_value(section, key)
func reset_all() -> void:
for key: String in _params:
_params[key]["value"] = _params[key]["default"]
any_changed.emit("__all__", null)
+42 -45
View File
@@ -1,19 +1,6 @@
extends CharacterBody3D
const WALK_SPEED = 6.0
const CHARGE_SPEED = 180.0
const WALK_ACCEL = 14.0
const CHARGE_ACCEL = 40.0
const DECELERATION = 12.0
const CHARGE_DECEL = 3.5
const WALK_TURN_FAST = 8.0
const WALK_TURN_SLOW = 3.5
const CHARGE_TURN = 0.55
const JUMP_VELOCITY = 4.5
# Hoof positions in bull_v10 local space — tweak y/z if hooves don't line up with the model
# Hoof positions in bull_v10 local space — adjust y/z if puffs don't land on the hooves
const HOOF_OFFSETS: Array = [
Vector3(-0.30, -0.25, -0.50),
Vector3( 0.30, -0.25, -0.50),
@@ -21,8 +8,6 @@ const HOOF_OFFSETS: Array = [
Vector3( 0.30, -0.25, 0.60),
]
@export var visual_turn_speed: float = 12.0
@onready var camera_pivot: Node3D = $SpringArmPivot
@onready var cube_guy: Node3D = $bull_v10
@@ -37,6 +22,7 @@ var _dust_state: _DustState = _DustState.NONE
func _ready() -> void:
_setup_hoof_dust()
DP.any_changed.connect(_on_dp_changed)
func _setup_hoof_dust() -> void:
@@ -46,7 +32,6 @@ func _setup_hoof_dust() -> void:
mat.vertex_color_use_as_albedo = true
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
# 4×2 sphere = chunky low-poly diamond shape
var sphere := SphereMesh.new()
sphere.radius = 0.10
sphere.height = 0.20
@@ -54,12 +39,10 @@ func _setup_hoof_dust() -> void:
sphere.rings = 2
sphere.material = mat
# sand colour, fades to transparent over lifetime
var ramp := Gradient.new()
ramp.set_color(0, Color(0.80, 0.69, 0.46, 1.0))
ramp.set_color(1, Color(0.80, 0.69, 0.46, 0.0))
# puff blooms up then vanishes
var scale_curve := Curve.new()
scale_curve.add_point(Vector2(0.0, 0.2))
scale_curve.add_point(Vector2(0.4, 1.0))
@@ -70,17 +53,17 @@ func _setup_hoof_dust() -> void:
p.position = offset
p.emitting = false
p.amount = 12
p.lifetime = 0.9
p.explosiveness = 0.35
p.lifetime = DP.f("dust_lifetime")
p.explosiveness = DP.f("dust_explosiveness")
p.randomness = 0.6
p.local_coords = false
p.direction = Vector3(0.0, 1.0, 0.0)
p.spread = 45.0
p.gravity = Vector3(0.0, -1.5, 0.0)
p.initial_velocity_min = 0.8
p.initial_velocity_max = 1.8
p.scale_amount_min = 0.4
p.scale_amount_max = 0.7
p.spread = DP.f("dust_spread")
p.gravity = Vector3(0.0, DP.f("dust_gravity_y"), 0.0)
p.initial_velocity_min = DP.f("walk_vel_min")
p.initial_velocity_max = DP.f("walk_vel_max")
p.scale_amount_min = DP.f("walk_scale_min")
p.scale_amount_max = DP.f("walk_scale_max")
p.mesh = sphere
p.color_ramp = ramp
p.scale_amount_curve = scale_curve
@@ -93,29 +76,42 @@ func _set_dust_state(new_state: _DustState) -> void:
return
_dust_state = new_state
for p: CPUParticles3D in _hoof_emitters:
p.lifetime = DP.f("dust_lifetime")
p.explosiveness = DP.f("dust_explosiveness")
p.spread = DP.f("dust_spread")
p.gravity = Vector3(0.0, DP.f("dust_gravity_y"), 0.0)
match new_state:
_DustState.NONE:
p.emitting = false
_DustState.WALK:
p.scale_amount_min = 0.35
p.scale_amount_max = 0.65
p.initial_velocity_min = 0.8
p.initial_velocity_max = 1.8
p.scale_amount_min = DP.f("walk_scale_min")
p.scale_amount_max = DP.f("walk_scale_max")
p.initial_velocity_min = DP.f("walk_vel_min")
p.initial_velocity_max = DP.f("walk_vel_max")
p.emitting = true
_DustState.CHARGE:
p.scale_amount_min = 1.0
p.scale_amount_max = 2.2
p.initial_velocity_min = 2.8
p.initial_velocity_max = 5.5
p.scale_amount_min = DP.f("charge_scale_min")
p.scale_amount_max = DP.f("charge_scale_max")
p.initial_velocity_min = DP.f("charge_vel_min")
p.initial_velocity_max = DP.f("charge_vel_max")
p.emitting = true
func _on_dp_changed(_key: String, _val: Variant) -> void:
# Force re-apply so slider changes take effect without needing a state transition
if _hoof_emitters.is_empty() or _dust_state == _DustState.NONE:
return
var prev := _dust_state
_dust_state = _DustState.NONE
_set_dust_state(prev)
func _physics_process(delta: float) -> void:
if not is_on_floor():
velocity += get_gravity() * delta
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = JUMP_VELOCITY
velocity.y = DP.f("jump_velocity")
var input_dir := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
var cam_basis := camera_pivot.global_transform.basis
@@ -123,35 +119,36 @@ func _physics_process(delta: float) -> void:
var flat_right := Vector3(cam_basis.x.x, 0.0, cam_basis.x.z).normalized()
var direction := (flat_forward * -input_dir.y + flat_right * input_dir.x).normalized()
var charging := Input.is_action_pressed(&"charge")
if direction:
var target_angle := atan2(direction.x, direction.z)
if current_speed < 0.01:
facing_angle = target_angle
var charging := Input.is_key_pressed(KEY_SHIFT)
var speed_t: float = clampf(current_speed / WALK_SPEED, 0.0, 1.0)
var turn_rate: float = CHARGE_TURN if charging else lerpf(WALK_TURN_FAST, WALK_TURN_SLOW, speed_t)
var speed_t: float = clampf(current_speed / DP.f("walk_speed"), 0.0, 1.0)
var turn_rate: float = DP.f("charge_turn") if charging \
else lerpf(DP.f("walk_turn_fast"), DP.f("walk_turn_slow"), speed_t)
var diff := wrapf(target_angle - facing_angle, -PI, PI)
facing_angle += clampf(diff, -turn_rate * delta, turn_rate * delta)
var top_speed: float = CHARGE_SPEED if charging else WALK_SPEED
var accel: float = CHARGE_ACCEL if charging else WALK_ACCEL
var top_speed: float = DP.f("charge_speed") if charging else DP.f("walk_speed")
var accel: float = DP.f("charge_accel") if charging else DP.f("walk_accel")
current_speed = move_toward(current_speed, top_speed, accel * delta)
else:
var charging := Input.is_key_pressed(KEY_SHIFT)
var decel: float = CHARGE_DECEL if charging else DECELERATION
var decel: float = DP.f("charge_decel") if charging else DP.f("deceleration")
current_speed = move_toward(current_speed, 0.0, decel * delta)
velocity.x = sin(facing_angle) * current_speed
velocity.z = cos(facing_angle) * current_speed
cube_guy.rotation.y = lerp_angle(cube_guy.rotation.y, facing_angle, delta * visual_turn_speed)
cube_guy.rotation.y = lerp_angle(cube_guy.rotation.y, facing_angle,
delta * DP.f("visual_turn_speed"))
move_and_slide()
var charging := Input.is_key_pressed(KEY_SHIFT)
if current_speed > 0.5 and is_on_floor():
_set_dust_state(_DustState.CHARGE if charging else _DustState.WALK)
else:
+5
View File
@@ -67,6 +67,11 @@ charge={
]
}
[autoload]
DP="*res://debug_params.gd"
DebugMenu="*res://debug_menu.gd"
[physics]
3d/physics_engine="Jolt Physics"