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
120 changed files with 1371 additions and 1699 deletions
+1 -47
View File
@@ -1,48 +1,2 @@
# ─── Binary assets → LFS ─────────────────────────────────────────── # Normalize EOL for all files that Git considers text files.
*.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
# ─── Lockable files types ──────────────────────────────────────────────
*.glb filter=lfs diff=lfs merge=lfs -text lockable
*.blend filter=lfs diff=lfs merge=lfs -text lockable
*.png filter=lfs diff=lfs merge=lfs -text lockable
*.res filter=lfs diff=lfs merge=lfs -text lockable
+3 -43
View File
@@ -1,43 +1,3 @@
# ─── Godot ───────────────────────────────────────────────────────── # Godot 4+ specific ignores
# Import cache. Regenerates on open. NEVER commit. .godot/
game/.godot/ /android/
# 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*
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -4,12 +4,12 @@ importer="scene"
importer_version=1 importer_version=1
type="PackedScene" type="PackedScene"
uid="uid://fp0gcwcyfd7g" uid="uid://fp0gcwcyfd7g"
path="res://.godot/imported/dart.glb-ca8e61ccf0bb4431c3c990bf6c68da95.scn" path="res://.godot/imported/Dart.glb-82b423f0f21d37c1bf28626083e1030e.scn"
[deps] [deps]
source_file="res://core/dart/dart.glb" source_file="res://Models/Dart/Dart.glb"
dest_files=["res://.godot/imported/dart.glb-ca8e61ccf0bb4431c3c990bf6c68da95.scn"] dest_files=["res://.godot/imported/Dart.glb-82b423f0f21d37c1bf28626083e1030e.scn"]
[params] [params]
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

