Compare commits
7
Commits
main
..
69f2028ce3
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
69f2028ce3 | ||
|
|
e65c48c1e3 | ||
|
|
a2331c3b94 | ||
|
|
5e2c395a88 | ||
|
|
3f5d30c490 | ||
|
|
a237d0c887 | ||
|
|
b3836e266f |
+41
-1
@@ -1,2 +1,42 @@
|
|||||||
# Normalize EOL for all files that Git considers text files.
|
# ─── Binary assets → LFS ───────────────────────────────────────────
|
||||||
|
*.glb filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.gltf filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.fbx filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.obj filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|
||||||
|
*.png filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.jpg filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.jpeg filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.webp filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.exr filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.hdr filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.svg -filter -diff -merge text
|
||||||
|
|
||||||
|
*.ogg filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.wav filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.mp3 filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|
||||||
|
*.ttf filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.otf filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|
||||||
|
# Godot binary resources (collision shapes, baked data)
|
||||||
|
*.res filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.scn filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|
||||||
|
# ─── Line endings ──────────────────────────────────────────────────
|
||||||
* text=auto eol=lf
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
*.gd text eol=lf
|
||||||
|
*.tscn text eol=lf
|
||||||
|
*.tres text eol=lf
|
||||||
|
*.godot text eol=lf
|
||||||
|
*.cfg text eol=lf
|
||||||
|
*.import text eol=lf
|
||||||
|
*.uid text eol=lf
|
||||||
|
*.json text eol=lf
|
||||||
|
*.md text eol=lf
|
||||||
|
*.sh text eol=lf
|
||||||
|
|
||||||
|
# ─── Don't diff these ──────────────────────────────────────────────
|
||||||
|
*.import -diff
|
||||||
|
*.uid -diff
|
||||||
|
|||||||
+43
-3
@@ -1,3 +1,43 @@
|
|||||||
# Godot 4+ specific ignores
|
# ─── Godot ─────────────────────────────────────────────────────────
|
||||||
.godot/
|
# Import cache. Regenerates on open. NEVER commit.
|
||||||
/android/
|
game/.godot/
|
||||||
|
|
||||||
|
# Orphaned temp files from interrupted imports
|
||||||
|
*.import-*
|
||||||
|
|
||||||
|
# Android build template
|
||||||
|
game/android/
|
||||||
|
|
||||||
|
# ─── Build output ──────────────────────────────────────────────────
|
||||||
|
/export/
|
||||||
|
/build/
|
||||||
|
*.pck
|
||||||
|
*.exe
|
||||||
|
*.x86_64
|
||||||
|
*.dmg
|
||||||
|
*.apk
|
||||||
|
*.aab
|
||||||
|
|
||||||
|
# ─── DCC backups (should live in Nextcloud, not here) ──────────────
|
||||||
|
*.blend[0-9]
|
||||||
|
*.blend[0-9][0-9]
|
||||||
|
*.kra~
|
||||||
|
*.krz
|
||||||
|
*.psb
|
||||||
|
*.tmp
|
||||||
|
|
||||||
|
# ─── Editors ───────────────────────────────────────────────────────
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# ─── OS ────────────────────────────────────────────────────────────
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
desktop.ini
|
||||||
|
|
||||||
|
# ─── Nextcloud sync artifacts ──────────────────────────────────────
|
||||||
|
.sync_*.db
|
||||||
|
*.~lock*
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
# DartGame
|
||||||
|
|
||||||
|
A 3D darts game where the core loop is **dragging modifiers onto board sectors** to create
|
||||||
|
compounding scoring interactions. Balatro's joker system, applied to a dartboard.
|
||||||
|
|
||||||
|
Built in **Godot 4**. Modding is a first-class concern, not an afterthought — our own
|
||||||
|
content ships through the same loader community mods will use.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Install `git-lfs` BEFORE you clone
|
||||||
|
|
||||||
|
Clone without it and you'll get **130-byte text pointers** where your textures and models
|
||||||
|
should be. Godot will throw import errors that look completely baffling and you will lose
|
||||||
|
an hour figuring out why.
|
||||||
|
|
||||||
|
**NixOS** — `configuration.nix`:
|
||||||
|
```nix
|
||||||
|
environment.systemPackages = with pkgs; [ git git-lfs ];
|
||||||
|
```
|
||||||
|
|
||||||
|
Or declaratively via Home Manager (also writes the LFS filter config, so a fresh machine is
|
||||||
|
correct with no imperative step):
|
||||||
|
```nix
|
||||||
|
programs.git = {
|
||||||
|
enable = true;
|
||||||
|
lfs.enable = true;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Debian/Ubuntu** — `sudo apt install git-lfs`
|
||||||
|
**macOS** — `brew install git-lfs`
|
||||||
|
|
||||||
|
Then, once per machine:
|
||||||
|
```bash
|
||||||
|
git lfs install
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <gitea-url>/DartGame.git
|
||||||
|
cd DartGame
|
||||||
|
git lfs pull
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verify LFS worked.** Open `game/core/board/dartboard.glb` in a text editor. If it's
|
||||||
|
readable text starting with `version https://git-lfs...`, LFS did **not** run. Stop and fix
|
||||||
|
it before opening the project.
|
||||||
|
|
||||||
|
### Tooling
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# GDScript formatter + linter
|
||||||
|
nix-shell -p gdtoolkit_4 # or: pipx install "gdtoolkit==4.*"
|
||||||
|
|
||||||
|
# Pre-commit hooks
|
||||||
|
pre-commit install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Open the project
|
||||||
|
|
||||||
|
Point Godot 4 at `game/project.godot`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Where things live
|
||||||
|
|
||||||
|
| | Nextcloud | This repo |
|
||||||
|
|---|---|---|
|
||||||
|
| `.blend`, `.kra`, `.psd` | ✅ | ❌ **never** |
|
||||||
|
| `.glb`, `.png` (exported) | ❌ | ✅ (LFS) |
|
||||||
|
| Code, scenes, `.tres` | ❌ | ✅ |
|
||||||
|
| Moodboards, references | ✅ | ❌ |
|
||||||
|
|
||||||
|
Source files live in the **company Nextcloud share**, not here. They churn constantly, never
|
||||||
|
merge, and 99% of their revisions have no corresponding code change — they'd bloat LFS
|
||||||
|
history forever, on every clone.
|
||||||
|
|
||||||
|
Exported artifacts **do** live here, because they must version in lockstep with the code
|
||||||
|
that references them. If a UV layout changes and a script needs updating, those belong in
|
||||||
|
one commit.
|
||||||
|
|
||||||
|
`SOURCES.md` maps each repo artifact back to its Nextcloud source.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
game/
|
||||||
|
├── core/ ← THE ENGINE. Contains zero game content. Ever.
|
||||||
|
│ ├── board/ dartboard scene, mesh, sector logic
|
||||||
|
│ ├── dart/ dart scene, physics
|
||||||
|
│ ├── scoring/ ScorePacket pipeline
|
||||||
|
│ ├── game/ GameContext ← the mod API
|
||||||
|
│ ├── modding/ loader, registry, base classes, primitives
|
||||||
|
│ ├── ui/ modifier tray, drag-and-drop
|
||||||
|
│ └── debug/ hit_simulator.tscn ← you'll use this daily
|
||||||
|
├── content/official/ ← OUR CONTENT, loaded as a mod like any other
|
||||||
|
│ ├── skins/
|
||||||
|
│ ├── modifiers/
|
||||||
|
│ └── modes/
|
||||||
|
└── shared/ fonts, shaders
|
||||||
|
```
|
||||||
|
|
||||||
|
### The two rules
|
||||||
|
|
||||||
|
**1. Nothing in `core/` knows about any specific content.**
|
||||||
|
About to write `if modifier.id == "mjolnir"` in `core/`? Stop — you need a new primitive.
|
||||||
|
|
||||||
|
**2. `content/official/` must be deletable.**
|
||||||
|
Delete it, and the game boots to an empty state without crashing. That's the proof the mod
|
||||||
|
pipeline is real. If it can't, we don't have a mod system — we have a plugin folder.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Docs
|
||||||
|
|
||||||
|
Full documentation lives in the Obsidian vault at
|
||||||
|
`Nextcloud/DartGame/documentation/`. Start with **Build Plan**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
**Branches:** `feat/`, `fix/`, `chore/` off `main`. `main` is protected and always runnable.
|
||||||
|
|
||||||
|
**Commits:** Conventional Commits.
|
||||||
|
```
|
||||||
|
feat(modding): add ModRegistry namespaced lookup
|
||||||
|
fix(scoring): ScorePacket.total() ignored final_override
|
||||||
|
```
|
||||||
|
|
||||||
|
**Before you open a PR:**
|
||||||
|
- [ ] `gdformat --check game/` passes
|
||||||
|
- [ ] `gdlint game/` passes
|
||||||
|
- [ ] Nothing in `core/` references a specific content ID
|
||||||
|
- [ ] New content is one self-contained folder
|
||||||
|
- [ ] If an asset changed: source updated in Nextcloud, `SOURCES.md` current
|
||||||
|
|
||||||
|
### ⚠️ Moving files
|
||||||
|
|
||||||
|
**Commit first.** Godot's refactoring is fragile. Move files from **inside the Godot
|
||||||
|
editor's FileSystem dock**, never your file manager, so references get fixed up.
|
||||||
|
|
||||||
|
If things break after a move or a branch switch:
|
||||||
|
```bash
|
||||||
|
rm -rf game/.godot/
|
||||||
|
```
|
||||||
|
then Project → Reload Current Project. If it's worse than that, `git restore .` — which only
|
||||||
|
works if you committed first.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
**Pre-alpha.** Currently in Phase 0 (repo hygiene). See [`docs/build_plan.md`](docs/build_plan.md).
|
||||||
|
|||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
# Sources
|
||||||
|
|
||||||
|
Every binary artifact in this repo was exported from a source file in Nextcloud. This maps
|
||||||
|
one to the other.
|
||||||
|
|
||||||
|
**Nextcloud share:** `DartGame/sources/`
|
||||||
|
|
||||||
|
> ⚠️ **Keep this current.** It is the only thing linking the two halves of the project. A
|
||||||
|
> programmer looking at `dartboard.glb` has no other way to find out where it came from.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Models
|
||||||
|
|
||||||
|
| Repo artifact | Nextcloud source | Last export |
|
||||||
|
|---|---|---|
|
||||||
|
| `game/core/board/dartboard.glb` | `sources/dartboard/dartboard.blend` | 2026-07-11 |
|
||||||
|
| `game/core/dart/dart.glb` | `sources/dart/dart.blend` | 2026-07-11 |
|
||||||
|
|
||||||
|
## Skins — bullseye
|
||||||
|
|
||||||
|
| Repo artifact | Nextcloud source | Last export |
|
||||||
|
|---|---|---|
|
||||||
|
| `game/content/official/skins/bullseye/neon_bulls.png` | `sources/dartboard/textures/neon_bulls.kra` | 2026-07-11 |
|
||||||
|
|
||||||
|
## Skins — sectors
|
||||||
|
|
||||||
|
| Repo artifact | Nextcloud source | Last export |
|
||||||
|
|---|---|---|
|
||||||
|
| `game/content/official/skins/sectors/neon_hiero_sector.png` | `sources/dartboard/textures/neon_hiero_sector.kra` | 2026-07-11 |
|
||||||
|
|
||||||
|
## Skins — flat tire
|
||||||
|
|
||||||
|
| Repo artifact | Nextcloud source | Last export |
|
||||||
|
|---|---|---|
|
||||||
|
| `game/content/official/skins/flat_tire/fire.png` | `sources/dartboard/textures/flat_tire.kra` | 2026-07-11 |
|
||||||
|
|
||||||
|
## Skins — flights
|
||||||
|
|
||||||
|
| Repo artifact | Nextcloud source | Last export |
|
||||||
|
|---|---|---|
|
||||||
|
| `game/content/official/skins/flights/default.png` | `sources/dart/textures/wings_default.kra` | 2026-07-11 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fallback textures
|
||||||
|
|
||||||
|
Single default textures that ship with the mesh so the editor isn't pink. Not skins — these
|
||||||
|
live in `core/`, not `content/`.
|
||||||
|
|
||||||
|
| Repo artifact | Nextcloud source |
|
||||||
|
|---|---|
|
||||||
|
| `game/core/board/dartboard_default.png` | `sources/dartboard/textures/cork.kra` |
|
||||||
|
| `game/core/dart/dart_default.png` | `sources/dart/textures/dart_default.kra` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## When you re-export
|
||||||
|
|
||||||
|
1. Export from the Nextcloud source
|
||||||
|
2. Drop the artifact into the repo path above
|
||||||
|
3. **Update the "Last export" date in this file**
|
||||||
|
4. Commit the artifact + this file together
|
||||||
|
|
||||||
|
If a row is missing, the artifact has no traceable source. Add it, or find out why it exists.
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
[gd_scene format=3 uid="uid://bswbcehkkqbk0"]
|
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://8kk531f88oqw" path="res://Scenes/DartPhysics.gd" id="1_s1kro"]
|
|
||||||
[ext_resource type="PackedScene" uid="uid://cjq45anerhxnk" path="res://models/Dart.glb" id="2_mlvfh"]
|
|
||||||
|
|
||||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_xxti7"]
|
|
||||||
radius = 0.27441406
|
|
||||||
height = 2.3862305
|
|
||||||
|
|
||||||
[node name="Dart" type="Node3D" unique_id=367982453]
|
|
||||||
|
|
||||||
[node name="RigidBody3D" type="RigidBody3D" parent="." unique_id=1993396351]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1.0000001, 0, 0, 0, 1.0000001, 0, 0, 0)
|
|
||||||
center_of_mass_mode = 1
|
|
||||||
script = ExtResource("1_s1kro")
|
|
||||||
angle_of_plane = -25.0
|
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="RigidBody3D" unique_id=1030334431]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.10033989, 0)
|
|
||||||
shape = SubResource("CapsuleShape3D_xxti7")
|
|
||||||
|
|
||||||
[node name="Dart2" parent="RigidBody3D" unique_id=1655536183 instance=ExtResource("2_mlvfh")]
|
|
||||||
transform = Transform3D(0.4, 0, 0, 0, 0.39999995, 0, 0, 0, 0.39999995, 0, -1.4526575, 0.015561234)
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
extends RigidBody3D
|
|
||||||
|
|
||||||
class_name Dart
|
|
||||||
|
|
||||||
#states
|
|
||||||
var in_flight: bool = false
|
|
||||||
var dragging: bool = false
|
|
||||||
|
|
||||||
#drag
|
|
||||||
var drag_plane: Plane
|
|
||||||
var drag_offset: Vector3 = Vector3.ZERO
|
|
||||||
var target_pos: Vector3 = Vector3.ZERO
|
|
||||||
|
|
||||||
# velocity tracking for release throw
|
|
||||||
var position_history: Array[Vector3] = []
|
|
||||||
var time_history: Array[float] = []
|
|
||||||
const HISTORY_LENGTH = 6
|
|
||||||
|
|
||||||
# drag "stiffness" tuning
|
|
||||||
@export var drag_follow_speed: float = 20.0 # how fast it chases the mouse
|
|
||||||
@export var max_drag_speed: float = 42.0 # clamp so it doesn't go insane
|
|
||||||
@export_range(-180,180) var angle_of_plane: float = 15.0
|
|
||||||
@export var throw_force_multiplier: float = 0.8 # scale raw throw velocity down if it's too fast
|
|
||||||
@export var max_throw_speed: float = 42.0 # hard cap so fast flicks don't kill the arc
|
|
||||||
@export var flight_gravity_scale: float = 4.0 # higher = more visible arc
|
|
||||||
@export var nose_align_speed: float = 1.0 # how fast dart rotates to face velocity
|
|
||||||
@export var dart_forward_axis: Vector3 = Vector3.FORWARD
|
|
||||||
|
|
||||||
func _ready():
|
|
||||||
freeze_mode = RigidBody3D.FREEZE_MODE_STATIC
|
|
||||||
freeze = true
|
|
||||||
|
|
||||||
func start_drag(camera: Camera3D, mouse_pos: Vector2, hit_point: Vector3):
|
|
||||||
dragging = true
|
|
||||||
in_flight = false
|
|
||||||
freeze = false
|
|
||||||
|
|
||||||
# keep it physics-active — don't freeze
|
|
||||||
gravity_scale = 0.0 # optional: ignore gravity while held so it doesn't sag
|
|
||||||
linear_damp = 5.0 # optional: a bit of damping for stability while dragged
|
|
||||||
angular_damp = 10.0
|
|
||||||
|
|
||||||
#drag_plane = Plane(camera.global_transform.basis.z, hit_point)
|
|
||||||
var base_normal = camera.global_transform.basis.z
|
|
||||||
var tilt = base_normal.rotated(camera.global_transform.basis.x, deg_to_rad(angle_of_plane)) # tilt adjustable around camera's right axis
|
|
||||||
drag_plane = Plane(tilt.normalized(), hit_point)
|
|
||||||
drag_offset = global_position - hit_point
|
|
||||||
|
|
||||||
target_pos = global_position #fixes a bug where dart drops until mouse is moved.
|
|
||||||
|
|
||||||
position_history.clear()
|
|
||||||
time_history.clear()
|
|
||||||
|
|
||||||
func update_drag(camera: Camera3D, mouse_pos: Vector2):
|
|
||||||
if not dragging:
|
|
||||||
return
|
|
||||||
|
|
||||||
var ray_origin = camera.project_ray_origin(mouse_pos)
|
|
||||||
var ray_dir = camera.project_ray_normal(mouse_pos)
|
|
||||||
|
|
||||||
var intersection = drag_plane.intersects_ray(ray_origin, ray_dir)
|
|
||||||
if intersection:
|
|
||||||
target_pos = intersection + drag_offset
|
|
||||||
|
|
||||||
func _align_dart_to_velocity(delta: float):
|
|
||||||
if linear_velocity.length() < 0.5:
|
|
||||||
return
|
|
||||||
|
|
||||||
var vel_dir = linear_velocity.normalized()
|
|
||||||
var current_forward = global_transform.basis * dart_forward_axis
|
|
||||||
var rotation_axis = current_forward.cross(vel_dir)
|
|
||||||
|
|
||||||
if rotation_axis.length() < 0.001:
|
|
||||||
return
|
|
||||||
|
|
||||||
rotation_axis = rotation_axis.normalized()
|
|
||||||
var angle = current_forward.angle_to(vel_dir)
|
|
||||||
var rotate_amount = min(angle, nose_align_speed * delta)
|
|
||||||
|
|
||||||
global_transform.basis = global_transform.basis.rotated(rotation_axis, rotate_amount)
|
|
||||||
global_transform.basis = global_transform.basis.orthonormalized()
|
|
||||||
|
|
||||||
func _physics_process(delta):
|
|
||||||
if dragging:
|
|
||||||
# chase the target position with velocity instead of teleporting
|
|
||||||
var to_target = target_pos - global_position
|
|
||||||
var desired_velocity = to_target * drag_follow_speed
|
|
||||||
|
|
||||||
if desired_velocity.length() > max_drag_speed:
|
|
||||||
desired_velocity = desired_velocity.normalized() * max_drag_speed
|
|
||||||
|
|
||||||
linear_velocity = desired_velocity
|
|
||||||
angular_velocity = angular_velocity.lerp(Vector3.ZERO, 0.2) # optional: damp spin while dragging
|
|
||||||
|
|
||||||
# track actual position for momentum calc on release
|
|
||||||
position_history.append(global_position)
|
|
||||||
time_history.append(Time.get_ticks_msec() / 1000.0)
|
|
||||||
if position_history.size() > HISTORY_LENGTH:
|
|
||||||
position_history.pop_front()
|
|
||||||
time_history.pop_front()
|
|
||||||
elif in_flight:
|
|
||||||
# Debug: confirm Y velocity is dropping (becoming negative) each frame
|
|
||||||
print("local +X world dir: ", global_transform.basis.x)
|
|
||||||
print("local +Y world dir: ", global_transform.basis.y)
|
|
||||||
print("local +Z world dir: ", global_transform.basis.z)
|
|
||||||
print("velocity dir: ", linear_velocity.normalized())
|
|
||||||
|
|
||||||
# Align dart nose to face velocity direction (tip points the way it's flying)
|
|
||||||
if linear_velocity.length() > 0.5:
|
|
||||||
_align_dart_to_velocity(delta)
|
|
||||||
else:
|
|
||||||
in_flight = false
|
|
||||||
angular_velocity = Vector3.ZERO
|
|
||||||
# If your dart tip points along +Z, use global_position + linear_velocity
|
|
||||||
# If it points along -Z, negate the direction below
|
|
||||||
var look_target = global_position + linear_velocity.normalized()
|
|
||||||
var up_ref = Vector3.UP
|
|
||||||
|
|
||||||
# Avoid gimbal lock when flying straight up/down
|
|
||||||
if abs(linear_velocity.normalized().dot(Vector3.UP)) > 0.99:
|
|
||||||
up_ref = Vector3.FORWARD
|
|
||||||
|
|
||||||
var target_basis = Basis.looking_at(linear_velocity.normalized(), up_ref)
|
|
||||||
global_transform.basis = global_transform.basis.slerp(
|
|
||||||
target_basis, nose_align_speed * delta
|
|
||||||
)
|
|
||||||
|
|
||||||
func release_drag():
|
|
||||||
if not dragging:
|
|
||||||
return
|
|
||||||
dragging = false
|
|
||||||
in_flight = true
|
|
||||||
|
|
||||||
gravity_scale = flight_gravity_scale
|
|
||||||
angular_damp = 0.0
|
|
||||||
linear_damp = 0.0 # reset to whatever your normal dart damping is
|
|
||||||
|
|
||||||
var velocity = calculate_throw_velocity() * throw_force_multiplier
|
|
||||||
if velocity.length() > max_throw_speed:
|
|
||||||
velocity = velocity.normalized() * max_throw_speed
|
|
||||||
linear_velocity = velocity
|
|
||||||
# angular_velocity left as-is from physics, or compute your own spin here
|
|
||||||
|
|
||||||
print("throw speed: %.2f" % velocity.length())
|
|
||||||
print("gravity_scale: ", gravity_scale)
|
|
||||||
print("linear_damp: ", linear_damp)
|
|
||||||
|
|
||||||
func calculate_throw_velocity() -> Vector3:
|
|
||||||
if position_history.size() < 2:
|
|
||||||
return linear_velocity # fallback to whatever physics already had
|
|
||||||
|
|
||||||
var start_pos = position_history[0]
|
|
||||||
var end_pos = position_history[position_history.size() - 1]
|
|
||||||
var start_time = time_history[0]
|
|
||||||
var end_time = time_history[time_history.size() - 1]
|
|
||||||
|
|
||||||
var dt = end_time - start_time
|
|
||||||
if dt <= 0.0:
|
|
||||||
return linear_velocity
|
|
||||||
|
|
||||||
return (end_pos - start_pos) / dt
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://8kk531f88oqw
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
extends Camera3D
|
|
||||||
|
|
||||||
var held_dart: Dart = null
|
|
||||||
|
|
||||||
func _unhandled_input(event):
|
|
||||||
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
|
|
||||||
if event.pressed:
|
|
||||||
try_grab_dart(event.position)
|
|
||||||
else:
|
|
||||||
if held_dart:
|
|
||||||
held_dart.release_drag()
|
|
||||||
held_dart = null
|
|
||||||
elif event is InputEventMouseMotion and held_dart:
|
|
||||||
held_dart.update_drag(self, event.position)
|
|
||||||
|
|
||||||
func try_grab_dart(mouse_pos: Vector2):
|
|
||||||
var ray_origin = project_ray_origin(mouse_pos)
|
|
||||||
var ray_dir = project_ray_normal(mouse_pos)
|
|
||||||
var ray_end = ray_origin + ray_dir * 1000.0
|
|
||||||
|
|
||||||
var space_state = get_world_3d().direct_space_state
|
|
||||||
var query = PhysicsRayQueryParameters3D.create(ray_origin, ray_end)
|
|
||||||
var result = space_state.intersect_ray(query)
|
|
||||||
|
|
||||||
if result and result.collider is Dart:
|
|
||||||
held_dart = result.collider
|
|
||||||
held_dart.start_drag(self, mouse_pos, result.position)
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
extends Node3D
|
|
||||||
|
|
||||||
func _unhandled_input(event):
|
|
||||||
if event.is_action_pressed("restart"):
|
|
||||||
get_tree().reload_current_scene()
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
[gd_scene format=3 uid="uid://doh7p84eej78c"]
|
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://geqmah8kbko0" path="res://Scenes/main.gd" id="1_8gbba"]
|
|
||||||
[ext_resource type="Script" uid="uid://ds3uiri3oc05v" path="res://Scenes/camera_3d.gd" id="1_bo1nx"]
|
|
||||||
[ext_resource type="PackedScene" uid="uid://bswbcehkkqbk0" path="res://Scenes/Dart.tscn" id="1_jjgbg"]
|
|
||||||
|
|
||||||
[sub_resource type="BoxShape3D" id="BoxShape3D_qu08v"]
|
|
||||||
size = Vector3(1, 0.01, 1)
|
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_6q475"]
|
|
||||||
albedo_color = Color(0, 0.42745098, 0.05490196, 1)
|
|
||||||
|
|
||||||
[sub_resource type="BoxMesh" id="BoxMesh_xxti7"]
|
|
||||||
material = SubResource("StandardMaterial3D_6q475")
|
|
||||||
size = Vector3(1, 0.01, 1)
|
|
||||||
|
|
||||||
[node name="Main" type="Node3D" unique_id=819165214]
|
|
||||||
script = ExtResource("1_8gbba")
|
|
||||||
|
|
||||||
[node name="Camera3D" type="Camera3D" parent="." unique_id=1570426839]
|
|
||||||
transform = Transform3D(-1, 2.4096988e-08, -8.4036174e-08, 0, 0.9612618, 0.2756374, 8.742278e-08, 0.2756374, -0.9612618, 0, 5.0674033, -6.36445)
|
|
||||||
fov = 90.0
|
|
||||||
script = ExtResource("1_bo1nx")
|
|
||||||
|
|
||||||
[node name="Dart" parent="." unique_id=367982453 instance=ExtResource("1_jjgbg")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 0.8363807, -0.5481493, 0, 0.5481492, 0.8363808, 0, 4.3246593, 0)
|
|
||||||
|
|
||||||
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1891469178]
|
|
||||||
transform = Transform3D(-0.789482, 0.24223976, -0.56394875, -0.56595325, 0.06825608, 0.821607, 0.23751883, 0.9678125, 0.083209395, -46.816067, 15.942318, -50.370865)
|
|
||||||
|
|
||||||
[node name="StaticBody3D" type="StaticBody3D" parent="." unique_id=889230389]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 63.39778)
|
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody3D" unique_id=229129629]
|
|
||||||
transform = Transform3D(100, 0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0)
|
|
||||||
shape = SubResource("BoxShape3D_qu08v")
|
|
||||||
|
|
||||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="StaticBody3D/CollisionShape3D" unique_id=849495197]
|
|
||||||
mesh = SubResource("BoxMesh_xxti7")
|
|
||||||
skeleton = NodePath("")
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
extends Camera3D
|
|
||||||
|
|
||||||
const RAY_LENGTH = 1000
|
|
||||||
|
|
||||||
func ray_cast():
|
|
||||||
var space_state = get_world_3d().direct_space_state
|
|
||||||
var cam = $"."
|
|
||||||
var mousepos = get_viewport().get_mouse_position()
|
|
||||||
var origin = cam.project_ray_origin(mousepos)
|
|
||||||
var end = origin + cam.project_ray_normal(mousepos) * RAY_LENGTH
|
|
||||||
var query = PhysicsRayQueryParameters3D.create(origin,end)
|
|
||||||
query.collide_with_areas = true
|
|
||||||
var result = space_state.intersect_ray(query)
|
|
||||||
result = result.get("collider")
|
|
||||||
return result
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://ckbdoamrm313u
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
extends Node3D
|
|
||||||
|
|
||||||
var draggingCollider
|
|
||||||
var mousePosition
|
|
||||||
var doDrag = false
|
|
||||||
|
|
||||||
func _input(event):
|
|
||||||
var intersect
|
|
||||||
|
|
||||||
if event is InputEventMouse:
|
|
||||||
intersect = get_mouse_intersect(event.position)
|
|
||||||
if intersect: mousePosition = intersect.position
|
|
||||||
#snap on collider
|
|
||||||
#if intersect: mousePosition = intersect.collider.global_position
|
|
||||||
|
|
||||||
if event is InputEventMouseButton:
|
|
||||||
var leftButtonPressed = event.button_index == MOUSE_BUTTON_LEFT && event.pressed
|
|
||||||
var leftButtonReleased = event.button_index == MOUSE_BUTTON_LEFT && !event.pressed
|
|
||||||
|
|
||||||
if leftButtonReleased:
|
|
||||||
doDrag = false
|
|
||||||
drag_and_drop(intersect)
|
|
||||||
elif leftButtonPressed:
|
|
||||||
doDrag = true
|
|
||||||
drag_and_drop(intersect)
|
|
||||||
|
|
||||||
|
|
||||||
func _process(delta):
|
|
||||||
if draggingCollider:
|
|
||||||
draggingCollider.global_position = mousePosition
|
|
||||||
|
|
||||||
func drag_and_drop(intersect):
|
|
||||||
var canMove = intersect.collider in get_tree().get_nodes_in_group("Dart")
|
|
||||||
if !draggingCollider && doDrag && canMove:
|
|
||||||
draggingCollider = intersect.collider
|
|
||||||
elif draggingCollider:
|
|
||||||
draggingCollider = null
|
|
||||||
|
|
||||||
func get_mouse_intersect(mousePosition):
|
|
||||||
var currentCamera = get_viewport().get_camera_3d()
|
|
||||||
var params = PhysicsRayQueryParameters3D.new()
|
|
||||||
|
|
||||||
params.from = currentCamera.project_ray_origin(mousePosition)
|
|
||||||
params.to = currentCamera.project_position(mousePosition, 1000)
|
|
||||||
if draggingCollider: params.exclude = [draggingCollider]
|
|
||||||
|
|
||||||
var worldspace = get_world_3d().direct_space_state
|
|
||||||
var result = worldspace.intersect_ray(params)
|
|
||||||
|
|
||||||
return result
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
extends MeshInstance3D
|
|
||||||
|
|
||||||
@onready var camera = get_node("Camera3D")
|
|
||||||
@onready var area = get_node("../../../Dart")
|
|
||||||
|
|
||||||
func _input(event):
|
|
||||||
if Input.is_action_just_pressed("leftClick"):
|
|
||||||
print(camera.ray_cast().position)
|
|
||||||
|
|
||||||
if camera.ray_cast() != null and Input.is_action_pressed("midleclic"):
|
|
||||||
var mousepos = get_viewport().get_mouse_position()
|
|
||||||
var origin = camera.projectray_origin(mousepos)
|
|
||||||
var end = camera.project_ray_normal(mousepos)
|
|
||||||
var depth = origin.distence_to(area.global_position)
|
|
||||||
var final_position = origin + end * depth
|
|
||||||
area.global_position=final_position
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://cc5yy1yc3j2uu
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"id": "official",
|
||||||
|
"display_name": "DartGame",
|
||||||
|
"author": "Ced Games",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"api_version": 1,
|
||||||
|
"depends": [],
|
||||||
|
"description": "The base game content."
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://bayaus0tgltb3"
|
||||||
|
path="res://.godot/imported/neon_hiero.png-401e600b4e3a81c5e49b654215a0e850.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://content/official/skins/bulls/neon_hiero.png"
|
||||||
|
dest_files=["res://.godot/imported/neon_hiero.png-401e600b4e3a81c5e49b654215a0e850.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
Binary file not shown.
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://b672fbyuxxktw"
|
||||||
|
path="res://.godot/imported/ocean_fire.png-b1f0d5fea08640de091cb5cf724309e6.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://content/official/skins/flat_tire/ocean_fire.png"
|
||||||
|
dest_files=["res://.godot/imported/ocean_fire.png-b1f0d5fea08640de091cb5cf724309e6.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
Binary file not shown.
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://btg3hosfx1sdg"
|
||||||
|
path="res://.godot/imported/toon_purple.png-f1a192acdf1c762039bd65e86672f7e4.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://content/official/skins/flat_tire/toon_purple.png"
|
||||||
|
dest_files=["res://.godot/imported/toon_purple.png-f1a192acdf1c762039bd65e86672f7e4.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
Binary file not shown.
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://brqn2ve8pm3i8"
|
||||||
|
path="res://.godot/imported/neon_hiero_black.png-7709dfdedde53a261eb17c86736a737f.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://content/official/skins/sectors/neon_hiero_black.png"
|
||||||
|
dest_files=["res://.godot/imported/neon_hiero_black.png-7709dfdedde53a261eb17c86736a737f.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
Binary file not shown.
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://bataamlsyyup6"
|
||||||
|
path="res://.godot/imported/neon_hiero_white.png-c93ce5ab952a3d2618c297fc1cae11f4.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://content/official/skins/sectors/neon_hiero_white.png"
|
||||||
|
dest_files=["res://.godot/imported/neon_hiero_white.png-c93ce5ab952a3d2618c297fc1cae11f4.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,173 @@
|
|||||||
|
class_name Dartboard
|
||||||
|
extends StaticBody3D
|
||||||
|
|
||||||
|
## The board. 21 material slots: sector_1 .. sector_20, plus "bulls".
|
||||||
|
##
|
||||||
|
## Owns the face -> surface -> slot mapping, which is what makes hit detection
|
||||||
|
## survive the board moving, rotating, or having its sectors remapped. Ask the
|
||||||
|
## geometry which sector was hit; don't calculate it from an angle.
|
||||||
|
|
||||||
|
## Every slot on the board, in the order Blender exported them (order doesn't
|
||||||
|
## matter — we index by name).
|
||||||
|
const BULL_SLOT := &"bulls"
|
||||||
|
|
||||||
|
@onready var mesh_instance: MeshInstance3D = $Model/dartboard
|
||||||
|
|
||||||
|
## StringName -> surface index
|
||||||
|
var _slot_to_surface: Dictionary = {}
|
||||||
|
## global face index -> StringName
|
||||||
|
var _face_to_slot: Dictionary = {}
|
||||||
|
|
||||||
|
var _outer_radius: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_index_slots()
|
||||||
|
_index_faces()
|
||||||
|
_cache_radius()
|
||||||
|
|
||||||
|
|
||||||
|
# --- Indexing -----------------------------------------------------------
|
||||||
|
|
||||||
|
func _index_slots() -> void:
|
||||||
|
var mesh := mesh_instance.mesh
|
||||||
|
if mesh == null:
|
||||||
|
push_error("Dartboard has no mesh.")
|
||||||
|
return
|
||||||
|
|
||||||
|
for i in mesh.get_surface_count():
|
||||||
|
var mat := mesh.surface_get_material(i)
|
||||||
|
if mat == null:
|
||||||
|
push_warning("Surface %d has no material — can't be skinned or hit." % i)
|
||||||
|
continue
|
||||||
|
|
||||||
|
var slot := StringName(mat.resource_name)
|
||||||
|
if slot == &"":
|
||||||
|
push_warning("Surface %d's material has no name." % i)
|
||||||
|
continue
|
||||||
|
|
||||||
|
_slot_to_surface[slot] = i
|
||||||
|
|
||||||
|
print("Dartboard: indexed %d slots." % _slot_to_surface.size())
|
||||||
|
if _slot_to_surface.size() != 21:
|
||||||
|
push_warning("Expected 21 slots (20 sectors + bulls), got %d." % _slot_to_surface.size())
|
||||||
|
|
||||||
|
|
||||||
|
## Build a global-face-index -> slot map.
|
||||||
|
##
|
||||||
|
## The physics engine reports face_index as an index into the WHOLE trimesh,
|
||||||
|
## but the mesh stores faces per-surface. So we walk the surfaces in order,
|
||||||
|
## accumulating a running face count, and record which range belongs to which.
|
||||||
|
##
|
||||||
|
## This assumes the ConcavePolygonShape3D was built from the mesh with surfaces
|
||||||
|
## in the same order — which is what create_trimesh_shape() does.
|
||||||
|
func _index_faces() -> void:
|
||||||
|
var mesh := mesh_instance.mesh
|
||||||
|
if mesh == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
var running := 0
|
||||||
|
for i in mesh.get_surface_count():
|
||||||
|
var arrays := mesh.surface_get_arrays(i)
|
||||||
|
if arrays.is_empty():
|
||||||
|
continue
|
||||||
|
|
||||||
|
var indices: PackedInt32Array = arrays[Mesh.ARRAY_INDEX]
|
||||||
|
var verts: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
||||||
|
|
||||||
|
# Triangle count: from the index buffer if present, else from vertices.
|
||||||
|
var tri_count := (indices.size() / 3) if indices.size() > 0 else (verts.size() / 3)
|
||||||
|
|
||||||
|
var mat := mesh.surface_get_material(i)
|
||||||
|
var slot := StringName(mat.resource_name) if mat else &""
|
||||||
|
|
||||||
|
for f in tri_count:
|
||||||
|
_face_to_slot[running + f] = slot
|
||||||
|
|
||||||
|
running += tri_count
|
||||||
|
|
||||||
|
print("Dartboard: mapped %d faces." % _face_to_slot.size())
|
||||||
|
|
||||||
|
|
||||||
|
func _cache_radius() -> void:
|
||||||
|
var aabb := mesh_instance.get_aabb()
|
||||||
|
# The disc's two large axes. The third is the board's thickness.
|
||||||
|
var sizes := [aabb.size.x, aabb.size.y, aabb.size.z]
|
||||||
|
sizes.sort()
|
||||||
|
_outer_radius = sizes[2] * 0.5 # largest dimension = diameter
|
||||||
|
|
||||||
|
|
||||||
|
# --- Public -------------------------------------------------------------
|
||||||
|
|
||||||
|
## Which slot does this trimesh face belong to?
|
||||||
|
func slot_for_face(face_index: int) -> StringName:
|
||||||
|
return _face_to_slot.get(face_index, &"")
|
||||||
|
|
||||||
|
|
||||||
|
## Paint a texture onto one slot. Used for both skins and modifier overlays.
|
||||||
|
func paint_slot(slot: StringName, texture: Texture2D) -> void:
|
||||||
|
if not _slot_to_surface.has(slot):
|
||||||
|
push_warning("No slot named '%s'." % slot)
|
||||||
|
return
|
||||||
|
|
||||||
|
var surface: int = _slot_to_surface[slot]
|
||||||
|
var mat := mesh_instance.get_active_material(surface)
|
||||||
|
if mat == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Duplicate, or we mutate the shared material from the .glb — which leaks
|
||||||
|
# to every instance AND can get written back to disk in the editor.
|
||||||
|
var unique := mat.duplicate() as StandardMaterial3D
|
||||||
|
unique.albedo_texture = texture
|
||||||
|
unique.albedo_color = Color.WHITE # don't tint the texture
|
||||||
|
mesh_instance.set_surface_override_material(surface, unique)
|
||||||
|
|
||||||
|
|
||||||
|
## Reset a slot to its imported material.
|
||||||
|
func clear_slot(slot: StringName) -> void:
|
||||||
|
if not _slot_to_surface.has(slot):
|
||||||
|
return
|
||||||
|
mesh_instance.set_surface_override_material(_slot_to_surface[slot], null)
|
||||||
|
|
||||||
|
|
||||||
|
func get_slots() -> Array[StringName]:
|
||||||
|
var out: Array[StringName] = []
|
||||||
|
out.assign(_slot_to_surface.keys())
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
## Board radius in local units.
|
||||||
|
func outer_radius() -> float:
|
||||||
|
return _outer_radius
|
||||||
|
|
||||||
|
|
||||||
|
## Project a LOCAL point onto the board's face plane, returning 2D coords
|
||||||
|
## where +Y is "up" on the board (toward the 20).
|
||||||
|
##
|
||||||
|
## Which two axes those are depends on how the .glb imported. glTF is +Y up,
|
||||||
|
## so a board modeled facing +Z in Blender usually lands facing +Z here, with
|
||||||
|
## the disc in the X/Y plane. If sector detection is 90 degrees off, this is
|
||||||
|
## the function to fix.
|
||||||
|
func planar_coords(local_point: Vector3) -> Vector2:
|
||||||
|
var aabb := mesh_instance.get_aabb()
|
||||||
|
var center := aabb.position + aabb.size * 0.5
|
||||||
|
|
||||||
|
# Identify the thin axis — that's the board's normal.
|
||||||
|
var s := aabb.size
|
||||||
|
if s.z <= s.x and s.z <= s.y:
|
||||||
|
return Vector2(local_point.x - center.x, local_point.y - center.y)
|
||||||
|
elif s.y <= s.x and s.y <= s.z:
|
||||||
|
return Vector2(local_point.x - center.x, local_point.z - center.z)
|
||||||
|
else:
|
||||||
|
return Vector2(local_point.z - center.z, local_point.y - center.y)
|
||||||
|
|
||||||
|
|
||||||
|
## Debug: what did Blender actually export?
|
||||||
|
func print_slots() -> void:
|
||||||
|
var mesh := mesh_instance.mesh
|
||||||
|
print("=== Dartboard slots ===")
|
||||||
|
for i in mesh.get_surface_count():
|
||||||
|
var mat := mesh.surface_get_material(i)
|
||||||
|
print(" surface %2d: '%s'" % [i, mat.resource_name if mat else "(none)"])
|
||||||
|
print(" radius: %.4f aabb: %s" % [_outer_radius, mesh_instance.get_aabb().size])
|
||||||
|
print("=======================")
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cme5xqn2hlrlu
|
||||||
Binary file not shown.
@@ -0,0 +1,45 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://djr5vbnb652ax"
|
||||||
|
path="res://.godot/imported/dartboard.glb-8db0957180c245aae0575b1ad098f883.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://core/board/dartboard.glb"
|
||||||
|
dest_files=["res://.godot/imported/dartboard.glb-8db0957180c245aae0575b1ad098f883.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=1
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
[gd_scene format=3 uid="uid://dojbd5wl7ntp0"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://cme5xqn2hlrlu" path="res://core/board/dartboard.gd" id="1"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://djr5vbnb652ax" path="res://core/board/dartboard.glb" id="2"]
|
||||||
|
[ext_resource type="Shape3D" uid="uid://vr03k1qlo10y" path="res://core/board/dartboard_collision.res" id="3_6aacy"]
|
||||||
|
[ext_resource type="Shape3D" uid="uid://c0vqtoe8q7yly" path="res://core/board/spider_collision.res" id="4_khw4q"]
|
||||||
|
|
||||||
|
[sub_resource type="PhysicsMaterial" id="PhysicsMaterial_uomu6"]
|
||||||
|
friction = 0.15
|
||||||
|
bounce = 0.6
|
||||||
|
|
||||||
|
[node name="Dartboard" type="StaticBody3D" unique_id=838863701]
|
||||||
|
script = ExtResource("1")
|
||||||
|
|
||||||
|
[node name="Model" parent="." unique_id=1556270899 instance=ExtResource("2")]
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=1180985747]
|
||||||
|
shape = ExtResource("3_6aacy")
|
||||||
|
|
||||||
|
[node name="SpiderBody" type="StaticBody3D" parent="." unique_id=216948518]
|
||||||
|
collision_layer = 8
|
||||||
|
physics_material_override = SubResource("PhysicsMaterial_uomu6")
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="SpiderBody" unique_id=1618627145]
|
||||||
|
shape = ExtResource("4_khw4q")
|
||||||
|
|
||||||
|
[editable path="Model"]
|
||||||
Binary file not shown.
@@ -0,0 +1,44 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dj6wb70lh5s4f"
|
||||||
|
path.s3tc="res://.godot/imported/dartboard_bulls.png-354417d2f083e713950eed8be5c54f73.s3tc.ctex"
|
||||||
|
metadata={
|
||||||
|
"imported_formats": ["s3tc_bptc"],
|
||||||
|
"vram_texture": true
|
||||||
|
}
|
||||||
|
generator_parameters={
|
||||||
|
"md5": "ce1804f3e86fea26503097d5e278c919"
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://core/board/dartboard_bulls.png"
|
||||||
|
dest_files=["res://.godot/imported/dartboard_bulls.png-354417d2f083e713950eed8be5c54f73.s3tc.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=2
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=true
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=0
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,44 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://da2elnsphlc5i"
|
||||||
|
path.s3tc="res://.godot/imported/dartboard_flat_tire.png-97f4bd426c06f71b009cd942664be6fa.s3tc.ctex"
|
||||||
|
metadata={
|
||||||
|
"imported_formats": ["s3tc_bptc"],
|
||||||
|
"vram_texture": true
|
||||||
|
}
|
||||||
|
generator_parameters={
|
||||||
|
"md5": "d24ebe44ff702bdbd5528c9d29427568"
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://core/board/dartboard_flat_tire.png"
|
||||||
|
dest_files=["res://.godot/imported/dartboard_flat_tire.png-97f4bd426c06f71b009cd942664be6fa.s3tc.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=2
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=true
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=0
|
||||||
Binary file not shown.
@@ -0,0 +1,44 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://cjw6x7ig1ogbo"
|
||||||
|
path.s3tc="res://.godot/imported/dartboard_sector_black.png-39f98bf69c481faad6b8976a0994ecb5.s3tc.ctex"
|
||||||
|
metadata={
|
||||||
|
"imported_formats": ["s3tc_bptc"],
|
||||||
|
"vram_texture": true
|
||||||
|
}
|
||||||
|
generator_parameters={
|
||||||
|
"md5": "831beecb2a48df4a1f6850e3e8c938dd"
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://core/board/dartboard_sector_black.png"
|
||||||
|
dest_files=["res://.godot/imported/dartboard_sector_black.png-39f98bf69c481faad6b8976a0994ecb5.s3tc.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=2
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=true
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=0
|
||||||
Binary file not shown.
@@ -0,0 +1,44 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://cry28pcmt01gk"
|
||||||
|
path.s3tc="res://.godot/imported/dartboard_sector_white.png-6dbf28d122f3327d568bcbb29e7f70a0.s3tc.ctex"
|
||||||
|
metadata={
|
||||||
|
"imported_formats": ["s3tc_bptc"],
|
||||||
|
"vram_texture": true
|
||||||
|
}
|
||||||
|
generator_parameters={
|
||||||
|
"md5": "aa25e4080cfa45ac8064cbf217904fb3"
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://core/board/dartboard_sector_white.png"
|
||||||
|
dest_files=["res://.godot/imported/dartboard_sector_white.png-6dbf28d122f3327d568bcbb29e7f70a0.s3tc.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=2
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=true
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=0
|
||||||
Binary file not shown.
@@ -0,0 +1,44 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://b87cl0ullks7x"
|
||||||
|
path.s3tc="res://.godot/imported/dartboard_spider.png-c5fe87c929f66762aaaaefebd3359c9d.s3tc.ctex"
|
||||||
|
metadata={
|
||||||
|
"imported_formats": ["s3tc_bptc"],
|
||||||
|
"vram_texture": true
|
||||||
|
}
|
||||||
|
generator_parameters={
|
||||||
|
"md5": "693560c0ab922fba23ec09a26ead7f48"
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://core/board/dartboard_spider.png"
|
||||||
|
dest_files=["res://.godot/imported/dartboard_spider.png-c5fe87c929f66762aaaaefebd3359c9d.s3tc.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=2
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=true
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=0
|
||||||
Binary file not shown.
@@ -0,0 +1,192 @@
|
|||||||
|
class_name Dart
|
||||||
|
extends RigidBody3D
|
||||||
|
|
||||||
|
## A single dart. Dragged from the tray, thrown at the board.
|
||||||
|
##
|
||||||
|
## Lifecycle: FROZEN -> DRAGGING -> IN_FLIGHT -> LANDED
|
||||||
|
## The scene root (main.gd) routes mouse input here; the dart does not
|
||||||
|
## hunt for the camera or raycast for itself.
|
||||||
|
|
||||||
|
signal landed(dart: Dart, hit_point: Vector3)
|
||||||
|
|
||||||
|
enum State { IDLE, DRAGGING, IN_FLIGHT, LANDED }
|
||||||
|
|
||||||
|
# --- Tuning -------------------------------------------------------------
|
||||||
|
|
||||||
|
@export_group("Drag")
|
||||||
|
## How hard the dart chases the cursor while held.
|
||||||
|
@export var drag_follow_speed: float = 20.0
|
||||||
|
## Clamp so a fast flick doesn't launch it across the room mid-drag.
|
||||||
|
@export var max_drag_speed: float = 42.0
|
||||||
|
## Tilt of the drag plane relative to the camera, in degrees.
|
||||||
|
@export_range(-180.0, 180.0) var drag_plane_angle: float = 15.0
|
||||||
|
@export var drag_linear_damp: float = 5.0
|
||||||
|
@export var drag_angular_damp: float = 10.0
|
||||||
|
|
||||||
|
@export_group("Throw")
|
||||||
|
@export var forward_impulse: float = 8.0
|
||||||
|
|
||||||
|
## Scale the raw flick velocity down. Raw mouse speed is usually too fast.
|
||||||
|
@export var throw_force_multiplier: float = 0.5
|
||||||
|
@export var max_throw_speed: float = 42.0
|
||||||
|
## Higher = more visible arc. Real gravity looks too floaty at dart scale.
|
||||||
|
@export var flight_gravity_scale: float = 2.0
|
||||||
|
|
||||||
|
@export_group("Flight")
|
||||||
|
## How fast the nose swings to face the direction of travel.
|
||||||
|
@export var nose_align_speed: float = 1.0
|
||||||
|
## Which local axis is the dart's tip. Check this against the model.
|
||||||
|
@export var forward_axis: Vector3 = Vector3.FORWARD
|
||||||
|
## Below this speed the dart is considered landed.
|
||||||
|
@export var landed_speed_threshold: float = 0.5
|
||||||
|
|
||||||
|
# --- State --------------------------------------------------------------
|
||||||
|
|
||||||
|
var state: State = State.IDLE
|
||||||
|
|
||||||
|
var _drag_plane: Plane
|
||||||
|
var _drag_offset: Vector3 = Vector3.ZERO
|
||||||
|
var _target_pos: Vector3 = Vector3.ZERO
|
||||||
|
|
||||||
|
## Sampled positions, used to compute throw velocity on release.
|
||||||
|
const HISTORY_LENGTH := 6
|
||||||
|
var _pos_history: Array[Vector3] = []
|
||||||
|
var _time_history: Array[float] = []
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
freeze_mode = RigidBody3D.FREEZE_MODE_STATIC
|
||||||
|
freeze = true
|
||||||
|
add_to_group(&"dart")
|
||||||
|
|
||||||
|
|
||||||
|
# --- Public API (called by main.gd) --------------------------------------
|
||||||
|
|
||||||
|
func begin_drag(camera: Camera3D, hit_point: Vector3) -> void:
|
||||||
|
state = State.DRAGGING
|
||||||
|
freeze = false
|
||||||
|
|
||||||
|
gravity_scale = 0.0 # don't sag while held
|
||||||
|
linear_damp = drag_linear_damp
|
||||||
|
angular_damp = drag_angular_damp
|
||||||
|
|
||||||
|
# The plane the dart slides along while dragged. Tilted off the camera's
|
||||||
|
# view axis so pulling back also pulls the dart down, which feels natural.
|
||||||
|
var base_normal := camera.global_transform.basis.z
|
||||||
|
var tilted := base_normal.rotated(
|
||||||
|
camera.global_transform.basis.x,
|
||||||
|
deg_to_rad(drag_plane_angle)
|
||||||
|
)
|
||||||
|
_drag_plane = Plane(tilted.normalized(), hit_point)
|
||||||
|
_drag_offset = global_position - hit_point
|
||||||
|
|
||||||
|
# Seed the target to the current position. Without this the dart drops
|
||||||
|
# to the plane origin until the mouse first moves.
|
||||||
|
_target_pos = global_position
|
||||||
|
|
||||||
|
_pos_history.clear()
|
||||||
|
_time_history.clear()
|
||||||
|
|
||||||
|
|
||||||
|
func update_drag(camera: Camera3D, mouse_pos: Vector2) -> void:
|
||||||
|
if state != State.DRAGGING:
|
||||||
|
return
|
||||||
|
|
||||||
|
var ray_origin := camera.project_ray_origin(mouse_pos)
|
||||||
|
var ray_dir := camera.project_ray_normal(mouse_pos)
|
||||||
|
var hit = _drag_plane.intersects_ray(ray_origin, ray_dir)
|
||||||
|
if hit != null:
|
||||||
|
_target_pos = hit + _drag_offset
|
||||||
|
|
||||||
|
func release_drag() -> void:
|
||||||
|
if state != State.DRAGGING:
|
||||||
|
return
|
||||||
|
state = State.IN_FLIGHT
|
||||||
|
gravity_scale = flight_gravity_scale
|
||||||
|
linear_damp = 0.0
|
||||||
|
angular_damp = 0.0
|
||||||
|
|
||||||
|
# The flick gives aim (up/down/left/right). It CANNOT give forward speed —
|
||||||
|
# the drag plane faces the camera, so motion toward the board is ~zero.
|
||||||
|
var flick := _calculate_throw_velocity() * throw_force_multiplier
|
||||||
|
|
||||||
|
# The forward component has to come from somewhere. Use the camera's
|
||||||
|
# forward, so "throw" always means "toward what you're looking at".
|
||||||
|
var cam := get_viewport().get_camera_3d()
|
||||||
|
var forward := -cam.global_transform.basis.z * forward_impulse
|
||||||
|
|
||||||
|
var throw := flick + forward
|
||||||
|
if throw.length() > max_throw_speed:
|
||||||
|
throw = throw.normalized() * max_throw_speed
|
||||||
|
linear_velocity = throw
|
||||||
|
|
||||||
|
var raw := _calculate_throw_velocity()
|
||||||
|
print("flick: %s forward: %s final: %s (%.1f m/s)" % [flick, forward, throw, throw.length()])
|
||||||
|
|
||||||
|
|
||||||
|
# --- Physics ------------------------------------------------------------
|
||||||
|
|
||||||
|
func _physics_process(delta: float) -> void:
|
||||||
|
match state:
|
||||||
|
State.DRAGGING:
|
||||||
|
_process_drag()
|
||||||
|
State.IN_FLIGHT:
|
||||||
|
_process_flight(delta)
|
||||||
|
|
||||||
|
|
||||||
|
func _process_drag() -> void:
|
||||||
|
# Chase the target with velocity rather than teleporting, so the dart
|
||||||
|
# still collides with things and carries momentum into the throw.
|
||||||
|
var to_target := _target_pos - global_position
|
||||||
|
var desired := to_target * drag_follow_speed
|
||||||
|
if desired.length() > max_drag_speed:
|
||||||
|
desired = desired.normalized() * max_drag_speed
|
||||||
|
|
||||||
|
linear_velocity = desired
|
||||||
|
angular_velocity = angular_velocity.lerp(Vector3.ZERO, 0.2)
|
||||||
|
|
||||||
|
_pos_history.append(global_position)
|
||||||
|
_time_history.append(Time.get_ticks_msec() / 1000.0)
|
||||||
|
if _pos_history.size() > HISTORY_LENGTH:
|
||||||
|
_pos_history.pop_front()
|
||||||
|
_time_history.pop_front()
|
||||||
|
|
||||||
|
|
||||||
|
func _process_flight(delta: float) -> void:
|
||||||
|
if linear_velocity.length() > landed_speed_threshold:
|
||||||
|
_align_to_velocity(delta)
|
||||||
|
else:
|
||||||
|
_on_landed()
|
||||||
|
|
||||||
|
|
||||||
|
func _align_to_velocity(delta: float) -> void:
|
||||||
|
var vel_dir := linear_velocity.normalized()
|
||||||
|
var current_forward := global_transform.basis * forward_axis
|
||||||
|
var axis := current_forward.cross(vel_dir)
|
||||||
|
|
||||||
|
if axis.length() < 0.001:
|
||||||
|
return # already aligned, or exactly opposed
|
||||||
|
|
||||||
|
axis = axis.normalized()
|
||||||
|
var angle := current_forward.angle_to(vel_dir)
|
||||||
|
var step := minf(angle, nose_align_speed * delta)
|
||||||
|
|
||||||
|
global_transform.basis = global_transform.basis.rotated(axis, step)
|
||||||
|
global_transform.basis = global_transform.basis.orthonormalized()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_landed() -> void:
|
||||||
|
state = State.LANDED
|
||||||
|
angular_velocity = Vector3.ZERO
|
||||||
|
landed.emit(self, global_position)
|
||||||
|
|
||||||
|
|
||||||
|
func _calculate_throw_velocity() -> Vector3:
|
||||||
|
if _pos_history.size() < 2:
|
||||||
|
return linear_velocity
|
||||||
|
|
||||||
|
var dt: float = _time_history[-1] - _time_history[0]
|
||||||
|
if dt <= 0.0:
|
||||||
|
return linear_velocity
|
||||||
|
|
||||||
|
return (_pos_history[-1] - _pos_history[0]) / dt
|
||||||
BIN
Binary file not shown.
@@ -3,13 +3,13 @@
|
|||||||
importer="scene"
|
importer="scene"
|
||||||
importer_version=1
|
importer_version=1
|
||||||
type="PackedScene"
|
type="PackedScene"
|
||||||
uid="uid://cjq45anerhxnk"
|
uid="uid://fp0gcwcyfd7g"
|
||||||
path="res://.godot/imported/Dart.glb-e68d17366fac688e4c81cf4cc62e1175.scn"
|
path="res://.godot/imported/dart.glb-ca8e61ccf0bb4431c3c990bf6c68da95.scn"
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://models/Dart.glb"
|
source_file="res://core/dart/dart.glb"
|
||||||
dest_files=["res://.godot/imported/Dart.glb-e68d17366fac688e4c81cf4cc62e1175.scn"]
|
dest_files=["res://.godot/imported/dart.glb-ca8e61ccf0bb4431c3c990bf6c68da95.scn"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
[gd_scene format=3 uid="uid://fshohiewm7et"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://sueo33rxr07e" path="res://core/dart/dart.gd" id="1"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://fp0gcwcyfd7g" path="res://core/dart/dart.glb" id="2"]
|
||||||
|
|
||||||
|
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"]
|
||||||
|
radius = 0.008
|
||||||
|
height = 0.15
|
||||||
|
|
||||||
|
[node name="Dart" type="RigidBody3D" unique_id=1124867376]
|
||||||
|
collision_layer = 2
|
||||||
|
script = ExtResource("1")
|
||||||
|
|
||||||
|
[node name="MeshInstance3D" parent="." unique_id=936158903 instance=ExtResource("2")]
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=2015440784]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.07, 0)
|
||||||
|
shape = SubResource("CapsuleShape3D_1")
|
||||||
|
|
||||||
|
[node name="Tip" type="Marker3D" parent="." unique_id=2014607155]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.14, 0)
|
||||||
Binary file not shown.
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
importer="texture"
|
importer="texture"
|
||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://62njxkpga6e8"
|
uid="uid://b3n3th3102f2g"
|
||||||
path.s3tc="res://.godot/imported/Dart_Dart-Default.png-ffb5dce2e6bbf58fe2b00949ee100702.s3tc.ctex"
|
path.s3tc="res://.godot/imported/dart_body.png-b1ba77c6cead7a089d94545960115fac.s3tc.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"imported_formats": ["s3tc_bptc"],
|
"imported_formats": ["s3tc_bptc"],
|
||||||
"vram_texture": true
|
"vram_texture": true
|
||||||
@@ -14,8 +14,8 @@ generator_parameters={
|
|||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://models/Dart_Dart-Default.png"
|
source_file="res://core/dart/dart_body.png"
|
||||||
dest_files=["res://.godot/imported/Dart_Dart-Default.png-ffb5dce2e6bbf58fe2b00949ee100702.s3tc.ctex"]
|
dest_files=["res://.godot/imported/dart_body.png-b1ba77c6cead7a089d94545960115fac.s3tc.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
Binary file not shown.
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
importer="texture"
|
importer="texture"
|
||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://b7bsi4ury2lve"
|
uid="uid://c85g6b4hhn83g"
|
||||||
path.s3tc="res://.godot/imported/Dart_Wings-Default.png-dcf030c3d00e4d9573d0a3e9c88fff0a.s3tc.ctex"
|
path.s3tc="res://.godot/imported/dart_wings.png-45f87827353eb4dc5f134b80f5e80ea7.s3tc.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"imported_formats": ["s3tc_bptc"],
|
"imported_formats": ["s3tc_bptc"],
|
||||||
"vram_texture": true
|
"vram_texture": true
|
||||||
@@ -14,8 +14,8 @@ generator_parameters={
|
|||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://models/Dart_Wings-Default.png"
|
source_file="res://core/dart/dart_wings.png"
|
||||||
dest_files=["res://.godot/imported/Dart_Wings-Default.png-dcf030c3d00e4d9573d0a3e9c88fff0a.s3tc.ctex"]
|
dest_files=["res://.godot/imported/dart_wings.png-45f87827353eb4dc5f134b80f5e80ea7.s3tc.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
extends Node
|
||||||
|
|
||||||
|
## Loads modifier definitions and serves them by id.
|
||||||
|
##
|
||||||
|
## ⚠️ PROTOTYPE. This is scaffolding, not the real mod loader.
|
||||||
|
##
|
||||||
|
## What's wrong with it, to be fixed in Phase 3:
|
||||||
|
## - DirAccess cannot walk into a PCK. This works in the editor and silently
|
||||||
|
## returns NOTHING in an exported build. Needs a build-time manifest.
|
||||||
|
## - No namespacing. Ids are bare strings, so two mods can collide.
|
||||||
|
## - No mod.json, no api_version, no dependency resolution.
|
||||||
|
## - Only reads content/official/. Doesn't touch user://mods/.
|
||||||
|
##
|
||||||
|
## Autoload name is ModRegistry (PascalCase), so call sites read as
|
||||||
|
## ModRegistry.get_by_id(...) rather than looking like a local variable.
|
||||||
|
|
||||||
|
const MODIFIER_DIR := "res://content/official/modifiers/"
|
||||||
|
|
||||||
|
var modifiers: Array[Modifier] = []
|
||||||
|
var _by_id: Dictionary = {}
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_load_from_folder(MODIFIER_DIR)
|
||||||
|
_reindex()
|
||||||
|
print("ModRegistry: loaded %d modifiers." % modifiers.size())
|
||||||
|
|
||||||
|
|
||||||
|
## Recursive, because each modifier now lives in its own folder:
|
||||||
|
## modifiers/mjolnir/mjolnir.tres
|
||||||
|
func _load_from_folder(path: String) -> void:
|
||||||
|
var dir := DirAccess.open(path)
|
||||||
|
if dir == null:
|
||||||
|
push_warning("ModRegistry: can't open %s" % path)
|
||||||
|
return
|
||||||
|
|
||||||
|
dir.list_dir_begin()
|
||||||
|
var fname := dir.get_next()
|
||||||
|
while fname != "":
|
||||||
|
var full := path.path_join(fname)
|
||||||
|
if dir.current_is_dir():
|
||||||
|
if not fname.begins_with("."):
|
||||||
|
_load_from_folder(full)
|
||||||
|
elif fname.ends_with(".tres"):
|
||||||
|
var res := load(full)
|
||||||
|
if res is Modifier:
|
||||||
|
modifiers.append(res)
|
||||||
|
else:
|
||||||
|
push_warning("ModRegistry: %s is not a Modifier" % full)
|
||||||
|
fname = dir.get_next()
|
||||||
|
dir.list_dir_end()
|
||||||
|
|
||||||
|
|
||||||
|
func _reindex() -> void:
|
||||||
|
_by_id.clear()
|
||||||
|
for m in modifiers:
|
||||||
|
if m.id.is_empty():
|
||||||
|
push_warning("ModRegistry: modifier with empty id, skipping.")
|
||||||
|
continue
|
||||||
|
if _by_id.has(m.id):
|
||||||
|
push_warning("ModRegistry: duplicate id '%s'." % m.id)
|
||||||
|
_by_id[m.id] = m
|
||||||
|
|
||||||
|
|
||||||
|
func get_by_id(id: String) -> Modifier:
|
||||||
|
return _by_id.get(id)
|
||||||
|
|
||||||
|
|
||||||
|
## Pick `count` distinct modifiers, weighted by rarity.
|
||||||
|
func pick_random(count: int) -> Array[Modifier]:
|
||||||
|
var pool := modifiers.duplicate()
|
||||||
|
var chosen: Array[Modifier] = []
|
||||||
|
count = mini(count, pool.size())
|
||||||
|
|
||||||
|
for _i in count:
|
||||||
|
var total := 0.0
|
||||||
|
for m in pool:
|
||||||
|
total += maxf(m.weight, 0.0001)
|
||||||
|
|
||||||
|
var r := randf() * total
|
||||||
|
var acc := 0.0
|
||||||
|
var picked := 0
|
||||||
|
for j in pool.size():
|
||||||
|
acc += maxf(pool[j].weight, 0.0001)
|
||||||
|
if r <= acc:
|
||||||
|
picked = j
|
||||||
|
break
|
||||||
|
|
||||||
|
chosen.append(pool[picked])
|
||||||
|
pool.remove_at(picked) # distinct — don't offer the same one twice
|
||||||
|
|
||||||
|
return chosen
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://btm4dwci50xmu
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
extends Resource
|
||||||
|
class_name Modifier
|
||||||
|
# One modifier definition: title + description + texture + scoring rule.
|
||||||
|
# Create instances of this as .tres files in the editor (right-click in
|
||||||
|
# FileSystem -> New Resource -> Modifier), OR build them in code in the catalog.
|
||||||
|
#
|
||||||
|
# Because a texture always maps to the same modifier, the texture lives HERE.
|
||||||
|
|
||||||
|
# A stable unique id (e.g. "stars_x5"). Useful for saving which modifiers are
|
||||||
|
# applied to which segments, and for lookups. Keep it unique across the catalog.
|
||||||
|
@export var id: String = ""
|
||||||
|
|
||||||
|
# Shown to the player when offered the piece.
|
||||||
|
@export var title: String = ""
|
||||||
|
@export_multiline var description: String = ""
|
||||||
|
|
||||||
|
# The texture painted onto the segment when this modifier is applied.
|
||||||
|
@export var texture: Texture2D
|
||||||
|
|
||||||
|
# --- Scoring rule ---
|
||||||
|
# Simple, data-only rules cover most cases without writing code per modifier.
|
||||||
|
# value = (base_value * mult + add) for the affected segment.
|
||||||
|
@export var score_mult: float = 1.0
|
||||||
|
@export var score_add: int = 0
|
||||||
|
|
||||||
|
# Optional: rarity weight for random selection (higher = more common).
|
||||||
|
@export var weight: float = 1.0
|
||||||
|
|
||||||
|
# Apply this modifier's rule to a segment's base value.
|
||||||
|
func apply_score(base_value: int) -> int:
|
||||||
|
return int(round(base_value * score_mult)) + score_add
|
||||||
|
|
||||||
|
# A short human-readable summary of the effect, for tooltips/UI.
|
||||||
|
func effect_text() -> String:
|
||||||
|
var parts: Array[String] = []
|
||||||
|
if score_mult != 1.0:
|
||||||
|
parts.append("x%s" % str(score_mult))
|
||||||
|
if score_add != 0:
|
||||||
|
parts.append(("+%d" % score_add) if score_add > 0 else str(score_add))
|
||||||
|
return " ".join(parts) if parts.size() > 0 else "no change"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://duh0lxb7gdimr
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
extends Control
|
||||||
|
|
||||||
|
## Resolves a screen position (or a dart's world position) to a board sector,
|
||||||
|
## and applies modifiers there.
|
||||||
|
##
|
||||||
|
## THE BOARD HAS 21 MATERIAL SLOTS: sector_1 .. sector_20, plus "bulls".
|
||||||
|
## The bull slot covers inner AND outer bull — they move together.
|
||||||
|
##
|
||||||
|
## Two ways to find which sector was hit:
|
||||||
|
##
|
||||||
|
## A) BY FACE INDEX (default, robust)
|
||||||
|
## Raycast returns face_index. Map face -> surface. The surface's material
|
||||||
|
## name IS the sector. No trigonometry.
|
||||||
|
## Survives the board rotating, tilting, or having its sectors remapped,
|
||||||
|
## because we're asking the geometry, not calculating from an angle.
|
||||||
|
## Requires ConcavePolygonShape3D (trimesh) collision.
|
||||||
|
##
|
||||||
|
## B) BY ANGLE (fallback)
|
||||||
|
## atan2 on the local hit point. Cheap, works with a box collider.
|
||||||
|
## Survives rotation (to_local undoes the transform) but NOT sector
|
||||||
|
## remapping — if Mjolnir makes every sector a 20, the angle still says
|
||||||
|
## "this is the 6".
|
||||||
|
##
|
||||||
|
## Use A. It's the only one that supports a moving/mutating board, which is the
|
||||||
|
## whole point of the modifier system.
|
||||||
|
|
||||||
|
const RAY_LENGTH := 1000.0
|
||||||
|
|
||||||
|
enum Ring { MISS, INNER_BULL, OUTER_BULL, INNER_SINGLE, TRIPLE, OUTER_SINGLE, DOUBLE }
|
||||||
|
enum Method { FACE_INDEX, ANGLE }
|
||||||
|
|
||||||
|
@export var camera: Camera3D
|
||||||
|
@export var dartboard: Dartboard
|
||||||
|
|
||||||
|
@export_group("Resolution")
|
||||||
|
@export var method: Method = Method.FACE_INDEX
|
||||||
|
|
||||||
|
@export_group("Angle fallback (only used if method = ANGLE)")
|
||||||
|
## Clockwise from 12 o'clock.
|
||||||
|
const DART_NUMBERS := [20, 1, 18, 4, 13, 6, 10, 15, 2, 17,
|
||||||
|
3, 19, 7, 16, 8, 11, 14, 9, 12, 5]
|
||||||
|
@export_range(0, 19) var sector_offset := 0
|
||||||
|
@export var reverse_direction := false
|
||||||
|
|
||||||
|
@export_group("Ring radii (fraction of outer radius)")
|
||||||
|
## Rings are ALWAYS resolved by radius, even in FACE_INDEX mode — the surface
|
||||||
|
## tells us WHICH sector, the radius tells us WHICH RING within it.
|
||||||
|
@export var bull_inner_max := 0.04
|
||||||
|
@export var bull_outer_max := 0.09
|
||||||
|
@export var inner_max := 0.45
|
||||||
|
@export var triple_max := 0.55
|
||||||
|
@export var outer_max := 0.80
|
||||||
|
|
||||||
|
## sector number (1-20), or 0 for bull -> Modifier
|
||||||
|
var applied: Dictionary = {}
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
|
||||||
|
|
||||||
|
# --- Public -------------------------------------------------------------
|
||||||
|
|
||||||
|
## Drop a modifier at a screen position. True if it landed on the board.
|
||||||
|
func apply_modifier_at_screen_pos(screen_pos: Vector2, modifier: Modifier) -> bool:
|
||||||
|
if modifier == null:
|
||||||
|
return false
|
||||||
|
if camera == null or dartboard == null:
|
||||||
|
push_error("DropCatcher: camera and dartboard exports must be set.")
|
||||||
|
return false
|
||||||
|
|
||||||
|
var hit := _raycast(screen_pos)
|
||||||
|
if hit.is_empty():
|
||||||
|
return false
|
||||||
|
|
||||||
|
var slot := _slot_from_hit(hit)
|
||||||
|
if slot == &"":
|
||||||
|
return false
|
||||||
|
|
||||||
|
dartboard.paint_slot(slot, modifier.texture)
|
||||||
|
applied[slot] = modifier
|
||||||
|
print("applied '%s' to %s" % [modifier.id, slot])
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
## Score a dart that landed at a world point.
|
||||||
|
func score_dart(world_point: Vector3, face_index: int = -1) -> int:
|
||||||
|
var slot := _slot_from_world(world_point, face_index)
|
||||||
|
var ring := _ring_from_radius(_norm_radius(world_point))
|
||||||
|
var base := _base_score(slot, ring)
|
||||||
|
|
||||||
|
if applied.has(slot):
|
||||||
|
return (applied[slot] as Modifier).apply_score(base)
|
||||||
|
return base
|
||||||
|
|
||||||
|
|
||||||
|
## Full breakdown of a hit. This is what Phase 2's ScoringEngine will consume.
|
||||||
|
func resolve_hit(world_point: Vector3, face_index: int = -1) -> Dictionary:
|
||||||
|
var slot := _slot_from_world(world_point, face_index)
|
||||||
|
var ring := _ring_from_radius(_norm_radius(world_point))
|
||||||
|
return {
|
||||||
|
"slot": slot, # &"sector_20" / &"bulls" / &""
|
||||||
|
"sector": _sector_number(slot), # 1-20, or 0 for bull/miss
|
||||||
|
"ring": ring,
|
||||||
|
"base_score": _base_score(slot, ring),
|
||||||
|
"modifier": applied.get(slot),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# --- Slot resolution ----------------------------------------------------
|
||||||
|
|
||||||
|
func _slot_from_hit(hit: Dictionary) -> StringName:
|
||||||
|
var point: Vector3 = hit.get("position", Vector3.ZERO)
|
||||||
|
var face: int = hit.get("face_index", -1)
|
||||||
|
return _slot_from_world(point, face)
|
||||||
|
|
||||||
|
|
||||||
|
func _slot_from_world(world_point: Vector3, face_index: int) -> StringName:
|
||||||
|
# The bull is a slot in its own right — check radius first, since the bull
|
||||||
|
# surface may be small enough that face lookup is fiddly at the very center.
|
||||||
|
var norm := _norm_radius(world_point)
|
||||||
|
if norm > 1.0:
|
||||||
|
return &""
|
||||||
|
if norm <= bull_outer_max:
|
||||||
|
return &"bulls"
|
||||||
|
|
||||||
|
if method == Method.FACE_INDEX and face_index >= 0:
|
||||||
|
var slot := dartboard.slot_for_face(face_index)
|
||||||
|
if slot != &"":
|
||||||
|
return slot
|
||||||
|
push_warning("Face %d didn't map to a slot; falling back to angle." % face_index)
|
||||||
|
|
||||||
|
return _slot_from_angle(world_point)
|
||||||
|
|
||||||
|
|
||||||
|
## Fallback. Works under rotation, NOT under sector remapping.
|
||||||
|
func _slot_from_angle(world_point: Vector3) -> StringName:
|
||||||
|
var local := dartboard.to_local(world_point)
|
||||||
|
var planar := dartboard.planar_coords(local)
|
||||||
|
|
||||||
|
var t := fposmod(atan2(planar.x, planar.y) / TAU, 1.0)
|
||||||
|
if reverse_direction:
|
||||||
|
t = 1.0 - t
|
||||||
|
|
||||||
|
var idx := int(floor(t * 20.0 + 0.5)) % 20
|
||||||
|
idx = (idx + sector_offset) % 20
|
||||||
|
return StringName("sector_%d" % DART_NUMBERS[idx])
|
||||||
|
|
||||||
|
|
||||||
|
# --- Rings & scoring ----------------------------------------------------
|
||||||
|
|
||||||
|
## Distance from board center, as a fraction of the outer radius.
|
||||||
|
func _norm_radius(world_point: Vector3) -> float:
|
||||||
|
var local := dartboard.to_local(world_point)
|
||||||
|
var planar := dartboard.planar_coords(local)
|
||||||
|
var outer := dartboard.outer_radius()
|
||||||
|
return planar.length() / outer if outer > 0.0 else 999.0
|
||||||
|
|
||||||
|
|
||||||
|
func _ring_from_radius(norm: float) -> Ring:
|
||||||
|
if norm > 1.0:
|
||||||
|
return Ring.MISS
|
||||||
|
if norm <= bull_inner_max:
|
||||||
|
return Ring.INNER_BULL
|
||||||
|
if norm <= bull_outer_max:
|
||||||
|
return Ring.OUTER_BULL
|
||||||
|
if norm <= inner_max:
|
||||||
|
return Ring.INNER_SINGLE
|
||||||
|
if norm <= triple_max:
|
||||||
|
return Ring.TRIPLE
|
||||||
|
if norm <= outer_max:
|
||||||
|
return Ring.OUTER_SINGLE
|
||||||
|
return Ring.DOUBLE
|
||||||
|
|
||||||
|
|
||||||
|
func _sector_number(slot: StringName) -> int:
|
||||||
|
var s := String(slot)
|
||||||
|
if not s.begins_with("sector_"):
|
||||||
|
return 0
|
||||||
|
return s.trim_prefix("sector_").to_int()
|
||||||
|
|
||||||
|
|
||||||
|
func _base_score(slot: StringName, ring: Ring) -> int:
|
||||||
|
match ring:
|
||||||
|
Ring.MISS:
|
||||||
|
return 0
|
||||||
|
Ring.INNER_BULL:
|
||||||
|
return 50
|
||||||
|
Ring.OUTER_BULL:
|
||||||
|
return 25
|
||||||
|
|
||||||
|
var n := _sector_number(slot)
|
||||||
|
if n == 0:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
match ring:
|
||||||
|
Ring.TRIPLE:
|
||||||
|
return n * 3
|
||||||
|
Ring.DOUBLE:
|
||||||
|
return n * 2
|
||||||
|
_:
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
# --- Raycast ------------------------------------------------------------
|
||||||
|
|
||||||
|
func _raycast(screen_pos: Vector2) -> Dictionary:
|
||||||
|
var origin := camera.project_ray_origin(screen_pos)
|
||||||
|
var params := PhysicsRayQueryParameters3D.create(
|
||||||
|
origin,
|
||||||
|
origin + camera.project_ray_normal(screen_pos) * RAY_LENGTH
|
||||||
|
)
|
||||||
|
params.collide_with_areas = false
|
||||||
|
params.collide_with_bodies = true
|
||||||
|
return get_viewport().world_3d.direct_space_state.intersect_ray(params)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://x5kcjofb5pek
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
extends Control
|
||||||
|
|
||||||
|
## Offers a set of random modifiers each round as draggable pieces.
|
||||||
|
## Dropping one on the board applies it and consumes the whole set.
|
||||||
|
|
||||||
|
const SectorSwatch := preload("res://core/ui/sector_swatch.gd")
|
||||||
|
|
||||||
|
@export var drop_catcher: Control
|
||||||
|
@export var pieces_per_round := 3
|
||||||
|
@export var piece_size := Vector2(90, 110)
|
||||||
|
@export var spacing := 24.0
|
||||||
|
@export var bottom_margin := 30.0
|
||||||
|
|
||||||
|
var _pieces: Array[Control] = []
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
spawn_round()
|
||||||
|
|
||||||
|
|
||||||
|
func spawn_round() -> void:
|
||||||
|
_clear_pieces()
|
||||||
|
|
||||||
|
# ModRegistry is the autoload. (Was "ModifierCatalog" — renamed.)
|
||||||
|
var choices: Array = ModRegistry.pick_random(pieces_per_round)
|
||||||
|
if choices.is_empty():
|
||||||
|
push_warning("ModifierTray: registry returned no modifiers.")
|
||||||
|
return
|
||||||
|
|
||||||
|
for mod in choices:
|
||||||
|
var piece := Control.new()
|
||||||
|
piece.set_script(SectorSwatch)
|
||||||
|
piece.custom_minimum_size = piece_size
|
||||||
|
piece.size = piece_size
|
||||||
|
piece.modifier = mod
|
||||||
|
piece.paint_texture = mod.texture
|
||||||
|
piece.drop_catcher = drop_catcher
|
||||||
|
piece.tray = self
|
||||||
|
add_child(piece)
|
||||||
|
_pieces.append(piece)
|
||||||
|
|
||||||
|
_layout_pieces()
|
||||||
|
|
||||||
|
|
||||||
|
func _layout_pieces() -> void:
|
||||||
|
var count := _pieces.size()
|
||||||
|
if count == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
var total_w := count * piece_size.x + (count - 1) * spacing
|
||||||
|
var screen := get_viewport_rect().size
|
||||||
|
var start_x := (screen.x - total_w) * 0.5
|
||||||
|
var y := screen.y - piece_size.y - bottom_margin
|
||||||
|
|
||||||
|
for i in count:
|
||||||
|
_pieces[i].position = Vector2(start_x + i * (piece_size.x + spacing), y)
|
||||||
|
|
||||||
|
|
||||||
|
## Called by a piece when it lands successfully — the whole set is spent.
|
||||||
|
func consume() -> void:
|
||||||
|
_clear_pieces()
|
||||||
|
|
||||||
|
|
||||||
|
func _clear_pieces() -> void:
|
||||||
|
for p in _pieces:
|
||||||
|
if is_instance_valid(p):
|
||||||
|
p.queue_free()
|
||||||
|
_pieces.clear()
|
||||||
|
|
||||||
|
|
||||||
|
# TESTING ONLY — SPACE rolls a new set.
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if event is InputEventKey and event.pressed and event.keycode == KEY_SPACE:
|
||||||
|
spawn_round()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dnek55fao0l72
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
extends Control
|
||||||
|
|
||||||
|
## A draggable modifier piece in the tray. Follows the cursor directly (no
|
||||||
|
## ghost). On release over the board it asks DropCatcher to apply itself.
|
||||||
|
##
|
||||||
|
## Was TrapezoidSwatch.gd — renamed because modifiers now attach to whole
|
||||||
|
## sectors, not individual segments.
|
||||||
|
|
||||||
|
@export var paint_texture: Texture2D:
|
||||||
|
set(value):
|
||||||
|
paint_texture = value
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
@export_range(0.0, 1.0) var top_width_ratio := 0.55
|
||||||
|
|
||||||
|
# Set by the tray when it spawns this piece.
|
||||||
|
var drop_catcher: Control
|
||||||
|
var tray: Node
|
||||||
|
var modifier: Modifier
|
||||||
|
|
||||||
|
var _dragging := false
|
||||||
|
var _home_position := Vector2.ZERO
|
||||||
|
var _drag_offset := Vector2.ZERO
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
if custom_minimum_size == Vector2.ZERO:
|
||||||
|
custom_minimum_size = Vector2(90, 110)
|
||||||
|
size = custom_minimum_size
|
||||||
|
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
var w := size.x
|
||||||
|
var h := size.y
|
||||||
|
var inset := w * (1.0 - top_width_ratio) * 0.5
|
||||||
|
|
||||||
|
var points := PackedVector2Array([
|
||||||
|
Vector2(inset, 0), Vector2(w - inset, 0),
|
||||||
|
Vector2(w, h), Vector2(0, h),
|
||||||
|
])
|
||||||
|
|
||||||
|
if paint_texture != null:
|
||||||
|
var uvs := PackedVector2Array([
|
||||||
|
Vector2(inset / w, 0), Vector2((w - inset) / w, 0),
|
||||||
|
Vector2(1, 1), Vector2(0, 1),
|
||||||
|
])
|
||||||
|
var white := PackedColorArray([Color.WHITE, Color.WHITE, Color.WHITE, Color.WHITE])
|
||||||
|
draw_polygon(points, white, uvs, paint_texture)
|
||||||
|
else:
|
||||||
|
draw_colored_polygon(points, Color(0.8, 0.8, 0.8))
|
||||||
|
|
||||||
|
var outline := points
|
||||||
|
outline.append(points[0])
|
||||||
|
draw_polyline(outline, Color(0, 0, 0, 0.6), 2.0)
|
||||||
|
|
||||||
|
|
||||||
|
func _gui_input(event: InputEvent) -> void:
|
||||||
|
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
|
||||||
|
if event.pressed:
|
||||||
|
_start_drag()
|
||||||
|
else:
|
||||||
|
_end_drag()
|
||||||
|
|
||||||
|
|
||||||
|
func _start_drag() -> void:
|
||||||
|
_dragging = true
|
||||||
|
_home_position = global_position
|
||||||
|
_drag_offset = global_position - get_global_mouse_position()
|
||||||
|
# top_level lets us move by global_position without reparenting.
|
||||||
|
# Reparenting mid-drag breaks the release event.
|
||||||
|
top_level = true
|
||||||
|
z_index = 100
|
||||||
|
modulate.a = 0.85
|
||||||
|
|
||||||
|
|
||||||
|
func _end_drag() -> void:
|
||||||
|
if not _dragging:
|
||||||
|
return
|
||||||
|
_dragging = false
|
||||||
|
modulate.a = 1.0
|
||||||
|
z_index = 0
|
||||||
|
|
||||||
|
var applied := false
|
||||||
|
if drop_catcher == null:
|
||||||
|
push_error("SectorSwatch has no drop_catcher (tray export not set?).")
|
||||||
|
elif not drop_catcher.has_method(&"apply_modifier_at_screen_pos"):
|
||||||
|
push_error("drop_catcher has no apply_modifier_at_screen_pos method.")
|
||||||
|
else:
|
||||||
|
applied = drop_catcher.apply_modifier_at_screen_pos(
|
||||||
|
get_global_mouse_position(), modifier
|
||||||
|
)
|
||||||
|
|
||||||
|
if applied:
|
||||||
|
if tray != null and tray.has_method(&"consume"):
|
||||||
|
tray.consume()
|
||||||
|
else:
|
||||||
|
queue_free()
|
||||||
|
else:
|
||||||
|
# Missed — snap home.
|
||||||
|
top_level = false
|
||||||
|
global_position = _home_position
|
||||||
|
|
||||||
|
|
||||||
|
func _process(_delta: float) -> void:
|
||||||
|
if _dragging:
|
||||||
|
global_position = get_global_mouse_position() + _drag_offset
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://d0cdxnymxd6bs
|
||||||
|
Before Width: | Height: | Size: 994 B After Width: | Height: | Size: 994 B |
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.cte
|
|||||||
compress/mode=0
|
compress/mode=0
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
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/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
@@ -19,14 +19,22 @@ run/main_scene="uid://doh7p84eej78c"
|
|||||||
config/features=PackedStringArray("4.7", "Mobile")
|
config/features=PackedStringArray("4.7", "Mobile")
|
||||||
config/icon="res://icon.svg"
|
config/icon="res://icon.svg"
|
||||||
|
|
||||||
|
[autoload]
|
||||||
|
|
||||||
|
ModRegistry="*uid://btm4dwci50xmu"
|
||||||
|
|
||||||
[display]
|
[display]
|
||||||
|
|
||||||
window/stretch/mode="viewport"
|
window/stretch/mode="viewport"
|
||||||
window/stretch/aspect="expand"
|
window/stretch/aspect="expand"
|
||||||
|
|
||||||
|
[filesystem]
|
||||||
|
|
||||||
|
import/blender/enabled=false
|
||||||
|
|
||||||
[global_group]
|
[global_group]
|
||||||
|
|
||||||
Dart="All throwable darts"
|
darts="All throwable darts"
|
||||||
|
|
||||||
[input]
|
[input]
|
||||||
|
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
extends Camera3D
|
||||||
|
|
||||||
|
## Owns dart input. The camera does the raycast because the raycast IS a
|
||||||
|
## camera operation — projecting a screen point into the world.
|
||||||
|
##
|
||||||
|
## The dart exposes begin_drag / update_drag / release_drag and never looks
|
||||||
|
## for the camera itself. This script decides who to call them on.
|
||||||
|
|
||||||
|
const RAY_LENGTH := 1000.0
|
||||||
|
|
||||||
|
var _held_dart: Dart = null
|
||||||
|
|
||||||
|
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if event is InputEventMouseButton:
|
||||||
|
var mb := event as InputEventMouseButton
|
||||||
|
if mb.button_index != MOUSE_BUTTON_LEFT:
|
||||||
|
return
|
||||||
|
if mb.pressed:
|
||||||
|
_try_grab(mb.position)
|
||||||
|
else:
|
||||||
|
_release()
|
||||||
|
|
||||||
|
elif event is InputEventMouseMotion and _held_dart != null:
|
||||||
|
_held_dart.update_drag(self, (event as InputEventMouseMotion).position)
|
||||||
|
|
||||||
|
|
||||||
|
func _try_grab(screen_pos: Vector2) -> void:
|
||||||
|
var hit := _raycast(screen_pos)
|
||||||
|
if hit.is_empty():
|
||||||
|
return
|
||||||
|
|
||||||
|
var body = hit.get("collider")
|
||||||
|
if body is Dart:
|
||||||
|
_held_dart = body as Dart
|
||||||
|
# NOTE: two args, not three. The old start_drag(camera, mouse_pos, point)
|
||||||
|
# passed mouse_pos but never used it — begin_drag only needs the hit point.
|
||||||
|
_held_dart.begin_drag(self, hit["position"])
|
||||||
|
|
||||||
|
|
||||||
|
func _release() -> void:
|
||||||
|
if _held_dart == null:
|
||||||
|
return
|
||||||
|
_held_dart.release_drag()
|
||||||
|
_held_dart = null
|
||||||
|
|
||||||
|
|
||||||
|
func _raycast(screen_pos: Vector2) -> Dictionary:
|
||||||
|
var params := PhysicsRayQueryParameters3D.create(
|
||||||
|
project_ray_origin(screen_pos),
|
||||||
|
project_ray_origin(screen_pos) + project_ray_normal(screen_pos) * RAY_LENGTH
|
||||||
|
)
|
||||||
|
if _held_dart != null:
|
||||||
|
params.exclude = [_held_dart.get_rid()]
|
||||||
|
|
||||||
|
return get_world_3d().direct_space_state.intersect_ray(params)
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
extends Node3D
|
||||||
|
|
||||||
|
## Scene root. Deliberately tiny.
|
||||||
|
##
|
||||||
|
## Dart input lives in camera_3d.gd (the raycast is a camera operation).
|
||||||
|
## Board behaviour lives in dartboard.gd. Modifier UI lives in core/ui/.
|
||||||
|
## This script wires nothing and owns nothing — it's just the tree root.
|
||||||
|
|
||||||
|
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if event.is_action_pressed(&"restart"):
|
||||||
|
get_tree().reload_current_scene()
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
[gd_scene format=3 uid="uid://doh7p84eej78c"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://geqmah8kbko0" path="res://scenes/main.gd" id="1_sugp2"]
|
||||||
|
[ext_resource type="Script" uid="uid://ds3uiri3oc05v" path="res://scenes/camera_3d.gd" id="2_jyhfs"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://fshohiewm7et" path="res://core/dart/dart.tscn" id="3_tbgi4"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://dojbd5wl7ntp0" path="res://core/board/dartboard.tscn" id="3_tefeu"]
|
||||||
|
[ext_resource type="Script" uid="uid://x5kcjofb5pek" path="res://core/ui/drop_catcher.gd" id="5_o6xl0"]
|
||||||
|
[ext_resource type="Script" uid="uid://dnek55fao0l72" path="res://core/ui/modifier_tray.gd" id="6_tipki"]
|
||||||
|
|
||||||
|
[node name="Main" type="Node3D" unique_id=819165214]
|
||||||
|
script = ExtResource("1_sugp2")
|
||||||
|
|
||||||
|
[node name="Camera3D" type="Camera3D" parent="." unique_id=1570426839]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.3, 2.37)
|
||||||
|
fov = 30.0
|
||||||
|
script = ExtResource("2_jyhfs")
|
||||||
|
|
||||||
|
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1891469178]
|
||||||
|
transform = Transform3D(-0.789482, 0.24223976, -0.56394875, -0.56595325, 0.06825608, 0.821607, 0.23751883, 0.9678125, 0.08320943, -46.816067, 15.942318, -50.370865)
|
||||||
|
|
||||||
|
[node name="Dartboard" parent="." unique_id=408060787 instance=ExtResource("3_tefeu")]
|
||||||
|
|
||||||
|
[node name="Dart" parent="." unique_id=367982453 instance=ExtResource("3_tbgi4")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 0.83676445, 0.5475633, 0, -0.54756325, 0.8367645, 0, -0.43, 1.8)
|
||||||
|
|
||||||
|
[node name="UI" type="CanvasLayer" parent="." unique_id=1068293458]
|
||||||
|
|
||||||
|
[node name="DropCatcher" type="Control" parent="UI" unique_id=1874461265 node_paths=PackedStringArray("camera", "dartboard")]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 0
|
||||||
|
offset_right = 40.0
|
||||||
|
offset_bottom = 40.0
|
||||||
|
script = ExtResource("5_o6xl0")
|
||||||
|
camera = NodePath("../../Camera3D")
|
||||||
|
dartboard = NodePath("../../Dartboard")
|
||||||
|
|
||||||
|
[node name="ModifierTray" type="Control" parent="UI" unique_id=1639987503 node_paths=PackedStringArray("drop_catcher")]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 0
|
||||||
|
offset_right = 40.0
|
||||||
|
offset_bottom = 40.0
|
||||||
|
script = ExtResource("6_tipki")
|
||||||
|
drop_catcher = NodePath("../DropCatcher")
|
||||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 26 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 31 KiB |
Reference in New Issue
Block a user