Compare commits

..
2 Commits
Author SHA256 Message Date
ced 5b29f61df9 stash changes added as intended 2026-07-17 17:52:55 -05:00
ced 66f3c13d61 Added dart board that the dart can get stuck too.
The prototype of the dart board was added to the main scene. Added and
area 3D to the dart board with the group dartable so that the added ray
on the end of the dart can collide with the board. Also added a button
to change camera angles using the c key. The dart only sticks to a
cylinder added to the board so there is no way to detect where and which
segment the dart's ray is currently stuck to.
2026-07-07 14:47:34 -05:00
115 changed files with 1245 additions and 1354 deletions
+1 -41
View File
@@ -1,42 +1,2 @@
# ─── 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 ──────────────────────────────────────────────────
# Normalize EOL for all files that Git considers text files.
* 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
+3 -43
View File
@@ -1,43 +1,3 @@
# ─── Godot ─────────────────────────────────────────────────────────
# Import cache. Regenerates on open. NEVER commit.
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*
# Godot 4+ specific ignores
.godot/
/android/
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -4,12 +4,12 @@ importer="scene"
importer_version=1
type="PackedScene"
uid="uid://fp0gcwcyfd7g"
path="res://.godot/imported/dart.glb-ca8e61ccf0bb4431c3c990bf6c68da95.scn"
path="res://.godot/imported/Dart.glb-82b423f0f21d37c1bf28626083e1030e.scn"
[deps]
source_file="res://core/dart/dart.glb"
dest_files=["res://.godot/imported/dart.glb-ca8e61ccf0bb4431c3c990bf6c68da95.scn"]
source_file="res://Models/Dart/Dart.glb"
dest_files=["res://.godot/imported/Dart.glb-82b423f0f21d37c1bf28626083e1030e.scn"]
[params]
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

@@ -2,20 +2,20 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://dj6wb70lh5s4f"
path.s3tc="res://.godot/imported/dartboard_bulls.png-354417d2f083e713950eed8be5c54f73.s3tc.ctex"
uid="uid://bobnv6cyi7dc6"
path.s3tc="res://.godot/imported/Dart_Dart-Default.png-4bd6068152c7966988ca2c23bd03ba76.s3tc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
generator_parameters={
"md5": "ce1804f3e86fea26503097d5e278c919"
"md5": "9c4718c8d2830abe8cc9971ef621b46a"
}
[deps]
source_file="res://core/board/dartboard_bulls.png"
dest_files=["res://.godot/imported/dartboard_bulls.png-354417d2f083e713950eed8be5c54f73.s3tc.ctex"]
source_file="res://Models/Dart/Dart_Dart-Default.png"
dest_files=["res://.godot/imported/Dart_Dart-Default.png-4bd6068152c7966988ca2c23bd03ba76.s3tc.ctex"]
[params]
Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

+44
View File
@@ -0,0 +1,44 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bqxi7x76uv4tq"
path.s3tc="res://.godot/imported/Dart_Wings-Default.png-358040735cbc551a0d2abb04506c708a.s3tc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
generator_parameters={
"md5": "52ee9a89e7c6ed5f537103a41115551e"
}
[deps]
source_file="res://Models/Dart/Dart_Wings-Default.png"
dest_files=["res://.godot/imported/Dart_Wings-Default.png-358040735cbc551a0d2abb04506c708a.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.

After

Width:  |  Height:  |  Size: 26 KiB

@@ -2,16 +2,16 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://bayaus0tgltb3"
path="res://.godot/imported/neon_hiero.png-401e600b4e3a81c5e49b654215a0e850.ctex"
uid="uid://c7jxj25ophbsb"
path="res://.godot/imported/Dart-Default.png-2feb9f56821a29d976ec19a662a6977f.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"]
source_file="res://Models/Dart/Textures/Dart-Default.png"
dest_files=["res://.godot/imported/Dart-Default.png-2feb9f56821a29d976ec19a662a6977f.ctex"]
[params]
Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

@@ -2,16 +2,16 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://b672fbyuxxktw"
path="res://.godot/imported/ocean_fire.png-b1f0d5fea08640de091cb5cf724309e6.ctex"
uid="uid://bhbtrcw8xbp1g"
path="res://.godot/imported/Wings-Default.png-b4a7a92ebd237d86daaae21adc10d873.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"]
source_file="res://Models/Dart/Textures/Wings-Default.png"
dest_files=["res://.godot/imported/Wings-Default.png-b4a7a92ebd237d86daaae21adc10d873.ctex"]
[params]
Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