@@ -2,20 +2,20 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://dj6wb70lh5s4f" uid="uid://bobnv6cyi7dc6"
path.s3tc="res://.godot/imported/dartboard_bulls.png-354417d2f083e713950eed8be5c54f73.s3tc.ctex" path.s3tc="res://.godot/imported/Dart_Dart-Default.png-4bd6068152c7966988ca2c23bd03ba76.s3tc.ctex"
metadata={ metadata={
"imported_formats": ["s3tc_bptc"], "imported_formats": ["s3tc_bptc"],
"vram_texture": true "vram_texture": true
} }
generator_parameters={ generator_parameters={
"md5": "ce1804f3e86fea26503097d5e278c919" "md5": "9c4718c8d2830abe8cc9971ef621b46a"
} }
[deps] [deps]
source_file="res://core/board/dartboard_bulls.png" source_file="res://Models/Dart/Dart_Dart-Default.png"
dest_files=["res://.godot/imported/dartboard_bulls.png-354417d2f083e713950eed8be5c54f73.s3tc.ctex"] dest_files=["res://.godot/imported/Dart_Dart-Default.png-4bd6068152c7966988ca2c23bd03ba76.s3tc.ctex"]
[params] [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" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://bayaus0tgltb3" uid="uid://c7jxj25ophbsb"
path="res://.godot/imported/neon_hiero.png-401e600b4e3a81c5e49b654215a0e850.ctex" path="res://.godot/imported/Dart-Default.png-2feb9f56821a29d976ec19a662a6977f.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://content/official/skins/bulls/neon_hiero.png" source_file="res://Models/Dart/Textures/Dart-Default.png"
dest_files=["res://.godot/imported/neon_hiero.png-401e600b4e3a81c5e49b654215a0e850.ctex"] dest_files=["res://.godot/imported/Dart-Default.png-2feb9f56821a29d976ec19a662a6977f.ctex"]
[params] [params]
Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

@@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://b672fbyuxxktw" uid="uid://bhbtrcw8xbp1g"
path="res://.godot/imported/ocean_fire.png-b1f0d5fea08640de091cb5cf724309e6.ctex" path="res://.godot/imported/Wings-Default.png-b4a7a92ebd237d86daaae21adc10d873.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://content/official/skins/flat_tire/ocean_fire.png" source_file="res://Models/Dart/Textures/Wings-Default.png"
dest_files=["res://.godot/imported/ocean_fire.png-b1f0d5fea08640de091cb5cf724309e6.ctex"] dest_files=["res://.godot/imported/Wings-Default.png-b4a7a92ebd237d86daaae21adc10d873.ctex"]
[params] [params]
Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

@@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://btg3hosfx1sdg" uid="uid://ch4b1pnokuu8d"
path="res://.godot/imported/toon_purple.png-f1a192acdf1c762039bd65e86672f7e4.ctex" path="res://.godot/imported/Wings-Red-Blue.png-701faf0c1b6c3af11eb52058dfda7798.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://content/official/skins/flat_tire/toon_purple.png" source_file="res://Models/Dart/Textures/Wings-Red-Blue.png"
dest_files=["res://.godot/imported/toon_purple.png-f1a192acdf1c762039bd65e86672f7e4.ctex"] dest_files=["res://.godot/imported/Wings-Red-Blue.png-701faf0c1b6c3af11eb52058dfda7798.ctex"]
[params] [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" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://cjw6x7ig1ogbo" uid="uid://dk6gnbvgumq7c"
path.s3tc="res://.godot/imported/dartboard_sector_black.png-39f98bf69c481faad6b8976a0994ecb5.s3tc.ctex" path.s3tc="res://.godot/imported/Dartboard-Proto_Cork.png-9700f4c0b675dee65dc4e1c14aa931b3.s3tc.ctex"
metadata={ metadata={
"imported_formats": ["s3tc_bptc"], "imported_formats": ["s3tc_bptc"],
"vram_texture": true "vram_texture": true
} }
generator_parameters={ generator_parameters={
"md5": "831beecb2a48df4a1f6850e3e8c938dd" "md5": "5b186b11689f911ad6691488292c3475"
} }
[deps] [deps]
source_file="res://core/board/dartboard_sector_black.png" source_file="res://Models/Dartboard/Dartboard-Proto_Cork.png"
dest_files=["res://.godot/imported/dartboard_sector_black.png-39f98bf69c481faad6b8976a0994ecb5.s3tc.ctex"] dest_files=["res://.godot/imported/Dartboard-Proto_Cork.png-9700f4c0b675dee65dc4e1c14aa931b3.s3tc.ctex"]
[params] [params]
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

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

After

Width:  |  Height:  |  Size: 20 KiB

@@ -2,20 +2,20 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://da2elnsphlc5i" uid="uid://cvv4akpwsudu3"
path.s3tc="res://.godot/imported/dartboard_flat_tire.png-97f4bd426c06f71b009cd942664be6fa.s3tc.ctex" path.s3tc="res://.godot/imported/Dartboard-Proto_Red.png-e757930c4a42a8dc67547b3409641a91.s3tc.ctex"
metadata={ metadata={
"imported_formats": ["s3tc_bptc"], "imported_formats": ["s3tc_bptc"],
"vram_texture": true "vram_texture": true
} }
generator_parameters={ generator_parameters={
"md5": "d24ebe44ff702bdbd5528c9d29427568" "md5": "e46ac968aab4da0b0c65bc59957f2f96"
} }
[deps] [deps]
source_file="res://core/board/dartboard_flat_tire.png" source_file="res://Models/Dartboard/Dartboard-Proto_Red.png"
dest_files=["res://.godot/imported/dartboard_flat_tire.png-97f4bd426c06f71b009cd942664be6fa.s3tc.ctex"] dest_files=["res://.godot/imported/Dartboard-Proto_Red.png-e757930c4a42a8dc67547b3409641a91.s3tc.ctex"]
[params] [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" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://b87cl0ullks7x" uid="uid://ctr7mydgfca75"
path.s3tc="res://.godot/imported/dartboard_spider.png-c5fe87c929f66762aaaaefebd3359c9d.s3tc.ctex" path.s3tc="res://.godot/imported/Dartboard_Spider.png-3a7e01e42d10301e2794b3c6268069b4.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://core/board/dartboard_spider.png" source_file="res://Models/Dartboard/Dartboard_Spider.png"
dest_files=["res://.godot/imported/dartboard_spider.png-c5fe87c929f66762aaaaefebd3359c9d.s3tc.ctex"] dest_files=["res://.godot/imported/Dartboard_Spider.png-3a7e01e42d10301e2794b3c6268069b4.s3tc.ctex"]
[params] [params]
Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

@@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://dgq04uh0wal5a" uid="uid://b1vf3204p4ys6"
path="res://.godot/imported/mjolnir.png-5e80731d4fee936aa61acd006a5499ea.ctex" path="res://.godot/imported/Coffee.png-7ec89259677377dfb36274d8ce0e9258.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://content/official/modifiers/mjolnir/mjolnir.png" source_file="res://Models/Dartboard/Textures/Coffee.png"
dest_files=["res://.godot/imported/mjolnir.png-5e80731d4fee936aa61acd006a5499ea.ctex"] dest_files=["res://.godot/imported/Coffee.png-7ec89259677377dfb36274d8ce0e9258.ctex"]
[params] [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"
-191
View File
@@ -1,191 +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.7**. 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.
**NixOS** — 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;
};
```
Or system-wide:
```nix
environment.systemPackages = with pkgs; [ git git-lfs ];
```
**Debian/Ubuntu**`sudo apt install git-lfs`
**macOS**`brew install git-lfs`
Then, once per machine:
```bash
git lfs install
```
---
## Clone
```bash
git clone ssh://git@gitea.lilpenn.org:30009/ced/DartGame.git
cd DartGame
```
> **Note the `git@`.** Without it, SSH tries to log in as your local username and you get
> `Permission denied (publickey)`. Gitea's SSH user is always `git`.
### Verify LFS worked
```bash
file game/core/board/dartboard.glb
```
**Want:** `glTF binary model, version 2, length 3302888 bytes`
**Bad:** `ASCII text` ← that's an unfetched LFS pointer. Stop and fix it before opening Godot.
---
## Setup
```bash
# GDScript formatter + linter
nix-shell -p gdtoolkit_4 # or: pipx install "gdtoolkit==4.*"
# Pre-commit hooks
pre-commit install
```
Then point Godot 4 at `game/project.godot`.
---
## Where things live
| | Nextcloud | This repo |
|---|---|---|
| `.blend`, `.kra`, `.psd` | ✅ | ❌ **never** |
| `.glb`, `.png` (exported) | ❌ | ✅ (LFS) |
| `.res` (collision shapes) | ❌ | ✅ (LFS) |
| Code, scenes, `.tres` | ❌ | ✅ |
| Moodboards, references | ✅ | ❌ |
| **Documentation** | ✅ | ❌ |
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`](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, 21-slot logic, trimesh collision
│ ├── dart/ dart scene, drag/throw/flight physics
│ ├── modding/ mod registry, modifier base class
│ └── ui/ drop catcher, modifier tray, sector swatch
├── content/official/ ← OUR CONTENT, loaded as a mod like any other
│ ├── mod.json
│ ├── skins/ bulls/ · sectors/ · flat_tire/
│ └── modifiers/ (empty — Phase 5)
└── scenes/ main.tscn, camera_3d.gd
```
### 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.
---
## The board is 21 slots
`dartboard.glb` exports 21 material slots: `sector_1``sector_20`, plus `bulls`. Each sector
is its own material, so it can be skinned and modified independently.
Hit detection reads the **mesh face index** from the raycast and maps it to the material name —
*not* an angle. That's what lets a modifier rotate or remap the board and still score correctly.
Full detail in the docs.
---
## Docs
Full documentation is an Obsidian vault at `Nextcloud/DartGame/documentation/`.
**Start with `Build Plan`.** Then `Conventions` before your first commit.
| Note | |
|---|---|
| **Build Plan** | What we're building. Eight phases, current status. |
| **Conventions** | Naming, git workflow, linting. **Read before your first commit.** |
| **Architecture** | How the engine works and why. |
| **Godot Gotchas** | Every trap that cost us time. Read it. |
| **Modding** | The mod format and public API. |
| **Asset Pipeline** | Nextcloud ↔ repo handoff, Blender export settings. |
| **File Tree** | Full directory reference. |
---
## Contributing
**Branches:** `feat/`, `fix/`, `chore/` off `main`. `main` is protected and always runnable.
**Commits:** Conventional Commits.
```
feat(modding): add ModRegistry namespaced lookup
fix(board): face-to-slot map missed the last surface
```
**Before you open a PR:**
- [ ] `gdformat --check game/` passes
- [ ] `gdlint game/` passes
- [ ] Nothing in `core/` references a specific content ID
- [ ] If an asset changed: source updated in Nextcloud, `SOURCES.md` current
- [ ] `git lfs status` shows binaries as LFS
### ⚠️ 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 branch switch:
```bash
rm -rf game/.godot/
```
then Project → Reload Current Project. Worse than that? `git restore .` — which only works if
you committed first.
---
## Status
**Pre-alpha.** Phase 0 (repo hygiene) complete. Phase 1 (skeleton) in progress — board and dart
scenes exist with collision; the throw mechanic is still being tuned.
See `Build Plan` in the docs vault.
-102
View File
@@ -1,102 +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 |
|---|---|
| `game/core/board/dartboard.glb` | `sources/dartboard/dartboard.blend` |
| `game/core/dart/dart.glb` | `sources/dart/dart.blend` |
---
## Fallback textures
These live in `core/`, **not** `content/`. They're the defaults that ship with the mesh so the
board renders correctly with zero mods loaded. They are not skins.
The board has 21 material slots (`sector_1``sector_20`, `bulls`); the 20 sector materials share
the two `sector_*` images by default.
| Repo artifact | Nextcloud source |
|---|---|
| `game/core/board/dartboard_bulls.png` | `sources/dartboard/textures/default/bulls.kra` |
| `game/core/board/dartboard_sector_black.png` | `sources/dartboard/textures/default/sector_black.kra` |
| `game/core/board/dartboard_sector_white.png` | `sources/dartboard/textures/default/sector_white.kra` |
| `game/core/board/dartboard_flat_tire.png` | `sources/dartboard/textures/default/flat_tire.kra` |
| `game/core/board/dartboard_spider.png` | `sources/dartboard/textures/default/spider.kra` |
| `game/core/dart/dart_body.png` | `sources/dart/textures/default/body.kra` |
| `game/core/dart/dart_wings.png` | `sources/dart/textures/default/wings.kra` |
> The `dartboard_*` / `dart_*` prefixes are Godot's — it extracts the textures embedded in the
> `.glb` on import and names them after the model. Don't rename them; the materials reference
> them by that name.
---
## Skins — bulls
| Repo artifact | Nextcloud source |
|---|---|
| `game/content/official/skins/bulls/neon_hiero.png` | `sources/dartboard/textures/bulls_skins/neon_hiero.kra` |
## Skins — sectors
| Repo artifact | Nextcloud source |
|---|---|
| `game/content/official/skins/sectors/neon_hiero_black.png` | `sources/dartboard/textures/sector_skins/neon_hiero_black.kra` |
| `game/content/official/skins/sectors/neon_hiero_white.png` | `sources/dartboard/textures/sector_skins/neon_hiero_white.kra` |
## Skins — flat tire
| Repo artifact | Nextcloud source |
|---|---|
| `game/content/official/skins/flat_tire/ocean_fire.png` | `sources/dartboard/textures/flat_tire_skins/ocean_fire.kra` |
| `game/content/official/skins/flat_tire/toon_purple.png` | `sources/dartboard/textures/flat_tire_skins/toon_purple.kra` |
---
## Generated, not exported
These are produced **by Godot**, not by Blender or Krita. They have no Nextcloud source.
| Artifact | What it is |
|---|---|
| `game/core/board/dartboard_collision.res` | Trimesh (`ConcavePolygonShape3D`), 1358 faces. Generated from the `dartboard` mesh via Mesh → Create Collision Shape → Trimesh, then extracted from the `.tscn` with Shape → Save As. **~9 MB.** |
| `game/core/board/spider_collision.res` | Same, for the spider wire. |
> Regenerate these if the board geometry changes. They are **not** rebuilt automatically on
> re-export.
---
## Not yet exported
Sources that exist in Nextcloud but have no artifact in the repo yet.
| Nextcloud source | Destined for |
|---|---|
| `sources/dart/textures/wings/red_blue.kra` | `content/official/skins/flights/red_blue.png` |
---
## When you re-export
1. Export from the Nextcloud source
2. Drop the artifact into the repo path above
3. Commit the artifact **and its `.import` file** together
4. Update this file if anything new appeared
**Names must match.** `neon_hiero_black.kra``neon_hiero_black.png`. We drifted once already
(`purple_toon.kra` vs `toon_purple.png`) — that's exactly what this file exists to catch.
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
+69
View File
@@ -0,0 +1,69 @@
extends Control
# 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
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()
# 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: catalog returned no modifiers.")
return
for mod in choices:
var piece := Control.new()
piece.set_script(TrapezoidSwatch)
piece.custom_minimum_size = piece_size
piece.size = piece_size
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)
_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 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: 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()
+97
View File
@@ -0,0 +1,97 @@
extends Control
# 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 # 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 # 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
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),
])
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:
_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 freely, ignoring the container
# layout, WITHOUT reparenting (reparenting mid-drag breaks the release event).
top_level = true
z_index = 100 # float above everything during drag
modulate.a = 0.85
func _end_drag() -> void:
if not _dragging:
return
_dragging = false
modulate.a = 1.0
z_index = 0
# 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("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:
painted = drop_catcher.apply_modifier_at_screen_pos(get_global_mouse_position(), modifier)
if painted:
# Used successfully: consume this round's modifier choices.
if tray and tray.has_method("consume"):
tray.consume()
else:
queue_free()
else:
# 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.
@@ -1,14 +0,0 @@
[gd_resource type="Resource" script_class="Modifier" load_steps=3 format=3]
[ext_resource type="Script" path="res://core/modding/modifier.gd" id="1"]
[ext_resource type="Texture2D" path="res://content/official/skins/sectors/neon_hiero_black.png" id="2"]
[resource]
script = ExtResource("1")
id = "test_pink"
title = "Test Marker"
description = "Drops onto any sector and replaces its texture. Proves face-index lookup works."
texture = ExtResource("2")
score_mult = 1.0
score_add = 0
weight = 1.0
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://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.
@@ -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
-279
View File
@@ -1,279 +0,0 @@
class_name Dartboard
extends StaticBody3D
## The board. 22 material slots:
## sector_1 .. sector_20 (20) ← the scoring wedges
## bulls (1) ← inner AND outer bull, they move together
## flat_tire (1) ← the outer band
##
## 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.
signal slot_painted(slot: StringName)
const BULL_SLOT := &"bulls"
const FLAT_TIRE_SLOT := &"flat_tire"
const SECTOR_PREFIX := "sector_"
@onready var mesh_instance: MeshInstance3D = $Model/dartboard
## StringName -> surface index
var _slot_to_surface: Dictionary = {}
## global trimesh face index -> StringName
var _face_to_slot: Dictionary = {}
## surface index -> [first_face, last_face], for debugging
var _surface_face_range: 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. Check the scene tree — expected $Model/dartboard.")
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 — it can't be hit or skinned." % i)
continue
var slot := StringName(mat.resource_name)
if slot == &"":
push_warning("Surface %d's material has no name." % i)
continue
if _slot_to_surface.has(slot):
push_warning("Duplicate slot name '%s' (surfaces %d and %d)."
% [slot, _slot_to_surface[slot], i])
_slot_to_surface[slot] = i
# Sanity: we expect exactly 20 sectors.
var sectors := 0
for slot in _slot_to_surface:
if String(slot).begins_with(SECTOR_PREFIX):
sectors += 1
if sectors != 20:
push_error("Expected 20 sector slots, found %d. Check the Blender material names."
% sectors)
## Build a global-face-index -> slot map.
##
## The physics engine reports face_index as an index into the WHOLE trimesh.
## The mesh stores faces per-surface. create_trimesh_shape() concatenates the
## surfaces in order, so we can walk them accumulating a running face count.
##
## ⚠️ This assumes surface order == trimesh face order. Verified by
## debug_verify_faces(), not assumed. If that assumption ever breaks (a Godot
## version change, a different collision generator), THIS is where it breaks.
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]
var tri_count: int = (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
_surface_face_range[i] = [running, running + tri_count - 1]
running += tri_count
func _cache_radius() -> void:
var aabb := mesh_instance.get_aabb()
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? &"" if unknown.
func slot_for_face(face_index: int) -> StringName:
return _face_to_slot.get(face_index, &"")
## Paint a texture onto one slot.
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]
# get_active_material returns the override if one exists, else the mesh's.
var mat := mesh_instance.get_active_material(surface)
if mat == null:
push_warning("Slot '%s' has no material to duplicate." % slot)
return
# ⚠️ Duplicate, or we mutate the SHARED material from the .glb — which leaks
# the change to every other slot using that material, AND can get written
# back to disk in the editor.
var unique := mat.duplicate() as StandardMaterial3D
if unique == null:
push_warning("Slot '%s' material isn't a StandardMaterial3D." % slot)
return
unique.albedo_texture = texture
unique.albedo_color = Color.WHITE # don't tint the texture
mesh_instance.set_surface_override_material(surface, unique)
slot_painted.emit(slot)
## 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 clear_all_slots() -> void:
for slot in _slot_to_surface:
clear_slot(slot)
func has_slot(slot: StringName) -> bool:
return _slot_to_surface.has(slot)
func is_sector(slot: StringName) -> bool:
return String(slot).begins_with(SECTOR_PREFIX)
## &"sector_20" -> 20. Returns 0 for bulls, flat_tire, or unknown.
func sector_number(slot: StringName) -> int:
if not is_sector(slot):
return 0
return String(slot).trim_prefix(SECTOR_PREFIX).to_int()
func get_slots() -> Array[StringName]:
var out: Array[StringName] = []
out.assign(_slot_to_surface.keys())
return out
func outer_radius() -> float:
return _outer_radius
## Project a LOCAL point onto the board's face plane.
## Returns 2D coords where +Y is "up" on the board.
##
## Your board's AABB is (0.451, 0.451, 0.029) — thin on Z — so the disc lies in
## the X/Y plane. This picks the two fat axes automatically.
func planar_coords(local_point: Vector3) -> Vector2:
var aabb := mesh_instance.get_aabb()
var center := aabb.position + aabb.size * 0.5
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)
## Distance from board centre as a fraction of the outer radius.
func normalized_radius(world_point: Vector3) -> float:
var planar := planar_coords(to_local(world_point))
return planar.length() / _outer_radius if _outer_radius > 0.0 else 999.0
# --- Debug --------------------------------------------------------------
func print_slots() -> void:
var mesh := mesh_instance.mesh
print("=== Dartboard: %d slots, %d faces ===" % [_slot_to_surface.size(), _face_to_slot.size()])
for i in mesh.get_surface_count():
var mat := mesh.surface_get_material(i)
var name_str: String = mat.resource_name if mat else "(none)"
var range_arr: Array = _surface_face_range.get(i, [-1, -1])
print(" surface %2d: %-12s faces %4d%4d" % [i, name_str, range_arr[0], range_arr[1]])
print(" radius: %.4f aabb: %s" % [_outer_radius, mesh_instance.get_aabb().size])
print("=====================================")
## ⚠️ VERIFY THE CORE ASSUMPTION.
##
## Fires a ray at each sector's known centre and checks that face_index maps
## back to the right slot. If surface order != trimesh face order, this catches
## it — and the whole face-index approach is invalid until it's fixed.
##
## Call once from main.gd, read the output, then delete the call.
func debug_verify_faces(camera: Camera3D) -> void:
print("=== Face-index verification ===")
var space := get_world_3d().direct_space_state
var errors := 0
var checks := 0
# March a grid across the board and confirm every hit resolves to a slot
# that actually exists.
const STEPS := 24
for iy in STEPS:
for ix in STEPS:
var u := (float(ix) / (STEPS - 1)) * 2.0 - 1.0 # -1..1
var v := (float(iy) / (STEPS - 1)) * 2.0 - 1.0
# Skip outside the disc
if Vector2(u, v).length() > 0.95:
continue
# A point on the board's face, in local space
var local := Vector3(u * _outer_radius, v * _outer_radius, 0.0)
var world := to_global(local)
# Ray straight at it, from in front
var from := world + global_transform.basis.z * 0.5
var params := PhysicsRayQueryParameters3D.create(from, world - global_transform.basis.z * 0.1)
params.collide_with_bodies = true
var hit := space.intersect_ray(params)
if hit.is_empty():
continue
checks += 1
var face: int = hit.get("face_index", -1)
var slot := slot_for_face(face)
if slot == &"":
errors += 1
if errors <= 5:
print(" ✗ face %d -> no slot (local %.3f, %.3f)" % [face, local.x, local.y])
print(" %d rays hit, %d unmapped faces" % [checks, errors])
if errors == 0:
print(" ✓ Every hit face maps to a slot.")
else:
push_error("Face mapping is BROKEN. %d faces don't map." % errors)
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

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