Debug menu F1 (use ctrl to lock cam). Design.md doc
This commit is contained in:
@@ -1,3 +1,6 @@
|
|||||||
# Godot 4+ specific ignores
|
# Godot 4+ specific ignores
|
||||||
.godot/
|
.godot/
|
||||||
/android/
|
/android/
|
||||||
|
|
||||||
|
# JetBrains IDE — contains absolute paths (Godot binary, SDK dirs), varies per user
|
||||||
|
.idea/
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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:** ? (15–25 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 |
|
||||||
|
|---|---|---|
|
||||||
|
| 1–3 | Matadors only | — |
|
||||||
|
| 4 | Matadors only | El Presidente |
|
||||||
|
| 5–7 | Matadors + Cowboys introduced | — |
|
||||||
|
| 8 | Mixed (matadors + cowboys) | Sheriff |
|
||||||
|
| 9–11 | 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 1–2s, 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 ~8–15s. 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 ~35–50s, 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
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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 Rider‑optimized 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:
|
||||||
|

|
||||||
|
|
||||||
|
## 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 auto‑set Rider’s 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
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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"
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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 |
@@ -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
|
||||||
@@ -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"
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 ====")
|
||||||
@@ -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
|
||||||
@@ -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
@@ -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()
|
||||||
@@ -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)
|
||||||
@@ -1,19 +1,6 @@
|
|||||||
extends CharacterBody3D
|
extends CharacterBody3D
|
||||||
|
|
||||||
const WALK_SPEED = 6.0
|
# Hoof positions in bull_v10 local space — adjust y/z if puffs don't land on the hooves
|
||||||
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
|
|
||||||
const HOOF_OFFSETS: Array = [
|
const HOOF_OFFSETS: Array = [
|
||||||
Vector3(-0.30, -0.25, -0.50),
|
Vector3(-0.30, -0.25, -0.50),
|
||||||
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),
|
Vector3( 0.30, -0.25, 0.60),
|
||||||
]
|
]
|
||||||
|
|
||||||
@export var visual_turn_speed: float = 12.0
|
|
||||||
|
|
||||||
@onready var camera_pivot: Node3D = $SpringArmPivot
|
@onready var camera_pivot: Node3D = $SpringArmPivot
|
||||||
@onready var cube_guy: Node3D = $bull_v10
|
@onready var cube_guy: Node3D = $bull_v10
|
||||||
|
|
||||||
@@ -37,6 +22,7 @@ var _dust_state: _DustState = _DustState.NONE
|
|||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
_setup_hoof_dust()
|
_setup_hoof_dust()
|
||||||
|
DP.any_changed.connect(_on_dp_changed)
|
||||||
|
|
||||||
|
|
||||||
func _setup_hoof_dust() -> void:
|
func _setup_hoof_dust() -> void:
|
||||||
@@ -46,7 +32,6 @@ func _setup_hoof_dust() -> void:
|
|||||||
mat.vertex_color_use_as_albedo = true
|
mat.vertex_color_use_as_albedo = true
|
||||||
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||||
|
|
||||||
# 4×2 sphere = chunky low-poly diamond shape
|
|
||||||
var sphere := SphereMesh.new()
|
var sphere := SphereMesh.new()
|
||||||
sphere.radius = 0.10
|
sphere.radius = 0.10
|
||||||
sphere.height = 0.20
|
sphere.height = 0.20
|
||||||
@@ -54,12 +39,10 @@ func _setup_hoof_dust() -> void:
|
|||||||
sphere.rings = 2
|
sphere.rings = 2
|
||||||
sphere.material = mat
|
sphere.material = mat
|
||||||
|
|
||||||
# sand colour, fades to transparent over lifetime
|
|
||||||
var ramp := Gradient.new()
|
var ramp := Gradient.new()
|
||||||
ramp.set_color(0, Color(0.80, 0.69, 0.46, 1.0))
|
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))
|
ramp.set_color(1, Color(0.80, 0.69, 0.46, 0.0))
|
||||||
|
|
||||||
# puff blooms up then vanishes
|
|
||||||
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))
|
||||||
scale_curve.add_point(Vector2(0.4, 1.0))
|
scale_curve.add_point(Vector2(0.4, 1.0))
|
||||||
@@ -70,17 +53,17 @@ func _setup_hoof_dust() -> void:
|
|||||||
p.position = offset
|
p.position = offset
|
||||||
p.emitting = false
|
p.emitting = false
|
||||||
p.amount = 12
|
p.amount = 12
|
||||||
p.lifetime = 0.9
|
p.lifetime = DP.f("dust_lifetime")
|
||||||
p.explosiveness = 0.35
|
p.explosiveness = DP.f("dust_explosiveness")
|
||||||
p.randomness = 0.6
|
p.randomness = 0.6
|
||||||
p.local_coords = false
|
p.local_coords = false
|
||||||
p.direction = Vector3(0.0, 1.0, 0.0)
|
p.direction = Vector3(0.0, 1.0, 0.0)
|
||||||
p.spread = 45.0
|
p.spread = DP.f("dust_spread")
|
||||||
p.gravity = Vector3(0.0, -1.5, 0.0)
|
p.gravity = Vector3(0.0, DP.f("dust_gravity_y"), 0.0)
|
||||||
p.initial_velocity_min = 0.8
|
p.initial_velocity_min = DP.f("walk_vel_min")
|
||||||
p.initial_velocity_max = 1.8
|
p.initial_velocity_max = DP.f("walk_vel_max")
|
||||||
p.scale_amount_min = 0.4
|
p.scale_amount_min = DP.f("walk_scale_min")
|
||||||
p.scale_amount_max = 0.7
|
p.scale_amount_max = DP.f("walk_scale_max")
|
||||||
p.mesh = sphere
|
p.mesh = sphere
|
||||||
p.color_ramp = ramp
|
p.color_ramp = ramp
|
||||||
p.scale_amount_curve = scale_curve
|
p.scale_amount_curve = scale_curve
|
||||||
@@ -93,29 +76,42 @@ func _set_dust_state(new_state: _DustState) -> void:
|
|||||||
return
|
return
|
||||||
_dust_state = new_state
|
_dust_state = new_state
|
||||||
for p: CPUParticles3D in _hoof_emitters:
|
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:
|
match new_state:
|
||||||
_DustState.NONE:
|
_DustState.NONE:
|
||||||
p.emitting = false
|
p.emitting = false
|
||||||
_DustState.WALK:
|
_DustState.WALK:
|
||||||
p.scale_amount_min = 0.35
|
p.scale_amount_min = DP.f("walk_scale_min")
|
||||||
p.scale_amount_max = 0.65
|
p.scale_amount_max = DP.f("walk_scale_max")
|
||||||
p.initial_velocity_min = 0.8
|
p.initial_velocity_min = DP.f("walk_vel_min")
|
||||||
p.initial_velocity_max = 1.8
|
p.initial_velocity_max = DP.f("walk_vel_max")
|
||||||
p.emitting = true
|
p.emitting = true
|
||||||
_DustState.CHARGE:
|
_DustState.CHARGE:
|
||||||
p.scale_amount_min = 1.0
|
p.scale_amount_min = DP.f("charge_scale_min")
|
||||||
p.scale_amount_max = 2.2
|
p.scale_amount_max = DP.f("charge_scale_max")
|
||||||
p.initial_velocity_min = 2.8
|
p.initial_velocity_min = DP.f("charge_vel_min")
|
||||||
p.initial_velocity_max = 5.5
|
p.initial_velocity_max = DP.f("charge_vel_max")
|
||||||
p.emitting = true
|
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:
|
func _physics_process(delta: float) -> void:
|
||||||
if not is_on_floor():
|
if not is_on_floor():
|
||||||
velocity += get_gravity() * delta
|
velocity += get_gravity() * delta
|
||||||
|
|
||||||
if Input.is_action_just_pressed("jump") and is_on_floor():
|
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 input_dir := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
|
||||||
var cam_basis := camera_pivot.global_transform.basis
|
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 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 direction := (flat_forward * -input_dir.y + flat_right * input_dir.x).normalized()
|
||||||
|
|
||||||
|
var charging := Input.is_action_pressed(&"charge")
|
||||||
|
|
||||||
if direction:
|
if direction:
|
||||||
var target_angle := atan2(direction.x, direction.z)
|
var target_angle := atan2(direction.x, direction.z)
|
||||||
|
|
||||||
if current_speed < 0.01:
|
if current_speed < 0.01:
|
||||||
facing_angle = target_angle
|
facing_angle = target_angle
|
||||||
|
|
||||||
var charging := Input.is_key_pressed(KEY_SHIFT)
|
var speed_t: float = clampf(current_speed / DP.f("walk_speed"), 0.0, 1.0)
|
||||||
var speed_t: float = clampf(current_speed / WALK_SPEED, 0.0, 1.0)
|
var turn_rate: float = DP.f("charge_turn") if charging \
|
||||||
var turn_rate: float = CHARGE_TURN if charging else lerpf(WALK_TURN_FAST, WALK_TURN_SLOW, speed_t)
|
else lerpf(DP.f("walk_turn_fast"), DP.f("walk_turn_slow"), speed_t)
|
||||||
|
|
||||||
var diff := wrapf(target_angle - facing_angle, -PI, PI)
|
var diff := wrapf(target_angle - facing_angle, -PI, PI)
|
||||||
facing_angle += clampf(diff, -turn_rate * delta, turn_rate * delta)
|
facing_angle += clampf(diff, -turn_rate * delta, turn_rate * delta)
|
||||||
|
|
||||||
var top_speed: float = CHARGE_SPEED if charging else WALK_SPEED
|
var top_speed: float = DP.f("charge_speed") if charging else DP.f("walk_speed")
|
||||||
var accel: float = CHARGE_ACCEL if charging else WALK_ACCEL
|
var accel: float = DP.f("charge_accel") if charging else DP.f("walk_accel")
|
||||||
current_speed = move_toward(current_speed, top_speed, accel * delta)
|
current_speed = move_toward(current_speed, top_speed, accel * delta)
|
||||||
else:
|
else:
|
||||||
var charging := Input.is_key_pressed(KEY_SHIFT)
|
var decel: float = DP.f("charge_decel") if charging else DP.f("deceleration")
|
||||||
var decel: float = CHARGE_DECEL if charging else DECELERATION
|
|
||||||
current_speed = move_toward(current_speed, 0.0, decel * delta)
|
current_speed = move_toward(current_speed, 0.0, decel * delta)
|
||||||
|
|
||||||
velocity.x = sin(facing_angle) * current_speed
|
velocity.x = sin(facing_angle) * current_speed
|
||||||
velocity.z = cos(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()
|
move_and_slide()
|
||||||
|
|
||||||
var charging := Input.is_key_pressed(KEY_SHIFT)
|
|
||||||
if current_speed > 0.5 and is_on_floor():
|
if current_speed > 0.5 and is_on_floor():
|
||||||
_set_dust_state(_DustState.CHARGE if charging else _DustState.WALK)
|
_set_dust_state(_DustState.CHARGE if charging else _DustState.WALK)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -67,6 +67,11 @@ charge={
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[autoload]
|
||||||
|
|
||||||
|
DP="*res://debug_params.gd"
|
||||||
|
DebugMenu="*res://debug_menu.gd"
|
||||||
|
|
||||||
[physics]
|
[physics]
|
||||||
|
|
||||||
3d/physics_engine="Jolt Physics"
|
3d/physics_engine="Jolt Physics"
|
||||||
|
|||||||
Reference in New Issue
Block a user