@@ -2,16 +2,16 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://btg3hosfx1sdg"
path="res://.godot/imported/toon_purple.png-f1a192acdf1c762039bd65e86672f7e4.ctex"
uid="uid://ch4b1pnokuu8d"
path="res://.godot/imported/Wings-Red-Blue.png-701faf0c1b6c3af11eb52058dfda7798.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"]
source_file="res://Models/Dart/Textures/Wings-Red-Blue.png"
dest_files=["res://.godot/imported/Wings-Red-Blue.png-701faf0c1b6c3af11eb52058dfda7798.ctex"]
[params]
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://c7ysojicqbx2v"
path="res://.godot/imported/Dartboard-Proto.glb-f1487085b0c09a3b97c6aa8be3864f01.scn"
[deps]
source_file="res://Models/Dartboard/Dartboard-Proto.glb"
dest_files=["res://.godot/imported/Dartboard-Proto.glb-f1487085b0c09a3b97c6aa8be3864f01.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
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@@ -2,20 +2,20 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://cjw6x7ig1ogbo"
path.s3tc="res://.godot/imported/dartboard_sector_black.png-39f98bf69c481faad6b8976a0994ecb5.s3tc.ctex"
uid="uid://dk6gnbvgumq7c"
path.s3tc="res://.godot/imported/Dartboard-Proto_Cork.png-9700f4c0b675dee65dc4e1c14aa931b3.s3tc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
generator_parameters={
"md5": "831beecb2a48df4a1f6850e3e8c938dd"
"md5": "5b186b11689f911ad6691488292c3475"
}
[deps]
source_file="res://core/board/dartboard_sector_black.png"
dest_files=["res://.godot/imported/dartboard_sector_black.png-39f98bf69c481faad6b8976a0994ecb5.s3tc.ctex"]
source_file="res://Models/Dartboard/Dartboard-Proto_Cork.png"
dest_files=["res://.godot/imported/Dartboard-Proto_Cork.png-9700f4c0b675dee65dc4e1c14aa931b3.s3tc.ctex"]
[params]
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@@ -2,20 +2,20 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://cry28pcmt01gk"
path.s3tc="res://.godot/imported/dartboard_sector_white.png-6dbf28d122f3327d568bcbb29e7f70a0.s3tc.ctex"
uid="uid://cs381hglod4k1"
path.s3tc="res://.godot/imported/Dartboard-Proto_Green.png-0f939f85106a12f4ae54352c096222a2.s3tc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
generator_parameters={
"md5": "aa25e4080cfa45ac8064cbf217904fb3"
"md5": "c03f264d9d0115e28f662ed5ab27bbb2"
}
[deps]
source_file="res://core/board/dartboard_sector_white.png"
dest_files=["res://.godot/imported/dartboard_sector_white.png-6dbf28d122f3327d568bcbb29e7f70a0.s3tc.ctex"]
source_file="res://Models/Dartboard/Dartboard-Proto_Green.png"
dest_files=["res://.godot/imported/Dartboard-Proto_Green.png-0f939f85106a12f4ae54352c096222a2.s3tc.ctex"]
[params]
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@@ -2,20 +2,20 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://da2elnsphlc5i"
path.s3tc="res://.godot/imported/dartboard_flat_tire.png-97f4bd426c06f71b009cd942664be6fa.s3tc.ctex"
uid="uid://cvv4akpwsudu3"
path.s3tc="res://.godot/imported/Dartboard-Proto_Red.png-e757930c4a42a8dc67547b3409641a91.s3tc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
generator_parameters={
"md5": "d24ebe44ff702bdbd5528c9d29427568"
"md5": "e46ac968aab4da0b0c65bc59957f2f96"
}
[deps]
source_file="res://core/board/dartboard_flat_tire.png"
dest_files=["res://.godot/imported/dartboard_flat_tire.png-97f4bd426c06f71b009cd942664be6fa.s3tc.ctex"]
source_file="res://Models/Dartboard/Dartboard-Proto_Red.png"
dest_files=["res://.godot/imported/Dartboard-Proto_Red.png-e757930c4a42a8dc67547b3409641a91.s3tc.ctex"]
[params]
Binary file not shown.
Binary file not shown.
Binary file not shown.
+45
View File
@@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://6nml3641dpce"
path="res://.godot/imported/Dartboard.glb-35f0707e0e9b18915b2d48c5c14ac862.scn"
[deps]
source_file="res://Models/Dartboard/Dartboard.glb"
dest_files=["res://.godot/imported/Dartboard.glb-35f0707e0e9b18915b2d48c5c14ac862.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
Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

@@ -2,8 +2,8 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://b87cl0ullks7x"
path.s3tc="res://.godot/imported/dartboard_spider.png-c5fe87c929f66762aaaaefebd3359c9d.s3tc.ctex"
uid="uid://ctr7mydgfca75"
path.s3tc="res://.godot/imported/Dartboard_Spider.png-3a7e01e42d10301e2794b3c6268069b4.s3tc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
@@ -14,8 +14,8 @@ generator_parameters={
[deps]
source_file="res://core/board/dartboard_spider.png"
dest_files=["res://.godot/imported/dartboard_spider.png-c5fe87c929f66762aaaaefebd3359c9d.s3tc.ctex"]
source_file="res://Models/Dartboard/Dartboard_Spider.png"
dest_files=["res://.godot/imported/Dartboard_Spider.png-3a7e01e42d10301e2794b3c6268069b4.s3tc.ctex"]
[params]
Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

@@ -2,16 +2,16 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://brqn2ve8pm3i8"
path="res://.godot/imported/neon_hiero_black.png-7709dfdedde53a261eb17c86736a737f.ctex"
uid="uid://b1vf3204p4ys6"
path="res://.godot/imported/Coffee.png-7ec89259677377dfb36274d8ce0e9258.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"]
source_file="res://Models/Dartboard/Textures/Coffee.png"
dest_files=["res://.godot/imported/Coffee.png-7ec89259677377dfb36274d8ce0e9258.ctex"]
[params]
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://blvifdtblwcxg"
path="res://.godot/imported/Cork.png-8b577bb6a5aa8a82bae563118e794e18.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Models/Dartboard/Textures/Cork.png"
dest_files=["res://.godot/imported/Cork.png-8b577bb6a5aa8a82bae563118e794e18.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.

After

Width:  |  Height:  |  Size: 437 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dxxon6pdl5aw4"
path="res://.godot/imported/Flowers.jpg-4755265eb848ca410e8aa3e15ab13280.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Models/Dartboard/Textures/Flowers.jpg"
dest_files=["res://.godot/imported/Flowers.jpg-4755265eb848ca410e8aa3e15ab13280.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.

After

Width:  |  Height:  |  Size: 20 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c2qvwu0v2uolj"
path="res://.godot/imported/Green.png-6f1f5b0c58d2ec51c6b638c11ba4264b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Models/Dartboard/Textures/Green.png"
dest_files=["res://.godot/imported/Green.png-6f1f5b0c58d2ec51c6b638c11ba4264b.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.

After

Width:  |  Height:  |  Size: 20 KiB

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b1lclw6s66ou7"
path="res://.godot/imported/Red.png-5fe46449e66850590891dfad1c596ebf.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Models/Dartboard/Textures/Red.png"
dest_files=["res://.godot/imported/Red.png-5fe46449e66850590891dfad1c596ebf.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.

After

Width:  |  Height:  |  Size: 74 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b3qw07lguggu6"
path="res://.godot/imported/Spider.png-3f1c947c2e5bcf6b7d7e259d62e6005a.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Models/Dartboard/Textures/Spider.png"
dest_files=["res://.godot/imported/Spider.png-3f1c947c2e5bcf6b7d7e259d62e6005a.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.

After

Width:  |  Height:  |  Size: 83 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ccoht16d1pkr3"
path="res://.godot/imported/Stars.jpg-9cc50027cae80115a1f7344b30169970.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Models/Dartboard/Textures/Stars.jpg"
dest_files=["res://.godot/imported/Stars.jpg-9cc50027cae80115a1f7344b30169970.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
+13
View File
@@ -0,0 +1,13 @@
[gd_resource type="Resource" script_class="Modifier" format=3 uid="uid://ccyxtrth046pt"]
[ext_resource type="Script" uid="uid://duh0lxb7gdimr" path="res://Scripts/Modifier.gd" id="1_6fe4u"]
[ext_resource type="Texture2D" uid="uid://b1vf3204p4ys6" path="res://Models/Dartboard/Textures/Coffee.png" id="2_6fe4u"]
[resource]
script = ExtResource("1_6fe4u")
id = "0"
title = "Stars"
description = "Adds 40 to your score"
texture = ExtResource("2_6fe4u")
score_add = 40
metadata/_custom_type_script = "uid://duh0lxb7gdimr"
+13
View File
@@ -0,0 +1,13 @@
[gd_resource type="Resource" script_class="Modifier" format=3 uid="uid://bu11ohgune4yf"]
[ext_resource type="Script" uid="uid://duh0lxb7gdimr" path="res://Scripts/Modifier.gd" id="1_urlsx"]
[ext_resource type="Texture2D" uid="uid://dxxon6pdl5aw4" path="res://Models/Dartboard/Textures/Flowers.jpg" id="2_urlsx"]
[resource]
script = ExtResource("1_urlsx")
id = "0"
title = "Stars"
description = "Adds 40 to your score"
texture = ExtResource("2_urlsx")
score_add = 40
metadata/_custom_type_script = "uid://duh0lxb7gdimr"
+13
View File
@@ -0,0 +1,13 @@
[gd_resource type="Resource" script_class="Modifier" format=3 uid="uid://bigyh63smlbh5"]
[ext_resource type="Script" uid="uid://duh0lxb7gdimr" path="res://Scripts/Modifier.gd" id="1_oxvfs"]
[ext_resource type="Texture2D" uid="uid://ccoht16d1pkr3" path="res://Models/Dartboard/Textures/Stars.jpg" id="2_5aq3x"]
[resource]
script = ExtResource("1_oxvfs")
id = "0"
title = "Stars"
description = "Adds 40 to your score"
texture = ExtResource("2_5aq3x")
score_add = 40
metadata/_custom_type_script = "uid://duh0lxb7gdimr"
-160
View File
@@ -1,160 +0,0 @@
# 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
View File
@@ -1,65 +0,0 @@
# 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.
+27
View File
@@ -0,0 +1,27 @@
[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.00214429)
[node name="RayCast3D" type="RayCast3D" parent="RigidBody3D" unique_id=892030560]
transform = Transform3D(1, 0, 0, 0, -1, 8.742278e-08, 0, -8.742278e-08, -1, 0, 0.9578117, 0)
collide_with_areas = true
+164
View File
@@ -0,0 +1,164 @@
extends RigidBody3D
class_name Dart
#ray cast
@onready var point = $RayCast3D
#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 = 64.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.m
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 point.is_colliding():
var collider = point.get_collider()
if collider and collider.is_in_group("dartable"):
in_flight = false
dragging = false
await get_tree().create_timer(0.01).timeout
freeze_mode = RigidBody3D.FREEZE_MODE_STATIC
freeze = true
elif in_flight:
# 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
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
View File
@@ -0,0 +1 @@
uid://8kk531f88oqw
+59
View File
@@ -0,0 +1,59 @@
[gd_scene format=3 uid="uid://cqe6bo8v564dc"]
[ext_resource type="Script" uid="uid://d1i0lhibtflvq" path="res://Scripts/Main.gd" id="1_fq6v0"]
[ext_resource type="Script" uid="uid://x5kcjofb5pek" path="res://Scripts/DropCatcher.gd" id="3_qdhrm"]
[ext_resource type="Script" uid="uid://dnek55fao0l72" path="res://Scripts/ModifierTray.gd" id="5_kax6v"]
[ext_resource type="PackedScene" uid="uid://6nml3641dpce" path="res://models/Dartboard/Dartboard.glb" id="5_thh7l"]
[sub_resource type="CylinderShape3D" id="CylinderShape3D_qu08v"]
height = 0.18115234
radius = 0.96484375
[node name="DartboardProto" type="Node3D" unique_id=1829621191 node_paths=PackedStringArray("dartboard_root")]
script = ExtResource("1_fq6v0")
dartboard_root = NodePath("Dartboard")
[node name="Camera3D" type="Camera3D" parent="." unique_id=393502700]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 2.6578834, 0.6292654, 0)
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1020512688]
[node name="Dartboard" parent="." unique_id=1632971704 groups=["dartable"] instance=ExtResource("5_thh7l")]
transform = Transform3D(-4.371139e-08, 0, -1, 0, 1, 0, 1, 0, -4.371139e-08, 0.16272092, 1.2674973, 0)
[node name="Area3D" type="Area3D" parent="Dartboard" unique_id=470399503 groups=["dartable"]]
[node name="CollisionShape3D" type="CollisionShape3D" parent="Dartboard/Area3D" unique_id=2058320974]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 0.003960803, 0.0015273094, 0.0637207)
shape = SubResource("CylinderShape3D_qu08v")
[node name="CanvasLayer" type="CanvasLayer" parent="." unique_id=80012783]
[node name="DropCatcher" type="Control" parent="CanvasLayer" unique_id=1371111637 node_paths=PackedStringArray("camera", "main")]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("3_qdhrm")
camera = NodePath("../../Camera3D")
main = NodePath("../..")
reverse_direction = true
triple_max = 0.5
outer_max = 0.7
[node name="TextureTray" type="HBoxContainer" parent="CanvasLayer/DropCatcher" unique_id=799936408]
layout_mode = 0
offset_right = 40.0
offset_bottom = 40.0
[node name="ModifierTray" type="Control" parent="CanvasLayer" unique_id=2127296352 node_paths=PackedStringArray("drop_catcher")]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("5_kax6v")
drop_catcher = NodePath("../DropCatcher")
+27
View File
@@ -0,0 +1,27 @@
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)
+13
View File
@@ -0,0 +1,13 @@
extends Node3D
@onready var dart_camera = $Camera3D
@onready var board_camera = $Board_camera
var camera: bool = false
func _unhandled_input(event):
if event.is_action_pressed("restart"):
get_tree().reload_current_scene()
if event.is_action_pressed("camera"):
dart_camera.current = camera
board_camera.current = !camera
camera = !camera
+47
View File
@@ -0,0 +1,47 @@
[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"]
[ext_resource type="PackedScene" uid="uid://cqe6bo8v564dc" path="res://Scenes/Dartboard-Proto.tscn" id="4_jjvhh"]
[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("")
[node name="DartboardProto" parent="." unique_id=1829621191 groups=["dartable"] instance=ExtResource("4_jjvhh")]
transform = Transform3D(-2.1855695e-07, 0, 5, 0, 5, 0, -5, 0, -2.1855695e-07, -0.38649988, 1.5309646, 18.770962)
[node name="Board_camera" type="Camera3D" parent="." unique_id=1404073028]
transform = Transform3D(-0.6146763, 0, -0.78877944, 0, 1, 0, 0.78877944, 0, -0.6146763, -8.792414, 7.5380507, 13.506961)
+156
View File
@@ -0,0 +1,156 @@
extends Control
# DropCatcher.gd (manual-drag version)
# No longer uses Godot's built-in _drop_data. Instead it exposes
# paint_at_screen_pos(), which the trapezoid calls on release.
# This Control no longer needs to catch mouse events at all, so it can be
# tiny/invisible — but keep it in the tree so it can reach the camera & board.
@export var camera: Camera3D
@export var main: Node # the ROOT node (DartboardProto)
const RAY_LENGTH := 1000.0
var dartboard: MeshInstance3D
# Standard dartboard number sequence, clockwise starting from the TOP (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]
# Calibration: rotate which sector counts as "top". If the wrong number gets
# hit, bump this by 1 until it lines up. Range 0-19.
@export var sector_offset := 0
# If your board's numbers go counter-clockwise in 3D, flip this.
@export var reverse_direction := false
# --- Radial band boundaries (fractions of outer radius, 0=center 1=rim) ---
# Tune these in the Inspector by reading the norm= prints for each ring.
@export var bull_inner_max := 0.04 # <= this = inner bull (50)
@export var bull_outer_max := 0.09 # <= this = outer bull (25)
@export var inner_max := 0.45 # <= this = inner single
@export var triple_max := 0.55 # <= this = triple ring
@export var outer_max := 0.80 # <= this = outer single; above = double
# Stores which modifier is applied to each surface index: { surface_index: Modifier }
var applied_modifiers: Dictionary = {}
func _ready() -> void:
# Don't block mouse input; the trapezoids handle their own dragging.
mouse_filter = Control.MOUSE_FILTER_IGNORE
print("DropCatcher ready. camera=", camera, " main=", main)
func _get_board() -> MeshInstance3D:
if dartboard == null and main != null and "dartboard" in main:
dartboard = main.dartboard
return dartboard
# Public: try to apply a modifier (paint its texture + store its rule) at a
# screen position. Returns true if it hit the board, false if it missed.
func apply_modifier_at_screen_pos(screen_pos: Vector2, modifier: Resource) -> bool:
var board := _get_board()
if board == null:
push_error("DropCatcher: no dartboard mesh (is 'main' set?).")
return false
if camera == null:
push_error("DropCatcher: camera export not set.")
return false
if modifier == null:
return false
var ray_origin := camera.project_ray_origin(screen_pos)
var ray_dir := camera.project_ray_normal(screen_pos)
var ray_end := ray_origin + ray_dir * RAY_LENGTH
var space_state := get_viewport().world_3d.direct_space_state
var query := PhysicsRayQueryParameters3D.create(ray_origin, ray_end)
query.collide_with_areas = false
query.collide_with_bodies = true
var result := space_state.intersect_ray(query)
if result.is_empty():
print(" drop missed the board")
return false
var surface_index := _surface_from_hit(result, board)
if surface_index < 0:
return false
_paint_surface(board, surface_index, modifier.texture)
applied_modifiers[surface_index] = modifier # remember the scoring rule
print(" applied '", modifier.id, "' to surface ", surface_index)
return true
# Call this when a dart lands on a surface to get the modified score.
func score_for_surface(surface_index: int, base_value: int) -> int:
if applied_modifiers.has(surface_index):
return applied_modifiers[surface_index].apply_score(base_value)
return base_value
func _surface_from_hit(result: Dictionary, board: MeshInstance3D) -> int:
var hit_point: Vector3 = result.get("position", Vector3.ZERO)
var local := board.to_local(hit_point)
var aabb := board.get_aabb()
var center := aabb.position + aabb.size * 0.5
# Board disc lies in the X/Y plane (thin on Z). Measure radius/angle in X/Y.
var dx := local.x - center.x
var dy := local.y - center.y
var radius := Vector2(dx, dy).length()
var outer_radius : float = max(aabb.size.x, aabb.size.y) * 0.5
var norm := radius / outer_radius if outer_radius > 0.0 else 0.0
print(" local=", local, " dx=", "%.3f" % dx, " dy=", "%.3f" % dy, " norm=", "%.3f" % norm)
# --- RADIAL BAND ---
# These thresholds are fractions of the outer radius. TUNE to match your art:
# the real proportions of a dartboard, from center outward.
var seg_name := ""
if norm <= bull_inner_max:
seg_name = "bull_inner" # inner bull (50)
elif norm <= bull_outer_max:
seg_name = "bull_outer" # outer bull (25)
elif norm > 1.0:
print(" outside board")
return -1
else:
# --- ANGULAR SECTOR ---
# atan2 gives angle of the hit point. We map it to one of 20 sectors,
# then index into the dartboard number sequence.
var angle := atan2(dx, dy) # -PI..PI, 0 = straight up (+y = top)
# Convert to 0..1 going clockwise from top. Adjust the rotation so 0 = top.
var t := angle / (2.0 * PI)
t = fposmod(t, 1.0)
if reverse_direction:
t = 1.0 - t
var sector_index := int(floor(t * 20.0 + 0.5)) % 20
sector_index = (sector_index + sector_offset) % 20
var number : int = DART_NUMBERS[sector_index]
# Which ring band within the sector.
var band := ""
if norm <= inner_max:
band = "inner" # inner single
elif norm <= triple_max:
band = "triple" # triple ring
elif norm <= outer_max:
band = "outer" # outer single
else:
band = "double" # double ring
seg_name = "seg_%d_%s" % [number, band]
print(" norm=", "%.3f" % norm, " -> ", seg_name)
return _index_of_surface_named(board, seg_name)
func _index_of_surface_named(board: MeshInstance3D, target_name: String) -> int:
var mesh := board.mesh
for i in mesh.get_surface_count():
var mat := mesh.surface_get_material(i)
if mat and mat.resource_name == target_name:
return i
return -1
func _paint_surface(board: MeshInstance3D, surface_index: int, tex: Texture2D) -> void:
# Always build a fresh StandardMaterial3D so the texture shows as an IMAGE,
# not tinted by the old base color. If the segment looks like a flat color
# instead of the pattern, the mesh is missing proper UVs on that face.
var new_mat := StandardMaterial3D.new()
new_mat.albedo_color = Color.WHITE # white so texture isn't tinted
new_mat.albedo_texture = tex
new_mat.texture_filter = BaseMaterial3D.TEXTURE_FILTER_LINEAR
new_mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
board.set_surface_override_material(surface_index, new_mat)
+61
View File
@@ -0,0 +1,61 @@
extends Node3D
# Attach to the ROOT node (DartboardProto).
# Leave `dartboard_root` empty to auto-search, or drag the glb node in.
@export var dartboard_root: Node
var dartboard: MeshInstance3D
func _ready() -> void:
dartboard = _find_mesh_instance(dartboard_root if dartboard_root else self)
if dartboard == null:
push_error("Could not find a MeshInstance3D. Check that the glb is in the scene.")
return
print("Found dartboard mesh node: ", dartboard.name)
_print_surfaces()
_ensure_collision()
func _find_mesh_instance(node: Node) -> MeshInstance3D:
if node is MeshInstance3D and node.mesh != null:
return node
for child in node.get_children():
var found := _find_mesh_instance(child)
if found:
return found
return null
func _print_surfaces() -> void:
var mesh := dartboard.mesh
print("=== Dartboard surfaces ===")
for i in mesh.get_surface_count():
var mat := mesh.surface_get_material(i)
var mat_name := mat.resource_name if mat else "(no material)"
print("Surface ", i, " material name: '", mat_name, "'")
print("==========================")
var aabb := dartboard.get_aabb()
print("Board local size (x,y,z): ", aabb.size)
# CHEAP collision: one box that matches the board's bounding box.
# This replaces create_trimesh_collision(), which was making it run at ~3 FPS
# because it built a collision triangle for every face of the board.
func _ensure_collision() -> void:
# Remove any existing auto-added body so we don't stack shapes on re-runs.
for child in dartboard.get_children():
if child is StaticBody3D and child.name == "AutoBoardBody":
child.queue_free()
var body := StaticBody3D.new()
body.name = "AutoBoardBody"
var col := CollisionShape3D.new()
var box := BoxShape3D.new()
# Match the mesh's bounding box. Use the AABB size directly as the box size.
var aabb := dartboard.get_aabb()
box.size = aabb.size
col.shape = box
# Center the box on the mesh's bounding-box center (not always the origin).
col.position = aabb.position + aabb.size * 0.5
body.add_child(col)
dartboard.add_child(body)
print("Created cheap box collision for the dartboard.")
+1
View File
@@ -0,0 +1 @@
uid://d1i0lhibtflvq
+84
View File
@@ -0,0 +1,84 @@
extends Node
# ModifierCatalog.gd
# Holds the master list of every modifier in the game and picks random sets
# for each round. Register this as an AUTOLOAD (Project > Project Settings >
# Autoload) with the name "ModifierCatalog" so any script can reach it globally.
#
# Two ways to fill the catalog:
# A) Drop .tres Modifier files into res://Modifiers/ and they auto-load (below).
# B) Build them in code in _register_builtins() for quick prototyping.
var modifiers: Array[Modifier] = []
var _by_id: Dictionary = {} # id -> Modifier, for fast lookup
func _ready() -> void:
_load_from_folder("res://Modifiers/")
if modifiers.is_empty():
# Fallback so the prototype works before you author .tres files.
_register_builtins()
_reindex()
print("ModifierCatalog loaded ", modifiers.size(), " modifiers.")
# Load every .tres Modifier resource from a folder.
func _load_from_folder(path: String) -> void:
var dir := DirAccess.open(path)
if dir == null:
return
dir.list_dir_begin()
var fname := dir.get_next()
while fname != "":
if not dir.current_is_dir() and fname.ends_with(".tres"):
var res := load(path + fname)
if res is Modifier:
modifiers.append(res)
fname = dir.get_next()
dir.list_dir_end()
# Quick code-defined modifiers for prototyping. Replace texture paths with yours.
func _register_builtins() -> void:
modifiers.append(_make("stars_x5", "Stars", "Multiplies this segment's value by 5.",
"res://Models/Dartboard/Dartboard-Proto_Red.png", 5.0, 0))
modifiers.append(_make("double", "Double", "Doubles this segment's value.",
"res://Models/Dartboard/Dartboard-Proto_Green.png", 2.0, 0))
modifiers.append(_make("plus10", "Bonus +10", "Adds 10 points to this segment.",
"res://Models/Dartboard/Dartboard-Proto_Cork.png", 1.0, 10))
func _make(id: String, title: String, desc: String, tex_path: String,
mult: float, add: int) -> Modifier:
var m := Modifier.new()
m.id = id
m.title = title
m.description = desc
m.texture = load(tex_path)
m.score_mult = mult
m.score_add = add
return m
func _reindex() -> void:
_by_id.clear()
for m in modifiers:
_by_id[m.id] = m
func get_by_id(id: String) -> Modifier:
return _by_id.get(id)
# Pick `count` distinct random modifiers, respecting weight. Returns an Array.
func pick_random(count: int) -> Array[Modifier]:
var pool := modifiers.duplicate()
var chosen: Array[Modifier] = []
count = min(count, pool.size())
for _i in count:
var total := 0.0
for m in pool:
total += max(m.weight, 0.0001)
var r := randf() * total
var acc := 0.0
var picked_index := 0
for j in pool.size():
acc += max(pool[j].weight, 0.0001)
if r <= acc:
picked_index = j
break
chosen.append(pool[picked_index])
pool.remove_at(picked_index) # distinct: don't pick the same one twice
return chosen
@@ -1,40 +1,40 @@
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")
# ModifierTray.gd (catalog-driven)
# Each round, asks ModifierCatalog for 3 random modifiers and spawns a draggable
# trapezoid for each at the bottom-center. Dropping one onto the board applies it
# and consumes the round's pieces.
@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] = []
const TrapezoidSwatch := preload("res://Scripts/TrapezoidSwatch.gd")
var _pieces: Array[Control] = []
func _ready() -> void:
mouse_filter = Control.MOUSE_FILTER_IGNORE
spawn_round()
# Offer a fresh set of random modifier pieces for a new round.
func spawn_round() -> void:
_clear_pieces()
# ModRegistry is the autoload. (Was "ModifierCatalog" — renamed.)
var choices: Array = ModRegistry.pick_random(pieces_per_round)
# ModifierCatalog is an autoload singleton (set it up in Project Settings).
var choices: Array = ModifierCatalog.pick_random(pieces_per_round)
if choices.is_empty():
push_warning("ModifierTray: registry returned no modifiers.")
push_warning("ModifierTray: catalog returned no modifiers.")
return
for mod in choices:
var piece := Control.new()
piece.set_script(SectorSwatch)
piece.set_script(TrapezoidSwatch)
piece.custom_minimum_size = piece_size
piece.size = piece_size
piece.modifier = mod
piece.paint_texture = mod.texture
piece.modifier = mod # carries the WHOLE modifier
piece.paint_texture = mod.texture # convenience for drawing
piece.drop_catcher = drop_catcher
piece.tray = self
add_child(piece)
@@ -42,34 +42,28 @@ func spawn_round() -> void:
_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.
# Called by a piece when successfully used: remove the whole set.
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.
# TESTING ONLY: press SPACE to roll a new round of pieces.
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and event.keycode == KEY_SPACE:
spawn_round()
@@ -1,27 +1,25 @@
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.
# TrapezoidSwatch.gd (manual-drag version)
# The trapezoid node ITSELF follows the cursor while dragging (no ghost).
# On release over the board, it tells DropCatcher to paint the hit segment,
# then asks the tray to consume the round's modifiers.
@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 drop_catcher: Control # the DropCatcher node (does the 3D raycast+paint)
var tray: Node # the ModifierTray (so we can consume on use)
# The full modifier this piece carries (texture + title + scoring rule).
var modifier: Resource
var _dragging := false
var _home_position := Vector2.ZERO
var _drag_offset := Vector2.ZERO
var _home_position := Vector2.ZERO # where it sits in the tray
var _drag_offset := Vector2.ZERO # cursor-to-corner offset so it doesn't snap
func _ready() -> void:
mouse_filter = Control.MOUSE_FILTER_STOP
@@ -29,32 +27,26 @@ func _ready() -> void:
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)
draw_polygon(points, PackedColorArray([Color.WHITE, Color.WHITE, Color.WHITE, Color.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:
@@ -62,18 +54,16 @@ func _gui_input(event: InputEvent) -> void:
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 lets us move by global_position freely, ignoring the container
# layout, WITHOUT reparenting (reparenting mid-drag breaks the release event).
top_level = true
z_index = 100
z_index = 100 # float above everything during drag
modulate.a = 0.85
func _end_drag() -> void:
if not _dragging:
return
@@ -81,27 +71,27 @@ func _end_drag() -> void:
modulate.a = 1.0
z_index = 0
var applied := false
# Ask the DropCatcher to try painting wherever we released.
var painted := false
print("PIECE released. drop_catcher=", drop_catcher, " modifier=", modifier)
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("Piece has no drop_catcher reference! (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
)
painted = drop_catcher.apply_modifier_at_screen_pos(get_global_mouse_position(), modifier)
if applied:
if tray != null and tray.has_method(&"consume"):
if painted:
# Used successfully: consume this round's modifier choices.
if tray and tray.has_method("consume"):
tray.consume()
else:
queue_free()
else:
# Missed — snap home.
# Missed the board: snap back home.
top_level = false
global_position = _home_position
func _process(_delta: float) -> void:
if _dragging:
global_position = get_global_mouse_position() + _drag_offset
+15
View File
@@ -0,0 +1,15 @@
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
View File
@@ -0,0 +1 @@
uid://ckbdoamrm313u
+50
View File
@@ -0,0 +1,50 @@
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
+16
View File
@@ -0,0 +1,16 @@
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
View File
@@ -0,0 +1 @@
uid://cc5yy1yc3j2uu
-9
View File
@@ -1,9 +0,0 @@
{
"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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,40 +0,0 @@
[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
-173
View File
@@ -1,173 +0,0 @@
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("=======================")
-1
View File
@@ -1 +0,0 @@
uid://cme5xqn2hlrlu
Binary file not shown.
-27
View File
@@ -1,27 +0,0 @@
[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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-192
View File
@@ -1,192 +0,0 @@
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
View File
Binary file not shown.
-21
View File
@@ -1,21 +0,0 @@
[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.

Some files were not shown because too many files have changed in this diff Show More