Compare commits
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
c221ec6c2a | ||
|
|
69f2028ce3 | ||
|
|
e65c48c1e3 | ||
|
|
a2331c3b94 |
@@ -1,2 +1,48 @@
|
|||||||
# Normalize EOL for all files that Git considers text files.
|
# ─── Binary assets → LFS ───────────────────────────────────────────
|
||||||
|
*.glb filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.gltf filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.fbx filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.obj filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|
||||||
|
*.png filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.jpg filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.jpeg filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.webp filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.exr filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.hdr filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.svg -filter -diff -merge text
|
||||||
|
|
||||||
|
*.ogg filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.wav filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.mp3 filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|
||||||
|
*.ttf filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.otf filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|
||||||
|
# Godot binary resources (collision shapes, baked data)
|
||||||
|
*.res filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.scn filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|
||||||
|
# ─── Line endings ──────────────────────────────────────────────────
|
||||||
* text=auto eol=lf
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
*.gd text eol=lf
|
||||||
|
*.tscn text eol=lf
|
||||||
|
*.tres text eol=lf
|
||||||
|
*.godot text eol=lf
|
||||||
|
*.cfg text eol=lf
|
||||||
|
*.import text eol=lf
|
||||||
|
*.uid text eol=lf
|
||||||
|
*.json text eol=lf
|
||||||
|
*.md text eol=lf
|
||||||
|
*.sh text eol=lf
|
||||||
|
|
||||||
|
# ─── Don't diff these ──────────────────────────────────────────────
|
||||||
|
*.import -diff
|
||||||
|
*.uid -diff
|
||||||
|
|
||||||
|
# ─── 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
|
||||||
|
|||||||
@@ -1,3 +1,43 @@
|
|||||||
# Godot 4+ specific ignores
|
# ─── Godot ─────────────────────────────────────────────────────────
|
||||||
.godot/
|
# Import cache. Regenerates on open. NEVER commit.
|
||||||
/android/
|
game/.godot/
|
||||||
|
|
||||||
|
# Orphaned temp files from interrupted imports
|
||||||
|
*.import-*
|
||||||
|
|
||||||
|
# Android build template
|
||||||
|
game/android/
|
||||||
|
|
||||||
|
# ─── Build output ──────────────────────────────────────────────────
|
||||||
|
/export/
|
||||||
|
/build/
|
||||||
|
*.pck
|
||||||
|
*.exe
|
||||||
|
*.x86_64
|
||||||
|
*.dmg
|
||||||
|
*.apk
|
||||||
|
*.aab
|
||||||
|
|
||||||
|
# ─── DCC backups (should live in Nextcloud, not here) ──────────────
|
||||||
|
*.blend[0-9]
|
||||||
|
*.blend[0-9][0-9]
|
||||||
|
*.kra~
|
||||||
|
*.krz
|
||||||
|
*.psb
|
||||||
|
*.tmp
|
||||||
|
|
||||||
|
# ─── Editors ───────────────────────────────────────────────────────
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# ─── OS ────────────────────────────────────────────────────────────
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
desktop.ini
|
||||||
|
|
||||||
|
# ─── Nextcloud sync artifacts ──────────────────────────────────────
|
||||||
|
.sync_*.db
|
||||||
|
*.~lock*
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 31 KiB |
@@ -1,45 +0,0 @@
|
|||||||
[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
|
|
||||||
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 20 KiB |
@@ -1,44 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="texture"
|
|
||||||
type="CompressedTexture2D"
|
|
||||||
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": "c03f264d9d0115e28f662ed5ab27bbb2"
|
|
||||||
}
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
|
|
||||||
source_file="res://Models/Dartboard/Dartboard-Proto_Green.png"
|
|
||||||
dest_files=["res://.godot/imported/Dartboard-Proto_Green.png-0f939f85106a12f4ae54352c096222a2.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
|
|
||||||
|
Before Width: | Height: | Size: 20 KiB |
@@ -1,45 +0,0 @@
|
|||||||
[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
|
|
||||||
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 210 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 437 KiB |
@@ -1,40 +0,0 @@
|
|||||||
[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
|
|
||||||
|
Before Width: | Height: | Size: 20 KiB |
@@ -1,40 +0,0 @@
|
|||||||
[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
|
|
||||||
|
Before Width: | Height: | Size: 20 KiB |
@@ -1,40 +0,0 @@
|
|||||||
[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
|
|
||||||
|
Before Width: | Height: | Size: 74 KiB |
@@ -1,40 +0,0 @@
|
|||||||
[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
|
|
||||||
|
Before Width: | Height: | Size: 83 KiB |
@@ -1,40 +0,0 @@
|
|||||||
[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
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
[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"
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
[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"
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
[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"
|
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
# 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.
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
# 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.
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
[gd_scene format=3 uid="uid://bswbcehkkqbk0"]
|
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://8kk531f88oqw" path="res://Scenes/DartPhysics.gd" id="1_s1kro"]
|
|
||||||
[ext_resource type="PackedScene" uid="uid://cjq45anerhxnk" path="res://models/Dart.glb" id="2_mlvfh"]
|
|
||||||
|
|
||||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_xxti7"]
|
|
||||||
radius = 0.27441406
|
|
||||||
height = 2.3862305
|
|
||||||
|
|
||||||
[node name="Dart" type="Node3D" unique_id=367982453]
|
|
||||||
|
|
||||||
[node name="RigidBody3D" type="RigidBody3D" parent="." unique_id=1993396351]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1.0000001, 0, 0, 0, 1.0000001, 0, 0, 0)
|
|
||||||
center_of_mass_mode = 1
|
|
||||||
script = ExtResource("1_s1kro")
|
|
||||||
angle_of_plane = -25.0
|
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="RigidBody3D" unique_id=1030334431]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.10033989, 0)
|
|
||||||
shape = SubResource("CapsuleShape3D_xxti7")
|
|
||||||
|
|
||||||
[node name="Dart2" parent="RigidBody3D" unique_id=1655536183 instance=ExtResource("2_mlvfh")]
|
|
||||||
transform = Transform3D(0.4, 0, 0, 0, 0.39999995, 0, 0, 0, 0.39999995, 0, -1.4526575, 0.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
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
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 +0,0 @@
|
|||||||
uid://8kk531f88oqw
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
[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")
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
extends Camera3D
|
|
||||||
|
|
||||||
var held_dart: Dart = null
|
|
||||||
|
|
||||||
func _unhandled_input(event):
|
|
||||||
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
|
|
||||||
if event.pressed:
|
|
||||||
try_grab_dart(event.position)
|
|
||||||
else:
|
|
||||||
if held_dart:
|
|
||||||
held_dart.release_drag()
|
|
||||||
held_dart = null
|
|
||||||
elif event is InputEventMouseMotion and held_dart:
|
|
||||||
held_dart.update_drag(self, event.position)
|
|
||||||
|
|
||||||
func try_grab_dart(mouse_pos: Vector2):
|
|
||||||
var ray_origin = project_ray_origin(mouse_pos)
|
|
||||||
var ray_dir = project_ray_normal(mouse_pos)
|
|
||||||
var ray_end = ray_origin + ray_dir * 1000.0
|
|
||||||
|
|
||||||
var space_state = get_world_3d().direct_space_state
|
|
||||||
var query = PhysicsRayQueryParameters3D.create(ray_origin, ray_end)
|
|
||||||
var result = space_state.intersect_ray(query)
|
|
||||||
|
|
||||||
if result and result.collider is Dart:
|
|
||||||
held_dart = result.collider
|
|
||||||
held_dart.start_drag(self, mouse_pos, result.position)
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
[gd_scene format=3 uid="uid://doh7p84eej78c"]
|
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://geqmah8kbko0" path="res://Scenes/main.gd" id="1_8gbba"]
|
|
||||||
[ext_resource type="Script" uid="uid://ds3uiri3oc05v" path="res://Scenes/camera_3d.gd" id="1_bo1nx"]
|
|
||||||
[ext_resource type="PackedScene" uid="uid://bswbcehkkqbk0" path="res://Scenes/Dart.tscn" id="1_jjgbg"]
|
|
||||||
[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)
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
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 +0,0 @@
|
|||||||
uid://d1i0lhibtflvq
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
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,69 +0,0 @@
|
|||||||
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()
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
extends Camera3D
|
|
||||||
|
|
||||||
const RAY_LENGTH = 1000
|
|
||||||
|
|
||||||
func ray_cast():
|
|
||||||
var space_state = get_world_3d().direct_space_state
|
|
||||||
var cam = $"."
|
|
||||||
var mousepos = get_viewport().get_mouse_position()
|
|
||||||
var origin = cam.project_ray_origin(mousepos)
|
|
||||||
var end = origin + cam.project_ray_normal(mousepos) * RAY_LENGTH
|
|
||||||
var query = PhysicsRayQueryParameters3D.create(origin,end)
|
|
||||||
query.collide_with_areas = true
|
|
||||||
var result = space_state.intersect_ray(query)
|
|
||||||
result = result.get("collider")
|
|
||||||
return result
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://ckbdoamrm313u
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
extends Node3D
|
|
||||||
|
|
||||||
var draggingCollider
|
|
||||||
var mousePosition
|
|
||||||
var doDrag = false
|
|
||||||
|
|
||||||
func _input(event):
|
|
||||||
var intersect
|
|
||||||
|
|
||||||
if event is InputEventMouse:
|
|
||||||
intersect = get_mouse_intersect(event.position)
|
|
||||||
if intersect: mousePosition = intersect.position
|
|
||||||
#snap on collider
|
|
||||||
#if intersect: mousePosition = intersect.collider.global_position
|
|
||||||
|
|
||||||
if event is InputEventMouseButton:
|
|
||||||
var leftButtonPressed = event.button_index == MOUSE_BUTTON_LEFT && event.pressed
|
|
||||||
var leftButtonReleased = event.button_index == MOUSE_BUTTON_LEFT && !event.pressed
|
|
||||||
|
|
||||||
if leftButtonReleased:
|
|
||||||
doDrag = false
|
|
||||||
drag_and_drop(intersect)
|
|
||||||
elif leftButtonPressed:
|
|
||||||
doDrag = true
|
|
||||||
drag_and_drop(intersect)
|
|
||||||
|
|
||||||
|
|
||||||
func _process(delta):
|
|
||||||
if draggingCollider:
|
|
||||||
draggingCollider.global_position = mousePosition
|
|
||||||
|
|
||||||
func drag_and_drop(intersect):
|
|
||||||
var canMove = intersect.collider in get_tree().get_nodes_in_group("Dart")
|
|
||||||
if !draggingCollider && doDrag && canMove:
|
|
||||||
draggingCollider = intersect.collider
|
|
||||||
elif draggingCollider:
|
|
||||||
draggingCollider = null
|
|
||||||
|
|
||||||
func get_mouse_intersect(mousePosition):
|
|
||||||
var currentCamera = get_viewport().get_camera_3d()
|
|
||||||
var params = PhysicsRayQueryParameters3D.new()
|
|
||||||
|
|
||||||
params.from = currentCamera.project_ray_origin(mousePosition)
|
|
||||||
params.to = currentCamera.project_position(mousePosition, 1000)
|
|
||||||
if draggingCollider: params.exclude = [draggingCollider]
|
|
||||||
|
|
||||||
var worldspace = get_world_3d().direct_space_state
|
|
||||||
var result = worldspace.intersect_ray(params)
|
|
||||||
|
|
||||||
return result
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
extends MeshInstance3D
|
|
||||||
|
|
||||||
@onready var camera = get_node("Camera3D")
|
|
||||||
@onready var area = get_node("../../../Dart")
|
|
||||||
|
|
||||||
func _input(event):
|
|
||||||
if Input.is_action_just_pressed("leftClick"):
|
|
||||||
print(camera.ray_cast().position)
|
|
||||||
|
|
||||||
if camera.ray_cast() != null and Input.is_action_pressed("midleclic"):
|
|
||||||
var mousepos = get_viewport().get_mouse_position()
|
|
||||||
var origin = camera.projectray_origin(mousepos)
|
|
||||||
var end = camera.project_ray_normal(mousepos)
|
|
||||||
var depth = origin.distence_to(area.global_position)
|
|
||||||
var final_position = origin + end * depth
|
|
||||||
area.global_position=final_position
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://cc5yy1yc3j2uu
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"id": "official",
|
||||||
|
"display_name": "DartGame",
|
||||||
|
"author": "Ced Games",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"api_version": 1,
|
||||||
|
"depends": [],
|
||||||
|
"description": "The base game content."
|
||||||
|
}
|
||||||
@@ -2,16 +2,16 @@
|
|||||||
|
|
||||||
importer="texture"
|
importer="texture"
|
||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://b1vf3204p4ys6"
|
uid="uid://dgq04uh0wal5a"
|
||||||
path="res://.godot/imported/Coffee.png-7ec89259677377dfb36274d8ce0e9258.ctex"
|
path="res://.godot/imported/mjolnir.png-5e80731d4fee936aa61acd006a5499ea.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"vram_texture": false
|
"vram_texture": false
|
||||||
}
|
}
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://Models/Dartboard/Textures/Coffee.png"
|
source_file="res://content/official/modifiers/mjolnir/mjolnir.png"
|
||||||
dest_files=["res://.godot/imported/Coffee.png-7ec89259677377dfb36274d8ce0e9258.ctex"]
|
dest_files=["res://.godot/imported/mjolnir.png-5e80731d4fee936aa61acd006a5499ea.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[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
|
||||||
@@ -2,16 +2,16 @@
|
|||||||
|
|
||||||
importer="texture"
|
importer="texture"
|
||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://blvifdtblwcxg"
|
uid="uid://bayaus0tgltb3"
|
||||||
path="res://.godot/imported/Cork.png-8b577bb6a5aa8a82bae563118e794e18.ctex"
|
path="res://.godot/imported/neon_hiero.png-401e600b4e3a81c5e49b654215a0e850.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"vram_texture": false
|
"vram_texture": false
|
||||||
}
|
}
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://Models/Dartboard/Textures/Cork.png"
|
source_file="res://content/official/skins/bulls/neon_hiero.png"
|
||||||
dest_files=["res://.godot/imported/Cork.png-8b577bb6a5aa8a82bae563118e794e18.ctex"]
|
dest_files=["res://.godot/imported/neon_hiero.png-401e600b4e3a81c5e49b654215a0e850.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
@@ -2,16 +2,16 @@
|
|||||||
|
|
||||||
importer="texture"
|
importer="texture"
|
||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://ch4b1pnokuu8d"
|
uid="uid://b672fbyuxxktw"
|
||||||
path="res://.godot/imported/Wings-Red-Blue.png-701faf0c1b6c3af11eb52058dfda7798.ctex"
|
path="res://.godot/imported/ocean_fire.png-b1f0d5fea08640de091cb5cf724309e6.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"vram_texture": false
|
"vram_texture": false
|
||||||
}
|
}
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://Models/Dart/Textures/Wings-Red-Blue.png"
|
source_file="res://content/official/skins/flat_tire/ocean_fire.png"
|
||||||
dest_files=["res://.godot/imported/Wings-Red-Blue.png-701faf0c1b6c3af11eb52058dfda7798.ctex"]
|
dest_files=["res://.godot/imported/ocean_fire.png-b1f0d5fea08640de091cb5cf724309e6.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
@@ -2,16 +2,16 @@
|
|||||||
|
|
||||||
importer="texture"
|
importer="texture"
|
||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://c7jxj25ophbsb"
|
uid="uid://btg3hosfx1sdg"
|
||||||
path="res://.godot/imported/Dart-Default.png-2feb9f56821a29d976ec19a662a6977f.ctex"
|
path="res://.godot/imported/toon_purple.png-f1a192acdf1c762039bd65e86672f7e4.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"vram_texture": false
|
"vram_texture": false
|
||||||
}
|
}
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://Models/Dart/Textures/Dart-Default.png"
|
source_file="res://content/official/skins/flat_tire/toon_purple.png"
|
||||||
dest_files=["res://.godot/imported/Dart-Default.png-2feb9f56821a29d976ec19a662a6977f.ctex"]
|
dest_files=["res://.godot/imported/toon_purple.png-f1a192acdf1c762039bd65e86672f7e4.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
@@ -2,16 +2,16 @@
|
|||||||
|
|
||||||
importer="texture"
|
importer="texture"
|
||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://bhbtrcw8xbp1g"
|
uid="uid://brqn2ve8pm3i8"
|
||||||
path="res://.godot/imported/Wings-Default.png-b4a7a92ebd237d86daaae21adc10d873.ctex"
|
path="res://.godot/imported/neon_hiero_black.png-7709dfdedde53a261eb17c86736a737f.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"vram_texture": false
|
"vram_texture": false
|
||||||
}
|
}
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://Models/Dart/Textures/Wings-Default.png"
|
source_file="res://content/official/skins/sectors/neon_hiero_black.png"
|
||||||
dest_files=["res://.godot/imported/Wings-Default.png-b4a7a92ebd237d86daaae21adc10d873.ctex"]
|
dest_files=["res://.godot/imported/neon_hiero_black.png-7709dfdedde53a261eb17c86736a737f.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://bataamlsyyup6"
|
||||||
|
path="res://.godot/imported/neon_hiero_white.png-c93ce5ab952a3d2618c297fc1cae11f4.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://content/official/skins/sectors/neon_hiero_white.png"
|
||||||
|
dest_files=["res://.godot/imported/neon_hiero_white.png-c93ce5ab952a3d2618c297fc1cae11f4.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
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("===============================")
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cme5xqn2hlrlu
|
||||||
@@ -3,13 +3,13 @@
|
|||||||
importer="scene"
|
importer="scene"
|
||||||
importer_version=1
|
importer_version=1
|
||||||
type="PackedScene"
|
type="PackedScene"
|
||||||
uid="uid://cjq45anerhxnk"
|
uid="uid://djr5vbnb652ax"
|
||||||
path="res://.godot/imported/Dart.glb-c455f16296a9e65bbb66a88577786d63.scn"
|
path="res://.godot/imported/dartboard.glb-8db0957180c245aae0575b1ad098f883.scn"
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://Models/Dart.glb"
|
source_file="res://core/board/dartboard.glb"
|
||||||
dest_files=["res://.godot/imported/Dart.glb-c455f16296a9e65bbb66a88577786d63.scn"]
|
dest_files=["res://.godot/imported/dartboard.glb-8db0957180c245aae0575b1ad098f883.scn"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
[gd_scene format=3 uid="uid://dojbd5wl7ntp0"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://cme5xqn2hlrlu" path="res://core/board/dartboard.gd" id="1"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://djr5vbnb652ax" path="res://core/board/dartboard.glb" id="2"]
|
||||||
|
[ext_resource type="Shape3D" uid="uid://vr03k1qlo10y" path="res://core/board/dartboard_collision.res" id="3_6aacy"]
|
||||||
|
[ext_resource type="Shape3D" uid="uid://c0vqtoe8q7yly" path="res://core/board/spider_collision.res" id="4_khw4q"]
|
||||||
|
|
||||||
|
[sub_resource type="PhysicsMaterial" id="PhysicsMaterial_uomu6"]
|
||||||
|
friction = 0.15
|
||||||
|
bounce = 0.6
|
||||||
|
|
||||||
|
[node name="Dartboard" type="StaticBody3D" unique_id=838863701]
|
||||||
|
script = ExtResource("1")
|
||||||
|
|
||||||
|
[node name="Model" parent="." unique_id=1556270899 instance=ExtResource("2")]
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=1180985747]
|
||||||
|
shape = ExtResource("3_6aacy")
|
||||||
|
|
||||||
|
[node name="SpiderBody" type="StaticBody3D" parent="." unique_id=216948518]
|
||||||
|
collision_layer = 8
|
||||||
|
physics_material_override = SubResource("PhysicsMaterial_uomu6")
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="SpiderBody" unique_id=1618627145]
|
||||||
|
shape = ExtResource("4_khw4q")
|
||||||
|
|
||||||
|
[editable path="Model"]
|
||||||
@@ -2,20 +2,20 @@
|
|||||||
|
|
||||||
importer="texture"
|
importer="texture"
|
||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://bobnv6cyi7dc6"
|
uid="uid://dj6wb70lh5s4f"
|
||||||
path.s3tc="res://.godot/imported/Dart_Dart-Default.png-4bd6068152c7966988ca2c23bd03ba76.s3tc.ctex"
|
path.s3tc="res://.godot/imported/dartboard_bulls.png-354417d2f083e713950eed8be5c54f73.s3tc.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"imported_formats": ["s3tc_bptc"],
|
"imported_formats": ["s3tc_bptc"],
|
||||||
"vram_texture": true
|
"vram_texture": true
|
||||||
}
|
}
|
||||||
generator_parameters={
|
generator_parameters={
|
||||||
"md5": "9c4718c8d2830abe8cc9971ef621b46a"
|
"md5": "ce1804f3e86fea26503097d5e278c919"
|
||||||
}
|
}
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://Models/Dart/Dart_Dart-Default.png"
|
source_file="res://core/board/dartboard_bulls.png"
|
||||||
dest_files=["res://.godot/imported/Dart_Dart-Default.png-4bd6068152c7966988ca2c23bd03ba76.s3tc.ctex"]
|
dest_files=["res://.godot/imported/dartboard_bulls.png-354417d2f083e713950eed8be5c54f73.s3tc.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
@@ -2,20 +2,20 @@
|
|||||||
|
|
||||||
importer="texture"
|
importer="texture"
|
||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://bqxi7x76uv4tq"
|
uid="uid://da2elnsphlc5i"
|
||||||
path.s3tc="res://.godot/imported/Dart_Wings-Default.png-358040735cbc551a0d2abb04506c708a.s3tc.ctex"
|
path.s3tc="res://.godot/imported/dartboard_flat_tire.png-97f4bd426c06f71b009cd942664be6fa.s3tc.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"imported_formats": ["s3tc_bptc"],
|
"imported_formats": ["s3tc_bptc"],
|
||||||
"vram_texture": true
|
"vram_texture": true
|
||||||
}
|
}
|
||||||
generator_parameters={
|
generator_parameters={
|
||||||
"md5": "52ee9a89e7c6ed5f537103a41115551e"
|
"md5": "d24ebe44ff702bdbd5528c9d29427568"
|
||||||
}
|
}
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://Models/Dart/Dart_Wings-Default.png"
|
source_file="res://core/board/dartboard_flat_tire.png"
|
||||||
dest_files=["res://.godot/imported/Dart_Wings-Default.png-358040735cbc551a0d2abb04506c708a.s3tc.ctex"]
|
dest_files=["res://.godot/imported/dartboard_flat_tire.png-97f4bd426c06f71b009cd942664be6fa.s3tc.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
@@ -2,20 +2,20 @@
|
|||||||
|
|
||||||
importer="texture"
|
importer="texture"
|
||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://dk6gnbvgumq7c"
|
uid="uid://cjw6x7ig1ogbo"
|
||||||
path.s3tc="res://.godot/imported/Dartboard-Proto_Cork.png-9700f4c0b675dee65dc4e1c14aa931b3.s3tc.ctex"
|
path.s3tc="res://.godot/imported/dartboard_sector_black.png-39f98bf69c481faad6b8976a0994ecb5.s3tc.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"imported_formats": ["s3tc_bptc"],
|
"imported_formats": ["s3tc_bptc"],
|
||||||
"vram_texture": true
|
"vram_texture": true
|
||||||
}
|
}
|
||||||
generator_parameters={
|
generator_parameters={
|
||||||
"md5": "5b186b11689f911ad6691488292c3475"
|
"md5": "831beecb2a48df4a1f6850e3e8c938dd"
|
||||||
}
|
}
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://Models/Dartboard/Dartboard-Proto_Cork.png"
|
source_file="res://core/board/dartboard_sector_black.png"
|
||||||
dest_files=["res://.godot/imported/Dartboard-Proto_Cork.png-9700f4c0b675dee65dc4e1c14aa931b3.s3tc.ctex"]
|
dest_files=["res://.godot/imported/dartboard_sector_black.png-39f98bf69c481faad6b8976a0994ecb5.s3tc.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
@@ -2,20 +2,20 @@
|
|||||||
|
|
||||||
importer="texture"
|
importer="texture"
|
||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://cvv4akpwsudu3"
|
uid="uid://cry28pcmt01gk"
|
||||||
path.s3tc="res://.godot/imported/Dartboard-Proto_Red.png-e757930c4a42a8dc67547b3409641a91.s3tc.ctex"
|
path.s3tc="res://.godot/imported/dartboard_sector_white.png-6dbf28d122f3327d568bcbb29e7f70a0.s3tc.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"imported_formats": ["s3tc_bptc"],
|
"imported_formats": ["s3tc_bptc"],
|
||||||
"vram_texture": true
|
"vram_texture": true
|
||||||
}
|
}
|
||||||
generator_parameters={
|
generator_parameters={
|
||||||
"md5": "e46ac968aab4da0b0c65bc59957f2f96"
|
"md5": "aa25e4080cfa45ac8064cbf217904fb3"
|
||||||
}
|
}
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://Models/Dartboard/Dartboard-Proto_Red.png"
|
source_file="res://core/board/dartboard_sector_white.png"
|
||||||
dest_files=["res://.godot/imported/Dartboard-Proto_Red.png-e757930c4a42a8dc67547b3409641a91.s3tc.ctex"]
|
dest_files=["res://.godot/imported/dartboard_sector_white.png-6dbf28d122f3327d568bcbb29e7f70a0.s3tc.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
importer="texture"
|
importer="texture"
|
||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://ctr7mydgfca75"
|
uid="uid://b87cl0ullks7x"
|
||||||
path.s3tc="res://.godot/imported/Dartboard_Spider.png-3a7e01e42d10301e2794b3c6268069b4.s3tc.ctex"
|
path.s3tc="res://.godot/imported/dartboard_spider.png-c5fe87c929f66762aaaaefebd3359c9d.s3tc.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"imported_formats": ["s3tc_bptc"],
|
"imported_formats": ["s3tc_bptc"],
|
||||||
"vram_texture": true
|
"vram_texture": true
|
||||||
@@ -14,8 +14,8 @@ generator_parameters={
|
|||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://Models/Dartboard/Dartboard_Spider.png"
|
source_file="res://core/board/dartboard_spider.png"
|
||||||
dest_files=["res://.godot/imported/Dartboard_Spider.png-3a7e01e42d10301e2794b3c6268069b4.s3tc.ctex"]
|
dest_files=["res://.godot/imported/dartboard_spider.png-c5fe87c929f66762aaaaefebd3359c9d.s3tc.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
class_name Dart
|
||||||
|
extends RigidBody3D
|
||||||
|
|
||||||
|
## A single dart. Dragged from the tray, thrown at the board.
|
||||||
|
##
|
||||||
|
## Lifecycle: FROZEN -> DRAGGING -> IN_FLIGHT -> LANDED
|
||||||
|
## The scene root (main.gd) routes mouse input here; the dart does not
|
||||||
|
## hunt for the camera or raycast for itself.
|
||||||
|
|
||||||
|
signal landed(dart: Dart, hit_point: Vector3)
|
||||||
|
|
||||||
|
enum State { IDLE, DRAGGING, IN_FLIGHT, LANDED }
|
||||||
|
|
||||||
|
# --- Tuning -------------------------------------------------------------
|
||||||
|
|
||||||
|
@export_group("Drag")
|
||||||
|
## How hard the dart chases the cursor while held.
|
||||||
|
@export var drag_follow_speed: float = 20.0
|
||||||
|
## Clamp so a fast flick doesn't launch it across the room mid-drag.
|
||||||
|
@export var max_drag_speed: float = 42.0
|
||||||
|
## Tilt of the drag plane relative to the camera, in degrees.
|
||||||
|
@export_range(-180.0, 180.0) var drag_plane_angle: float = 15.0
|
||||||
|
@export var drag_linear_damp: float = 5.0
|
||||||
|
@export var drag_angular_damp: float = 10.0
|
||||||
|
|
||||||
|
@export_group("Throw")
|
||||||
|
@export var forward_impulse: float = 8.0
|
||||||
|
|
||||||
|
## Scale the raw flick velocity down. Raw mouse speed is usually too fast.
|
||||||
|
@export var throw_force_multiplier: float = 0.5
|
||||||
|
@export var max_throw_speed: float = 42.0
|
||||||
|
## Higher = more visible arc. Real gravity looks too floaty at dart scale.
|
||||||
|
@export var flight_gravity_scale: float = 2.0
|
||||||
|
|
||||||
|
@export_group("Flight")
|
||||||
|
## How fast the nose swings to face the direction of travel.
|
||||||
|
@export var nose_align_speed: float = 1.0
|
||||||
|
## Which local axis is the dart's tip. Check this against the model.
|
||||||
|
@export var forward_axis: Vector3 = Vector3.FORWARD
|
||||||
|
## Below this speed the dart is considered landed.
|
||||||
|
@export var landed_speed_threshold: float = 0.5
|
||||||
|
|
||||||
|
# --- State --------------------------------------------------------------
|
||||||
|
|
||||||
|
var state: State = State.IDLE
|
||||||
|
|
||||||
|
var _drag_plane: Plane
|
||||||
|
var _drag_offset: Vector3 = Vector3.ZERO
|
||||||
|
var _target_pos: Vector3 = Vector3.ZERO
|
||||||
|
|
||||||
|
## Sampled positions, used to compute throw velocity on release.
|
||||||
|
const HISTORY_LENGTH := 6
|
||||||
|
var _pos_history: Array[Vector3] = []
|
||||||
|
var _time_history: Array[float] = []
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
freeze_mode = RigidBody3D.FREEZE_MODE_STATIC
|
||||||
|
freeze = true
|
||||||
|
add_to_group(&"dart")
|
||||||
|
|
||||||
|
|
||||||
|
# --- Public API (called by main.gd) --------------------------------------
|
||||||
|
|
||||||
|
func begin_drag(camera: Camera3D, hit_point: Vector3) -> void:
|
||||||
|
state = State.DRAGGING
|
||||||
|
freeze = false
|
||||||
|
|
||||||
|
gravity_scale = 0.0 # don't sag while held
|
||||||
|
linear_damp = drag_linear_damp
|
||||||
|
angular_damp = drag_angular_damp
|
||||||
|
|
||||||
|
# The plane the dart slides along while dragged. Tilted off the camera's
|
||||||
|
# view axis so pulling back also pulls the dart down, which feels natural.
|
||||||
|
var base_normal := camera.global_transform.basis.z
|
||||||
|
var tilted := base_normal.rotated(
|
||||||
|
camera.global_transform.basis.x,
|
||||||
|
deg_to_rad(drag_plane_angle)
|
||||||
|
)
|
||||||
|
_drag_plane = Plane(tilted.normalized(), hit_point)
|
||||||
|
_drag_offset = global_position - hit_point
|
||||||
|
|
||||||
|
# Seed the target to the current position. Without this the dart drops
|
||||||
|
# to the plane origin until the mouse first moves.
|
||||||
|
_target_pos = global_position
|
||||||
|
|
||||||
|
_pos_history.clear()
|
||||||
|
_time_history.clear()
|
||||||
|
|
||||||
|
|
||||||
|
func update_drag(camera: Camera3D, mouse_pos: Vector2) -> void:
|
||||||
|
if state != State.DRAGGING:
|
||||||
|
return
|
||||||
|
|
||||||
|
var ray_origin := camera.project_ray_origin(mouse_pos)
|
||||||
|
var ray_dir := camera.project_ray_normal(mouse_pos)
|
||||||
|
var hit = _drag_plane.intersects_ray(ray_origin, ray_dir)
|
||||||
|
if hit != null:
|
||||||
|
_target_pos = hit + _drag_offset
|
||||||
|
|
||||||
|
func release_drag() -> void:
|
||||||
|
if state != State.DRAGGING:
|
||||||
|
return
|
||||||
|
state = State.IN_FLIGHT
|
||||||
|
gravity_scale = flight_gravity_scale
|
||||||
|
linear_damp = 0.0
|
||||||
|
angular_damp = 0.0
|
||||||
|
|
||||||
|
# The flick gives aim (up/down/left/right). It CANNOT give forward speed —
|
||||||
|
# the drag plane faces the camera, so motion toward the board is ~zero.
|
||||||
|
var flick := _calculate_throw_velocity() * throw_force_multiplier
|
||||||
|
|
||||||
|
# The forward component has to come from somewhere. Use the camera's
|
||||||
|
# forward, so "throw" always means "toward what you're looking at".
|
||||||
|
var cam := get_viewport().get_camera_3d()
|
||||||
|
var forward := -cam.global_transform.basis.z * forward_impulse
|
||||||
|
|
||||||
|
var throw := flick + forward
|
||||||
|
if throw.length() > max_throw_speed:
|
||||||
|
throw = throw.normalized() * max_throw_speed
|
||||||
|
linear_velocity = throw
|
||||||
|
|
||||||
|
var raw := _calculate_throw_velocity()
|
||||||
|
print("flick: %s forward: %s final: %s (%.1f m/s)" % [flick, forward, throw, throw.length()])
|
||||||
|
|
||||||
|
|
||||||
|
# --- Physics ------------------------------------------------------------
|
||||||
|
|
||||||
|
func _physics_process(delta: float) -> void:
|
||||||
|
match state:
|
||||||
|
State.DRAGGING:
|
||||||
|
_process_drag()
|
||||||
|
State.IN_FLIGHT:
|
||||||
|
_process_flight(delta)
|
||||||
|
|
||||||
|
|
||||||
|
func _process_drag() -> void:
|
||||||
|
# Chase the target with velocity rather than teleporting, so the dart
|
||||||
|
# still collides with things and carries momentum into the throw.
|
||||||
|
var to_target := _target_pos - global_position
|
||||||
|
var desired := to_target * drag_follow_speed
|
||||||
|
if desired.length() > max_drag_speed:
|
||||||
|
desired = desired.normalized() * max_drag_speed
|
||||||
|
|
||||||
|
linear_velocity = desired
|
||||||
|
angular_velocity = angular_velocity.lerp(Vector3.ZERO, 0.2)
|
||||||
|
|
||||||
|
_pos_history.append(global_position)
|
||||||
|
_time_history.append(Time.get_ticks_msec() / 1000.0)
|
||||||
|
if _pos_history.size() > HISTORY_LENGTH:
|
||||||
|
_pos_history.pop_front()
|
||||||
|
_time_history.pop_front()
|
||||||
|
|
||||||
|
|
||||||
|
func _process_flight(delta: float) -> void:
|
||||||
|
if linear_velocity.length() > landed_speed_threshold:
|
||||||
|
_align_to_velocity(delta)
|
||||||
|
else:
|
||||||
|
_on_landed()
|
||||||
|
|
||||||
|
|
||||||
|
func _align_to_velocity(delta: float) -> void:
|
||||||
|
var vel_dir := linear_velocity.normalized()
|
||||||
|
var current_forward := global_transform.basis * forward_axis
|
||||||
|
var axis := current_forward.cross(vel_dir)
|
||||||
|
|
||||||
|
if axis.length() < 0.001:
|
||||||
|
return # already aligned, or exactly opposed
|
||||||
|
|
||||||
|
axis = axis.normalized()
|
||||||
|
var angle := current_forward.angle_to(vel_dir)
|
||||||
|
var step := minf(angle, nose_align_speed * delta)
|
||||||
|
|
||||||
|
global_transform.basis = global_transform.basis.rotated(axis, step)
|
||||||
|
global_transform.basis = global_transform.basis.orthonormalized()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_landed() -> void:
|
||||||
|
state = State.LANDED
|
||||||
|
angular_velocity = Vector3.ZERO
|
||||||
|
landed.emit(self, global_position)
|
||||||
|
|
||||||
|
|
||||||
|
func _calculate_throw_velocity() -> Vector3:
|
||||||
|
if _pos_history.size() < 2:
|
||||||
|
return linear_velocity
|
||||||
|
|
||||||
|
var dt: float = _time_history[-1] - _time_history[0]
|
||||||
|
if dt <= 0.0:
|
||||||
|
return linear_velocity
|
||||||
|
|
||||||
|
return (_pos_history[-1] - _pos_history[0]) / dt
|
||||||
@@ -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-82b423f0f21d37c1bf28626083e1030e.scn"
|
path="res://.godot/imported/dart.glb-ca8e61ccf0bb4431c3c990bf6c68da95.scn"
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://Models/Dart/Dart.glb"
|
source_file="res://core/dart/dart.glb"
|
||||||
dest_files=["res://.godot/imported/Dart.glb-82b423f0f21d37c1bf28626083e1030e.scn"]
|
dest_files=["res://.godot/imported/dart.glb-ca8e61ccf0bb4431c3c990bf6c68da95.scn"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
[gd_scene format=3 uid="uid://fshohiewm7et"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://sueo33rxr07e" path="res://core/dart/dart.gd" id="1"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://fp0gcwcyfd7g" path="res://core/dart/dart.glb" id="2"]
|
||||||
|
|
||||||
|
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"]
|
||||||
|
radius = 0.008
|
||||||
|
height = 0.15
|
||||||
|
|
||||||
|
[node name="Dart" type="RigidBody3D" unique_id=1124867376]
|
||||||
|
collision_layer = 2
|
||||||
|
script = ExtResource("1")
|
||||||
|
|
||||||
|
[node name="MeshInstance3D" parent="." unique_id=936158903 instance=ExtResource("2")]
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=2015440784]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.07, 0)
|
||||||
|
shape = SubResource("CapsuleShape3D_1")
|
||||||
|
|
||||||
|
[node name="Tip" type="Marker3D" parent="." unique_id=2014607155]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.14, 0)
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
importer="texture"
|
importer="texture"
|
||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://62njxkpga6e8"
|
uid="uid://b3n3th3102f2g"
|
||||||
path.s3tc="res://.godot/imported/Dart_Dart-Default.png-6f8aeb995de95881fb9c9013d8c53895.s3tc.ctex"
|
path.s3tc="res://.godot/imported/dart_body.png-b1ba77c6cead7a089d94545960115fac.s3tc.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"imported_formats": ["s3tc_bptc"],
|
"imported_formats": ["s3tc_bptc"],
|
||||||
"vram_texture": true
|
"vram_texture": true
|
||||||
@@ -14,8 +14,8 @@ generator_parameters={
|
|||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://Models/Dart_Dart-Default.png"
|
source_file="res://core/dart/dart_body.png"
|
||||||
dest_files=["res://.godot/imported/Dart_Dart-Default.png-6f8aeb995de95881fb9c9013d8c53895.s3tc.ctex"]
|
dest_files=["res://.godot/imported/dart_body.png-b1ba77c6cead7a089d94545960115fac.s3tc.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
importer="texture"
|
importer="texture"
|
||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://b7bsi4ury2lve"
|
uid="uid://c85g6b4hhn83g"
|
||||||
path.s3tc="res://.godot/imported/Dart_Wings-Default.png-5fe09a8386222e14f1aee4e3675a93cb.s3tc.ctex"
|
path.s3tc="res://.godot/imported/dart_wings.png-45f87827353eb4dc5f134b80f5e80ea7.s3tc.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"imported_formats": ["s3tc_bptc"],
|
"imported_formats": ["s3tc_bptc"],
|
||||||
"vram_texture": true
|
"vram_texture": true
|
||||||
@@ -14,8 +14,8 @@ generator_parameters={
|
|||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://Models/Dart_Wings-Default.png"
|
source_file="res://core/dart/dart_wings.png"
|
||||||
dest_files=["res://.godot/imported/Dart_Wings-Default.png-5fe09a8386222e14f1aee4e3675a93cb.s3tc.ctex"]
|
dest_files=["res://.godot/imported/dart_wings.png-45f87827353eb4dc5f134b80f5e80ea7.s3tc.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
extends Node
|
||||||
|
|
||||||
|
## Loads modifier definitions and serves them by id.
|
||||||
|
##
|
||||||
|
## ⚠️ PROTOTYPE. This is scaffolding, not the real mod loader.
|
||||||
|
##
|
||||||
|
## What's wrong with it, to be fixed in Phase 3:
|
||||||
|
## - DirAccess cannot walk into a PCK. This works in the editor and silently
|
||||||
|
## returns NOTHING in an exported build. Needs a build-time manifest.
|
||||||
|
## - No namespacing. Ids are bare strings, so two mods can collide.
|
||||||
|
## - No mod.json, no api_version, no dependency resolution.
|
||||||
|
## - Only reads content/official/. Doesn't touch user://mods/.
|
||||||
|
##
|
||||||
|
## Autoload name is ModRegistry (PascalCase), so call sites read as
|
||||||
|
## ModRegistry.get_by_id(...) rather than looking like a local variable.
|
||||||
|
|
||||||
|
const MODIFIER_DIR := "res://content/official/modifiers/"
|
||||||
|
|
||||||
|
var modifiers: Array[Modifier] = []
|
||||||
|
var _by_id: Dictionary = {}
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_load_from_folder(MODIFIER_DIR)
|
||||||
|
_reindex()
|
||||||
|
print("ModRegistry: loaded %d modifiers." % modifiers.size())
|
||||||
|
|
||||||
|
|
||||||
|
## Recursive, because each modifier now lives in its own folder:
|
||||||
|
## modifiers/mjolnir/mjolnir.tres
|
||||||
|
func _load_from_folder(path: String) -> void:
|
||||||
|
var dir := DirAccess.open(path)
|
||||||
|
if dir == null:
|
||||||
|
push_warning("ModRegistry: can't open %s" % path)
|
||||||
|
return
|
||||||
|
|
||||||
|
dir.list_dir_begin()
|
||||||
|
var fname := dir.get_next()
|
||||||
|
while fname != "":
|
||||||
|
var full := path.path_join(fname)
|
||||||
|
if dir.current_is_dir():
|
||||||
|
if not fname.begins_with("."):
|
||||||
|
_load_from_folder(full)
|
||||||
|
elif fname.ends_with(".tres"):
|
||||||
|
var res := load(full)
|
||||||
|
if res is Modifier:
|
||||||
|
modifiers.append(res)
|
||||||
|
else:
|
||||||
|
push_warning("ModRegistry: %s is not a Modifier" % full)
|
||||||
|
fname = dir.get_next()
|
||||||
|
dir.list_dir_end()
|
||||||
|
|
||||||
|
|
||||||
|
func _reindex() -> void:
|
||||||
|
_by_id.clear()
|
||||||
|
for m in modifiers:
|
||||||
|
if m.id.is_empty():
|
||||||
|
push_warning("ModRegistry: modifier with empty id, skipping.")
|
||||||
|
continue
|
||||||
|
if _by_id.has(m.id):
|
||||||
|
push_warning("ModRegistry: duplicate id '%s'." % m.id)
|
||||||
|
_by_id[m.id] = m
|
||||||
|
|
||||||
|
|
||||||
|
func get_by_id(id: String) -> Modifier:
|
||||||
|
return _by_id.get(id)
|
||||||
|
|
||||||
|
|
||||||
|
## Pick `count` distinct modifiers, weighted by rarity.
|
||||||
|
func pick_random(count: int) -> Array[Modifier]:
|
||||||
|
var pool := modifiers.duplicate()
|
||||||
|
var chosen: Array[Modifier] = []
|
||||||
|
count = mini(count, pool.size())
|
||||||
|
|
||||||
|
for _i in count:
|
||||||
|
var total := 0.0
|
||||||
|
for m in pool:
|
||||||
|
total += maxf(m.weight, 0.0001)
|
||||||
|
|
||||||
|
var r := randf() * total
|
||||||
|
var acc := 0.0
|
||||||
|
var picked := 0
|
||||||
|
for j in pool.size():
|
||||||
|
acc += maxf(pool[j].weight, 0.0001)
|
||||||
|
if r <= acc:
|
||||||
|
picked = j
|
||||||
|
break
|
||||||
|
|
||||||
|
chosen.append(pool[picked])
|
||||||
|
pool.remove_at(picked) # distinct — don't offer the same one twice
|
||||||
|
|
||||||
|
return chosen
|
||||||