diff --git a/art/sprites/FG_Tree_Fall.png b/art/sprites/FG_Tree_Fall.png new file mode 100644 index 0000000..6f9c887 Binary files /dev/null and b/art/sprites/FG_Tree_Fall.png differ diff --git a/art/sprites/FG_Tree_Fall.png.import b/art/sprites/FG_Tree_Fall.png.import new file mode 100644 index 0000000..c735e56 --- /dev/null +++ b/art/sprites/FG_Tree_Fall.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://qen8u4g1oee4" +path="res://.godot/imported/FG_Tree_Fall.png-63225671846c6354f20ce0b5e0a5e31e.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/FG_Tree_Fall.png" +dest_files=["res://.godot/imported/FG_Tree_Fall.png-63225671846c6354f20ce0b5e0a5e31e.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 diff --git a/art/sprites/FG_Tree_Stages.png b/art/sprites/FG_Tree_Stages.png new file mode 100644 index 0000000..9b4881d Binary files /dev/null and b/art/sprites/FG_Tree_Stages.png differ diff --git a/art/sprites/FG_Tree_Stages.png.import b/art/sprites/FG_Tree_Stages.png.import new file mode 100644 index 0000000..a22b563 --- /dev/null +++ b/art/sprites/FG_Tree_Stages.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bws0uhalpfbln" +path="res://.godot/imported/FG_Tree_Stages.png-620ce94d31b96e88794dc081cb63ea2e.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/FG_Tree_Stages.png" +dest_files=["res://.godot/imported/FG_Tree_Stages.png-620ce94d31b96e88794dc081cb63ea2e.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 diff --git a/art/sprites/FG_Tree_Summer.png b/art/sprites/FG_Tree_Summer.png new file mode 100644 index 0000000..7408581 Binary files /dev/null and b/art/sprites/FG_Tree_Summer.png differ diff --git a/art/sprites/FG_Tree_Summer.png.import b/art/sprites/FG_Tree_Summer.png.import new file mode 100644 index 0000000..3691b91 --- /dev/null +++ b/art/sprites/FG_Tree_Summer.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cepguvswk8ecj" +path="res://.godot/imported/FG_Tree_Summer.png-1bc1b92f64d4677ada2e125e991dc553.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/FG_Tree_Summer.png" +dest_files=["res://.godot/imported/FG_Tree_Summer.png-1bc1b92f64d4677ada2e125e991dc553.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 diff --git a/art/sprites/characters/Character_001_Dead.png b/art/sprites/characters/Character_001_Dead.png new file mode 100644 index 0000000..2340c33 Binary files /dev/null and b/art/sprites/characters/Character_001_Dead.png differ diff --git a/art/sprites/characters/Character_001_Dead.png.import b/art/sprites/characters/Character_001_Dead.png.import new file mode 100644 index 0000000..4fdb88a --- /dev/null +++ b/art/sprites/characters/Character_001_Dead.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dl5678nw0ak4q" +path="res://.godot/imported/Character_001_Dead.png-53f8a2c7c9d21301ad6ecc32b2e18e90.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_001_Dead.png" +dest_files=["res://.godot/imported/Character_001_Dead.png-53f8a2c7c9d21301ad6ecc32b2e18e90.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 diff --git a/art/sprites/characters/Character_001_Idle.png b/art/sprites/characters/Character_001_Idle.png new file mode 100644 index 0000000..9d86ee8 Binary files /dev/null and b/art/sprites/characters/Character_001_Idle.png differ diff --git a/art/sprites/characters/Character_001_Idle.png.import b/art/sprites/characters/Character_001_Idle.png.import new file mode 100644 index 0000000..1ee7f2a --- /dev/null +++ b/art/sprites/characters/Character_001_Idle.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://vdtgxefdrv6h" +path="res://.godot/imported/Character_001_Idle.png-949c25bb7badfcc54ea7b50733e40589.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_001_Idle.png" +dest_files=["res://.godot/imported/Character_001_Idle.png-949c25bb7badfcc54ea7b50733e40589.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 diff --git a/art/sprites/characters/Character_001_Walk.png b/art/sprites/characters/Character_001_Walk.png new file mode 100644 index 0000000..3177249 Binary files /dev/null and b/art/sprites/characters/Character_001_Walk.png differ diff --git a/art/sprites/characters/Character_001_Walk.png.import b/art/sprites/characters/Character_001_Walk.png.import new file mode 100644 index 0000000..0f2c40a --- /dev/null +++ b/art/sprites/characters/Character_001_Walk.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bx7dgdvihp3kj" +path="res://.godot/imported/Character_001_Walk.png-84dbf748230688d019f76cc700c71c17.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_001_Walk.png" +dest_files=["res://.godot/imported/Character_001_Walk.png-84dbf748230688d019f76cc700c71c17.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 diff --git a/art/sprites/characters/Character_002_Dead.png b/art/sprites/characters/Character_002_Dead.png new file mode 100644 index 0000000..f1be9a0 Binary files /dev/null and b/art/sprites/characters/Character_002_Dead.png differ diff --git a/art/sprites/characters/Character_002_Dead.png.import b/art/sprites/characters/Character_002_Dead.png.import new file mode 100644 index 0000000..70cfd3c --- /dev/null +++ b/art/sprites/characters/Character_002_Dead.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bvyms2xddxkky" +path="res://.godot/imported/Character_002_Dead.png-6e0015f8f83360b89fdd2ea19f2e0a7c.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_002_Dead.png" +dest_files=["res://.godot/imported/Character_002_Dead.png-6e0015f8f83360b89fdd2ea19f2e0a7c.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 diff --git a/art/sprites/characters/Character_002_Idle.png b/art/sprites/characters/Character_002_Idle.png new file mode 100644 index 0000000..c767dc8 Binary files /dev/null and b/art/sprites/characters/Character_002_Idle.png differ diff --git a/art/sprites/characters/Character_002_Idle.png.import b/art/sprites/characters/Character_002_Idle.png.import new file mode 100644 index 0000000..92e87f2 --- /dev/null +++ b/art/sprites/characters/Character_002_Idle.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://docwang41u716" +path="res://.godot/imported/Character_002_Idle.png-6255c26c24043e08900d4ae605cfeb76.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_002_Idle.png" +dest_files=["res://.godot/imported/Character_002_Idle.png-6255c26c24043e08900d4ae605cfeb76.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 diff --git a/art/sprites/characters/Character_002_Walk.png b/art/sprites/characters/Character_002_Walk.png new file mode 100644 index 0000000..9d75294 Binary files /dev/null and b/art/sprites/characters/Character_002_Walk.png differ diff --git a/art/sprites/characters/Character_002_Walk.png.import b/art/sprites/characters/Character_002_Walk.png.import new file mode 100644 index 0000000..9d84919 --- /dev/null +++ b/art/sprites/characters/Character_002_Walk.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bris5vy0xmie6" +path="res://.godot/imported/Character_002_Walk.png-2734f78de0275bc238cdbc3320fddda0.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_002_Walk.png" +dest_files=["res://.godot/imported/Character_002_Walk.png-2734f78de0275bc238cdbc3320fddda0.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 diff --git a/art/sprites/characters/Character_003_Dead.png b/art/sprites/characters/Character_003_Dead.png new file mode 100644 index 0000000..eb0ff13 Binary files /dev/null and b/art/sprites/characters/Character_003_Dead.png differ diff --git a/art/sprites/characters/Character_003_Dead.png.import b/art/sprites/characters/Character_003_Dead.png.import new file mode 100644 index 0000000..f5537bf --- /dev/null +++ b/art/sprites/characters/Character_003_Dead.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dqdudpnru30hk" +path="res://.godot/imported/Character_003_Dead.png-1d1221ee02cee53c8990f3b4ff17debf.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_003_Dead.png" +dest_files=["res://.godot/imported/Character_003_Dead.png-1d1221ee02cee53c8990f3b4ff17debf.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 diff --git a/art/sprites/characters/Character_003_Idle.png b/art/sprites/characters/Character_003_Idle.png new file mode 100644 index 0000000..f4eb96d Binary files /dev/null and b/art/sprites/characters/Character_003_Idle.png differ diff --git a/art/sprites/characters/Character_003_Idle.png.import b/art/sprites/characters/Character_003_Idle.png.import new file mode 100644 index 0000000..45b1af1 --- /dev/null +++ b/art/sprites/characters/Character_003_Idle.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cmirhu1042dd2" +path="res://.godot/imported/Character_003_Idle.png-a766339ea476ee9c81ef530027361cec.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_003_Idle.png" +dest_files=["res://.godot/imported/Character_003_Idle.png-a766339ea476ee9c81ef530027361cec.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 diff --git a/art/sprites/characters/Character_003_Walk.png b/art/sprites/characters/Character_003_Walk.png new file mode 100644 index 0000000..d09814d Binary files /dev/null and b/art/sprites/characters/Character_003_Walk.png differ diff --git a/art/sprites/characters/Character_003_Walk.png.import b/art/sprites/characters/Character_003_Walk.png.import new file mode 100644 index 0000000..13465a5 --- /dev/null +++ b/art/sprites/characters/Character_003_Walk.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://qqwwf465ak8y" +path="res://.godot/imported/Character_003_Walk.png-00de2bb6ade98a02f8b195e4d53e072f.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_003_Walk.png" +dest_files=["res://.godot/imported/Character_003_Walk.png-00de2bb6ade98a02f8b195e4d53e072f.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 diff --git a/art/sprites/characters/Character_004_Dead.png b/art/sprites/characters/Character_004_Dead.png new file mode 100644 index 0000000..fe4dbc7 Binary files /dev/null and b/art/sprites/characters/Character_004_Dead.png differ diff --git a/art/sprites/characters/Character_004_Dead.png.import b/art/sprites/characters/Character_004_Dead.png.import new file mode 100644 index 0000000..59c766b --- /dev/null +++ b/art/sprites/characters/Character_004_Dead.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cusg1ywjaxe38" +path="res://.godot/imported/Character_004_Dead.png-981400b980c80c3710e64738bd1cf562.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_004_Dead.png" +dest_files=["res://.godot/imported/Character_004_Dead.png-981400b980c80c3710e64738bd1cf562.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 diff --git a/art/sprites/characters/Character_004_Idle.png b/art/sprites/characters/Character_004_Idle.png new file mode 100644 index 0000000..c591502 Binary files /dev/null and b/art/sprites/characters/Character_004_Idle.png differ diff --git a/art/sprites/characters/Character_004_Idle.png.import b/art/sprites/characters/Character_004_Idle.png.import new file mode 100644 index 0000000..b335ea3 --- /dev/null +++ b/art/sprites/characters/Character_004_Idle.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bgke8dpxels81" +path="res://.godot/imported/Character_004_Idle.png-342dbbc4500cc6871b75d10b5a4fc95f.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_004_Idle.png" +dest_files=["res://.godot/imported/Character_004_Idle.png-342dbbc4500cc6871b75d10b5a4fc95f.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 diff --git a/art/sprites/characters/Character_004_Walk.png b/art/sprites/characters/Character_004_Walk.png new file mode 100644 index 0000000..6991792 Binary files /dev/null and b/art/sprites/characters/Character_004_Walk.png differ diff --git a/art/sprites/characters/Character_004_Walk.png.import b/art/sprites/characters/Character_004_Walk.png.import new file mode 100644 index 0000000..c0fd46d --- /dev/null +++ b/art/sprites/characters/Character_004_Walk.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cm05cheuxkrtm" +path="res://.godot/imported/Character_004_Walk.png-d111e552e0083c96124a1b00f2599f41.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_004_Walk.png" +dest_files=["res://.godot/imported/Character_004_Walk.png-d111e552e0083c96124a1b00f2599f41.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 diff --git a/art/sprites/characters/Character_005_Dead.png b/art/sprites/characters/Character_005_Dead.png new file mode 100644 index 0000000..5f3a593 Binary files /dev/null and b/art/sprites/characters/Character_005_Dead.png differ diff --git a/art/sprites/characters/Character_005_Dead.png.import b/art/sprites/characters/Character_005_Dead.png.import new file mode 100644 index 0000000..d1a7970 --- /dev/null +++ b/art/sprites/characters/Character_005_Dead.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c1kkoepbenudx" +path="res://.godot/imported/Character_005_Dead.png-71cd93adab35964081cdb8b40879e3f1.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_005_Dead.png" +dest_files=["res://.godot/imported/Character_005_Dead.png-71cd93adab35964081cdb8b40879e3f1.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 diff --git a/art/sprites/characters/Character_005_Idle.png b/art/sprites/characters/Character_005_Idle.png new file mode 100644 index 0000000..bb5b062 Binary files /dev/null and b/art/sprites/characters/Character_005_Idle.png differ diff --git a/art/sprites/characters/Character_005_Idle.png.import b/art/sprites/characters/Character_005_Idle.png.import new file mode 100644 index 0000000..7ac7c53 --- /dev/null +++ b/art/sprites/characters/Character_005_Idle.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://doq7u1lurebfx" +path="res://.godot/imported/Character_005_Idle.png-26dd621a4b4777f2380bc2c5b6138d38.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_005_Idle.png" +dest_files=["res://.godot/imported/Character_005_Idle.png-26dd621a4b4777f2380bc2c5b6138d38.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 diff --git a/art/sprites/characters/Character_005_Walk.png b/art/sprites/characters/Character_005_Walk.png new file mode 100644 index 0000000..416e4d5 Binary files /dev/null and b/art/sprites/characters/Character_005_Walk.png differ diff --git a/art/sprites/characters/Character_005_Walk.png.import b/art/sprites/characters/Character_005_Walk.png.import new file mode 100644 index 0000000..fac2c69 --- /dev/null +++ b/art/sprites/characters/Character_005_Walk.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://btabpxr6tfheg" +path="res://.godot/imported/Character_005_Walk.png-3f89560bbea7fdef9a96610b7ac147fa.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_005_Walk.png" +dest_files=["res://.godot/imported/Character_005_Walk.png-3f89560bbea7fdef9a96610b7ac147fa.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 diff --git a/art/sprites/characters/Character_006_Dead.png b/art/sprites/characters/Character_006_Dead.png new file mode 100644 index 0000000..dfc8ff6 Binary files /dev/null and b/art/sprites/characters/Character_006_Dead.png differ diff --git a/art/sprites/characters/Character_006_Dead.png.import b/art/sprites/characters/Character_006_Dead.png.import new file mode 100644 index 0000000..c75c94c --- /dev/null +++ b/art/sprites/characters/Character_006_Dead.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b4o73ivsxbg75" +path="res://.godot/imported/Character_006_Dead.png-8aed15fa6599b13181065136d33bde6f.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_006_Dead.png" +dest_files=["res://.godot/imported/Character_006_Dead.png-8aed15fa6599b13181065136d33bde6f.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 diff --git a/art/sprites/characters/Character_006_Idle.png b/art/sprites/characters/Character_006_Idle.png new file mode 100644 index 0000000..5cdea3e Binary files /dev/null and b/art/sprites/characters/Character_006_Idle.png differ diff --git a/art/sprites/characters/Character_006_Idle.png.import b/art/sprites/characters/Character_006_Idle.png.import new file mode 100644 index 0000000..89d1b5a --- /dev/null +++ b/art/sprites/characters/Character_006_Idle.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c2vhpfcn6ayla" +path="res://.godot/imported/Character_006_Idle.png-87c1a57347f63318a7da74a3696653fd.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_006_Idle.png" +dest_files=["res://.godot/imported/Character_006_Idle.png-87c1a57347f63318a7da74a3696653fd.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 diff --git a/art/sprites/characters/Character_006_Walk.png b/art/sprites/characters/Character_006_Walk.png new file mode 100644 index 0000000..2a4e180 Binary files /dev/null and b/art/sprites/characters/Character_006_Walk.png differ diff --git a/art/sprites/characters/Character_006_Walk.png.import b/art/sprites/characters/Character_006_Walk.png.import new file mode 100644 index 0000000..bb5bb39 --- /dev/null +++ b/art/sprites/characters/Character_006_Walk.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bq5ba7puv8mx1" +path="res://.godot/imported/Character_006_Walk.png-751a8de1d914918d6e78a313db34ee8e.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_006_Walk.png" +dest_files=["res://.godot/imported/Character_006_Walk.png-751a8de1d914918d6e78a313db34ee8e.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 diff --git a/art/sprites/characters/Character_007_Dead.png b/art/sprites/characters/Character_007_Dead.png new file mode 100644 index 0000000..729e554 Binary files /dev/null and b/art/sprites/characters/Character_007_Dead.png differ diff --git a/art/sprites/characters/Character_007_Dead.png.import b/art/sprites/characters/Character_007_Dead.png.import new file mode 100644 index 0000000..1d4fa58 --- /dev/null +++ b/art/sprites/characters/Character_007_Dead.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://fvs0homlm10v" +path="res://.godot/imported/Character_007_Dead.png-5786612961751424d5bf62e4dbe77874.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_007_Dead.png" +dest_files=["res://.godot/imported/Character_007_Dead.png-5786612961751424d5bf62e4dbe77874.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 diff --git a/art/sprites/characters/Character_007_Idle.png b/art/sprites/characters/Character_007_Idle.png new file mode 100644 index 0000000..3e3f728 Binary files /dev/null and b/art/sprites/characters/Character_007_Idle.png differ diff --git a/art/sprites/characters/Character_007_Idle.png.import b/art/sprites/characters/Character_007_Idle.png.import new file mode 100644 index 0000000..326b047 --- /dev/null +++ b/art/sprites/characters/Character_007_Idle.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://xc2nntg87xji" +path="res://.godot/imported/Character_007_Idle.png-0fa929882b6b9f3c123a27843c440ecd.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_007_Idle.png" +dest_files=["res://.godot/imported/Character_007_Idle.png-0fa929882b6b9f3c123a27843c440ecd.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 diff --git a/art/sprites/characters/Character_007_Walk.png b/art/sprites/characters/Character_007_Walk.png new file mode 100644 index 0000000..179e421 Binary files /dev/null and b/art/sprites/characters/Character_007_Walk.png differ diff --git a/art/sprites/characters/Character_007_Walk.png.import b/art/sprites/characters/Character_007_Walk.png.import new file mode 100644 index 0000000..c26bdcc --- /dev/null +++ b/art/sprites/characters/Character_007_Walk.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dkojdn2l3jl0n" +path="res://.godot/imported/Character_007_Walk.png-34af3247b8cb2b63fd059aac5242294c.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_007_Walk.png" +dest_files=["res://.godot/imported/Character_007_Walk.png-34af3247b8cb2b63fd059aac5242294c.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 diff --git a/art/sprites/characters/Character_008_Dead.png b/art/sprites/characters/Character_008_Dead.png new file mode 100644 index 0000000..72cba1e Binary files /dev/null and b/art/sprites/characters/Character_008_Dead.png differ diff --git a/art/sprites/characters/Character_008_Dead.png.import b/art/sprites/characters/Character_008_Dead.png.import new file mode 100644 index 0000000..c0ac8ed --- /dev/null +++ b/art/sprites/characters/Character_008_Dead.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://di1brsgcgwscn" +path="res://.godot/imported/Character_008_Dead.png-fd70bff68e81ace60033e4d24809d9fe.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_008_Dead.png" +dest_files=["res://.godot/imported/Character_008_Dead.png-fd70bff68e81ace60033e4d24809d9fe.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 diff --git a/art/sprites/characters/Character_008_Idle.png b/art/sprites/characters/Character_008_Idle.png new file mode 100644 index 0000000..59bee34 Binary files /dev/null and b/art/sprites/characters/Character_008_Idle.png differ diff --git a/art/sprites/characters/Character_008_Idle.png.import b/art/sprites/characters/Character_008_Idle.png.import new file mode 100644 index 0000000..e816ef1 --- /dev/null +++ b/art/sprites/characters/Character_008_Idle.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cvvpak354pxb7" +path="res://.godot/imported/Character_008_Idle.png-3f55478f0d4f6dabc7b8b70c3d3a37cd.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_008_Idle.png" +dest_files=["res://.godot/imported/Character_008_Idle.png-3f55478f0d4f6dabc7b8b70c3d3a37cd.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 diff --git a/art/sprites/characters/Character_008_Walk.png b/art/sprites/characters/Character_008_Walk.png new file mode 100644 index 0000000..771466c Binary files /dev/null and b/art/sprites/characters/Character_008_Walk.png differ diff --git a/art/sprites/characters/Character_008_Walk.png.import b/art/sprites/characters/Character_008_Walk.png.import new file mode 100644 index 0000000..e65739e --- /dev/null +++ b/art/sprites/characters/Character_008_Walk.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c8stsomyh2rq7" +path="res://.godot/imported/Character_008_Walk.png-2275a9017262a05b52e927ec771e2094.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_008_Walk.png" +dest_files=["res://.godot/imported/Character_008_Walk.png-2275a9017262a05b52e927ec771e2094.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 diff --git a/art/sprites/characters/Character_009_Dead.png b/art/sprites/characters/Character_009_Dead.png new file mode 100644 index 0000000..fa1880a Binary files /dev/null and b/art/sprites/characters/Character_009_Dead.png differ diff --git a/art/sprites/characters/Character_009_Dead.png.import b/art/sprites/characters/Character_009_Dead.png.import new file mode 100644 index 0000000..f410750 --- /dev/null +++ b/art/sprites/characters/Character_009_Dead.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bnq8iug5dhdag" +path="res://.godot/imported/Character_009_Dead.png-72043e0166ec8cec41355a2e3a00d7bd.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_009_Dead.png" +dest_files=["res://.godot/imported/Character_009_Dead.png-72043e0166ec8cec41355a2e3a00d7bd.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 diff --git a/art/sprites/characters/Character_009_Idle.png b/art/sprites/characters/Character_009_Idle.png new file mode 100644 index 0000000..4fbf47e Binary files /dev/null and b/art/sprites/characters/Character_009_Idle.png differ diff --git a/art/sprites/characters/Character_009_Idle.png.import b/art/sprites/characters/Character_009_Idle.png.import new file mode 100644 index 0000000..27428e3 --- /dev/null +++ b/art/sprites/characters/Character_009_Idle.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dhcaxufn8awxr" +path="res://.godot/imported/Character_009_Idle.png-0ab1c95e4e42dd0240b47ad8441fa4c7.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_009_Idle.png" +dest_files=["res://.godot/imported/Character_009_Idle.png-0ab1c95e4e42dd0240b47ad8441fa4c7.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 diff --git a/art/sprites/characters/Character_009_Walk.png b/art/sprites/characters/Character_009_Walk.png new file mode 100644 index 0000000..2ed34cd Binary files /dev/null and b/art/sprites/characters/Character_009_Walk.png differ diff --git a/art/sprites/characters/Character_009_Walk.png.import b/art/sprites/characters/Character_009_Walk.png.import new file mode 100644 index 0000000..ac9d72e --- /dev/null +++ b/art/sprites/characters/Character_009_Walk.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c6ytplnnfb3ou" +path="res://.godot/imported/Character_009_Walk.png-6cd650d24ac756f6bfd224f999d84b8f.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_009_Walk.png" +dest_files=["res://.godot/imported/Character_009_Walk.png-6cd650d24ac756f6bfd224f999d84b8f.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 diff --git a/art/sprites/characters/Character_010_Dead.png b/art/sprites/characters/Character_010_Dead.png new file mode 100644 index 0000000..296d4db Binary files /dev/null and b/art/sprites/characters/Character_010_Dead.png differ diff --git a/art/sprites/characters/Character_010_Dead.png.import b/art/sprites/characters/Character_010_Dead.png.import new file mode 100644 index 0000000..71b0b4b --- /dev/null +++ b/art/sprites/characters/Character_010_Dead.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://wjckyu6cd8v6" +path="res://.godot/imported/Character_010_Dead.png-89846b459c078f30bee90118f78a0755.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_010_Dead.png" +dest_files=["res://.godot/imported/Character_010_Dead.png-89846b459c078f30bee90118f78a0755.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 diff --git a/art/sprites/characters/Character_010_Idle.png b/art/sprites/characters/Character_010_Idle.png new file mode 100644 index 0000000..878f08a Binary files /dev/null and b/art/sprites/characters/Character_010_Idle.png differ diff --git a/art/sprites/characters/Character_010_Idle.png.import b/art/sprites/characters/Character_010_Idle.png.import new file mode 100644 index 0000000..c1fe0e0 --- /dev/null +++ b/art/sprites/characters/Character_010_Idle.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cn7ar2h01prir" +path="res://.godot/imported/Character_010_Idle.png-dbb1408fdad0da2fd24a4c1fe75ae620.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_010_Idle.png" +dest_files=["res://.godot/imported/Character_010_Idle.png-dbb1408fdad0da2fd24a4c1fe75ae620.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 diff --git a/art/sprites/characters/Character_010_Walk.png b/art/sprites/characters/Character_010_Walk.png new file mode 100644 index 0000000..870b589 Binary files /dev/null and b/art/sprites/characters/Character_010_Walk.png differ diff --git a/art/sprites/characters/Character_010_Walk.png.import b/art/sprites/characters/Character_010_Walk.png.import new file mode 100644 index 0000000..80b445a --- /dev/null +++ b/art/sprites/characters/Character_010_Walk.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dkfceshiwp5jt" +path="res://.godot/imported/Character_010_Walk.png-420aca922b9f619ed9216c39355318ab.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_010_Walk.png" +dest_files=["res://.godot/imported/Character_010_Walk.png-420aca922b9f619ed9216c39355318ab.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 diff --git a/art/sprites/characters/Character_011_Dead.png b/art/sprites/characters/Character_011_Dead.png new file mode 100644 index 0000000..857cb83 Binary files /dev/null and b/art/sprites/characters/Character_011_Dead.png differ diff --git a/art/sprites/characters/Character_011_Dead.png.import b/art/sprites/characters/Character_011_Dead.png.import new file mode 100644 index 0000000..7fd918d --- /dev/null +++ b/art/sprites/characters/Character_011_Dead.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://doa0vm7xo5f1b" +path="res://.godot/imported/Character_011_Dead.png-c52196730c14299395595c4ef3547c34.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_011_Dead.png" +dest_files=["res://.godot/imported/Character_011_Dead.png-c52196730c14299395595c4ef3547c34.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 diff --git a/art/sprites/characters/Character_011_Idle.png b/art/sprites/characters/Character_011_Idle.png new file mode 100644 index 0000000..62eb941 Binary files /dev/null and b/art/sprites/characters/Character_011_Idle.png differ diff --git a/art/sprites/characters/Character_011_Idle.png.import b/art/sprites/characters/Character_011_Idle.png.import new file mode 100644 index 0000000..9756dce --- /dev/null +++ b/art/sprites/characters/Character_011_Idle.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dgm07ylwoxuqr" +path="res://.godot/imported/Character_011_Idle.png-079aba020f914b64188e3e11000d6489.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_011_Idle.png" +dest_files=["res://.godot/imported/Character_011_Idle.png-079aba020f914b64188e3e11000d6489.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 diff --git a/art/sprites/characters/Character_011_Walk.png b/art/sprites/characters/Character_011_Walk.png new file mode 100644 index 0000000..d18a9ea Binary files /dev/null and b/art/sprites/characters/Character_011_Walk.png differ diff --git a/art/sprites/characters/Character_011_Walk.png.import b/art/sprites/characters/Character_011_Walk.png.import new file mode 100644 index 0000000..82e49f4 --- /dev/null +++ b/art/sprites/characters/Character_011_Walk.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://caxnji7qtoftv" +path="res://.godot/imported/Character_011_Walk.png-e30d6c424901068d6cb1d62875371473.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_011_Walk.png" +dest_files=["res://.godot/imported/Character_011_Walk.png-e30d6c424901068d6cb1d62875371473.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 diff --git a/art/sprites/characters/Character_012_Dead.png b/art/sprites/characters/Character_012_Dead.png new file mode 100644 index 0000000..aed9b74 Binary files /dev/null and b/art/sprites/characters/Character_012_Dead.png differ diff --git a/art/sprites/characters/Character_012_Dead.png.import b/art/sprites/characters/Character_012_Dead.png.import new file mode 100644 index 0000000..e9e91a1 --- /dev/null +++ b/art/sprites/characters/Character_012_Dead.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://pq2m7142tryg" +path="res://.godot/imported/Character_012_Dead.png-603daed2c7a3b9483442a2b9603dea68.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_012_Dead.png" +dest_files=["res://.godot/imported/Character_012_Dead.png-603daed2c7a3b9483442a2b9603dea68.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 diff --git a/art/sprites/characters/Character_012_Idle.png b/art/sprites/characters/Character_012_Idle.png new file mode 100644 index 0000000..d91fe7e Binary files /dev/null and b/art/sprites/characters/Character_012_Idle.png differ diff --git a/art/sprites/characters/Character_012_Idle.png.import b/art/sprites/characters/Character_012_Idle.png.import new file mode 100644 index 0000000..3c8e414 --- /dev/null +++ b/art/sprites/characters/Character_012_Idle.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://y6bkvxi65e7v" +path="res://.godot/imported/Character_012_Idle.png-06d2acb1777ac4e3d84cc727341dcae5.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_012_Idle.png" +dest_files=["res://.godot/imported/Character_012_Idle.png-06d2acb1777ac4e3d84cc727341dcae5.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 diff --git a/art/sprites/characters/Character_012_Walk.png b/art/sprites/characters/Character_012_Walk.png new file mode 100644 index 0000000..da7ea22 Binary files /dev/null and b/art/sprites/characters/Character_012_Walk.png differ diff --git a/art/sprites/characters/Character_012_Walk.png.import b/art/sprites/characters/Character_012_Walk.png.import new file mode 100644 index 0000000..22ed979 --- /dev/null +++ b/art/sprites/characters/Character_012_Walk.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bsekmrv5pihek" +path="res://.godot/imported/Character_012_Walk.png-716625a565eca83c3c36b0419ddd28f1.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_012_Walk.png" +dest_files=["res://.godot/imported/Character_012_Walk.png-716625a565eca83c3c36b0419ddd28f1.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 diff --git a/art/sprites/characters/Character_013_Dead.png b/art/sprites/characters/Character_013_Dead.png new file mode 100644 index 0000000..b2f06dc Binary files /dev/null and b/art/sprites/characters/Character_013_Dead.png differ diff --git a/art/sprites/characters/Character_013_Dead.png.import b/art/sprites/characters/Character_013_Dead.png.import new file mode 100644 index 0000000..63596dc --- /dev/null +++ b/art/sprites/characters/Character_013_Dead.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://daukgomwycsgk" +path="res://.godot/imported/Character_013_Dead.png-49b06f46b7fa82025b0265b91dca90b2.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_013_Dead.png" +dest_files=["res://.godot/imported/Character_013_Dead.png-49b06f46b7fa82025b0265b91dca90b2.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 diff --git a/art/sprites/characters/Character_013_Idle.png b/art/sprites/characters/Character_013_Idle.png new file mode 100644 index 0000000..0906628 Binary files /dev/null and b/art/sprites/characters/Character_013_Idle.png differ diff --git a/art/sprites/characters/Character_013_Idle.png.import b/art/sprites/characters/Character_013_Idle.png.import new file mode 100644 index 0000000..5cbe2bd --- /dev/null +++ b/art/sprites/characters/Character_013_Idle.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ddkfrmcwb11bp" +path="res://.godot/imported/Character_013_Idle.png-7a5d4c80884c699f3b7a04bcfdddcc2b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_013_Idle.png" +dest_files=["res://.godot/imported/Character_013_Idle.png-7a5d4c80884c699f3b7a04bcfdddcc2b.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 diff --git a/art/sprites/characters/Character_013_Walk.png b/art/sprites/characters/Character_013_Walk.png new file mode 100644 index 0000000..692f7cd Binary files /dev/null and b/art/sprites/characters/Character_013_Walk.png differ diff --git a/art/sprites/characters/Character_013_Walk.png.import b/art/sprites/characters/Character_013_Walk.png.import new file mode 100644 index 0000000..58876e9 --- /dev/null +++ b/art/sprites/characters/Character_013_Walk.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://be4twnnlpy6m2" +path="res://.godot/imported/Character_013_Walk.png-03bdbadd3a5e3963af2bea56f7d34b60.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_013_Walk.png" +dest_files=["res://.godot/imported/Character_013_Walk.png-03bdbadd3a5e3963af2bea56f7d34b60.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 diff --git a/art/sprites/characters/Character_014_Dead.png b/art/sprites/characters/Character_014_Dead.png new file mode 100644 index 0000000..d4d8ae2 Binary files /dev/null and b/art/sprites/characters/Character_014_Dead.png differ diff --git a/art/sprites/characters/Character_014_Dead.png.import b/art/sprites/characters/Character_014_Dead.png.import new file mode 100644 index 0000000..1a34f95 --- /dev/null +++ b/art/sprites/characters/Character_014_Dead.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cvwe1xf2f73y3" +path="res://.godot/imported/Character_014_Dead.png-23c4d06b6e3863989e695afff750ed5c.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_014_Dead.png" +dest_files=["res://.godot/imported/Character_014_Dead.png-23c4d06b6e3863989e695afff750ed5c.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 diff --git a/art/sprites/characters/Character_014_Idle.png b/art/sprites/characters/Character_014_Idle.png new file mode 100644 index 0000000..89b99a8 Binary files /dev/null and b/art/sprites/characters/Character_014_Idle.png differ diff --git a/art/sprites/characters/Character_014_Idle.png.import b/art/sprites/characters/Character_014_Idle.png.import new file mode 100644 index 0000000..0395279 --- /dev/null +++ b/art/sprites/characters/Character_014_Idle.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b777g0yu7owor" +path="res://.godot/imported/Character_014_Idle.png-8314784856576367ed93bcb8e4cc27aa.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_014_Idle.png" +dest_files=["res://.godot/imported/Character_014_Idle.png-8314784856576367ed93bcb8e4cc27aa.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 diff --git a/art/sprites/characters/Character_014_Walk.png b/art/sprites/characters/Character_014_Walk.png new file mode 100644 index 0000000..d12c507 Binary files /dev/null and b/art/sprites/characters/Character_014_Walk.png differ diff --git a/art/sprites/characters/Character_014_Walk.png.import b/art/sprites/characters/Character_014_Walk.png.import new file mode 100644 index 0000000..7121bd1 --- /dev/null +++ b/art/sprites/characters/Character_014_Walk.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://vpdry2yyekvo" +path="res://.godot/imported/Character_014_Walk.png-bab0cfac7c66a94dcc7be5d5366bb948.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_014_Walk.png" +dest_files=["res://.godot/imported/Character_014_Walk.png-bab0cfac7c66a94dcc7be5d5366bb948.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 diff --git a/art/sprites/characters/Character_015_Dead.png b/art/sprites/characters/Character_015_Dead.png new file mode 100644 index 0000000..da5abe1 Binary files /dev/null and b/art/sprites/characters/Character_015_Dead.png differ diff --git a/art/sprites/characters/Character_015_Dead.png.import b/art/sprites/characters/Character_015_Dead.png.import new file mode 100644 index 0000000..42fc6aa --- /dev/null +++ b/art/sprites/characters/Character_015_Dead.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://n71lbmxkvioh" +path="res://.godot/imported/Character_015_Dead.png-b074949bcb971d96a392e2128c495ea5.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_015_Dead.png" +dest_files=["res://.godot/imported/Character_015_Dead.png-b074949bcb971d96a392e2128c495ea5.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 diff --git a/art/sprites/characters/Character_015_Idle.png b/art/sprites/characters/Character_015_Idle.png new file mode 100644 index 0000000..f099ff3 Binary files /dev/null and b/art/sprites/characters/Character_015_Idle.png differ diff --git a/art/sprites/characters/Character_015_Idle.png.import b/art/sprites/characters/Character_015_Idle.png.import new file mode 100644 index 0000000..0005f2c --- /dev/null +++ b/art/sprites/characters/Character_015_Idle.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://26c1bs1sbswi" +path="res://.godot/imported/Character_015_Idle.png-60b87cac6c8d2f27723308ffa4335d7d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_015_Idle.png" +dest_files=["res://.godot/imported/Character_015_Idle.png-60b87cac6c8d2f27723308ffa4335d7d.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 diff --git a/art/sprites/characters/Character_015_Walk.png b/art/sprites/characters/Character_015_Walk.png new file mode 100644 index 0000000..5cccaf0 Binary files /dev/null and b/art/sprites/characters/Character_015_Walk.png differ diff --git a/art/sprites/characters/Character_015_Walk.png.import b/art/sprites/characters/Character_015_Walk.png.import new file mode 100644 index 0000000..7f58cbf --- /dev/null +++ b/art/sprites/characters/Character_015_Walk.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://by474ej7lcv2q" +path="res://.godot/imported/Character_015_Walk.png-460f8657e875d12969b60f14c3147049.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_015_Walk.png" +dest_files=["res://.godot/imported/Character_015_Walk.png-460f8657e875d12969b60f14c3147049.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 diff --git a/autoload/event_bus.gd b/autoload/event_bus.gd index dc9a8b2..81de2c8 100644 --- a/autoload/event_bus.gd +++ b/autoload/event_bus.gd @@ -55,6 +55,8 @@ signal load_finished(slot: StringName, ok: bool, real_seconds_away: int) ## Emit # Phase 17 — Touch UX completion. signal pawn_selected(pawn) ## Emitted when Selection picks a pawn — opens PawnDetailPanel. signal pawn_deselected ## Emitted when Selection clears — closes PawnDetailPanel. +signal workbench_selected(workbench) +signal workbench_deselected signal pawn_priority_changed(pawn, category: StringName, level: int) ## Emitted when priority matrix updates a cell. signal alert_added(severity: StringName, text: String, focus_tile: Vector2i) ## Emitted by gameplay subsystems to surface a player notice. severity = info | warn | danger. signal request_wolf_spawn(count: int) ## Phase 15 EventCatalog → WolfSpawner. Decouples threat-event effects from spawner. diff --git a/autoload/save_system.gd b/autoload/save_system.gd index 115d730..6d134ce 100644 --- a/autoload/save_system.gd +++ b/autoload/save_system.gd @@ -29,6 +29,7 @@ const _PAWN_SCENE: PackedScene = preload("res://scenes/pawn/pawn.tscn") const _TREE_SCENE: PackedScene = preload("res://scenes/entities/tree.tscn") const _ROCK_SCENE: PackedScene = preload("res://scenes/entities/rock.tscn") const _BIG_ROCK_SCENE: PackedScene = preload("res://scenes/entities/big_rock.tscn") +const _BIG_ROCK_NODE_SCENE: PackedScene = preload("res://scenes/entities/big_rock_node.tscn") const _ITEM_SCENE: PackedScene = preload("res://scenes/entities/item.tscn") const _WALL_SCENE: PackedScene = preload("res://scenes/entities/wall.tscn") const _FLOOR_SCENE: PackedScene = preload("res://scenes/entities/floor.tscn") @@ -276,6 +277,7 @@ func _collect_entities() -> Array: var registries: Array = [ World.trees, World.rocks, + World.big_rock_nodes, World.items, World.build_queue, # ghost walls / floors / doors / grave_slots World.doors, @@ -316,6 +318,7 @@ const _SPAWN_PRIORITY: Dictionary = { &"tree": 0, &"rock": 0, &"big_rock": 0, + &"big_rock_node": 0, &"wall": 0, &"floor": 0, &"door": 1, @@ -351,6 +354,7 @@ func _register_factories() -> void: _factories[&"tree"] = _spawn_tree _factories[&"rock"] = _spawn_rock _factories[&"big_rock"] = _spawn_big_rock + _factories[&"big_rock_node"] = _spawn_big_rock_node _factories[&"item"] = _spawn_item _factories[&"wall"] = _spawn_wall _factories[&"floor"] = _spawn_floor @@ -377,8 +381,18 @@ func _register_factories() -> void: func _spawn_tree(world_scene: Node, d: Dictionary) -> void: var ent = _TREE_SCENE.instantiate() world_scene.add_child(ent) - ent.setup(Vector2i(int(d.get("tile_x", 0)), int(d.get("tile_y", 0)))) + # Pass growth_stage to setup() so _refresh_sprite() picks the right visual. + # Default 3 (STAGE_MATURE) so pre-growth-system saves load as mature trees. + var gs: int = int(d.get("growth_stage", 3)) + ent.setup(Vector2i(int(d.get("tile_x", 0)), int(d.get("tile_y", 0))), gs) ent.chop_progress = int(d.get("chop_progress", 0)) + ent.growth_progress = int(d.get("growth_progress", 0)) + ent.chop_designated = bool(d.get("chop_designated", false)) + ent.pending_plant = bool(d.get("pending_plant", false)) + ent._plant_progress = int(d.get("plant_progress", 0)) + # Re-register as build site if planting is still in progress. + if ent.pending_plant: + World.register_build_site(ent) ent.queue_redraw() @@ -398,6 +412,14 @@ func _spawn_big_rock(world_scene: Node, d: Dictionary) -> void: ent.queue_redraw() +func _spawn_big_rock_node(world_scene: Node, d: Dictionary) -> void: + var ent = _BIG_ROCK_NODE_SCENE.instantiate() + world_scene.add_child(ent) + ent.setup(Vector2i(int(d.get("tile_x", 0)), int(d.get("tile_y", 0)))) + ent.is_quarry_site = bool(d.get("is_quarry_site", false)) + ent.queue_redraw() + + func _spawn_item(world_scene: Node, d: Dictionary) -> void: var ent = _ITEM_SCENE.instantiate() world_scene.add_child(ent) @@ -407,6 +429,8 @@ func _spawn_item(world_scene: Node, d: Dictionary) -> void: Vector2i(int(d.get("tile_x", 0)), int(d.get("tile_y", 0))) ) ent.quality = int(d.get("quality", 1)) as Item.Quality + ent.subtype = StringName(d.get("subtype", "")) + ent.queue_redraw() func _spawn_wall(world_scene: Node, d: Dictionary) -> void: diff --git a/autoload/strings.gd b/autoload/strings.gd index c67813c..a96fe0e 100644 --- a/autoload/strings.gd +++ b/autoload/strings.gd @@ -194,8 +194,27 @@ const TABLE: Dictionary = { &"tool.workbench_millstone": "Millstone", &"tool.workbench_hearth": "Hearth", &"tool.workbench_cremation_pyre": "Cremation Pyre", + &"tool.paint_quarry": "Build quarry", &"tool.stockpile_general": "Stockpile", &"tool.graveyard": "Graveyard", + &"tool.plant_tree": "Plant tree", + # Tree growth stage names (shown in inspect tooltip). + &"tree.stage.sapling": "Sapling", + &"tree.stage.young": "Young tree", + &"tree.stage.growing": "Growing tree", + &"tree.stage.mature": "Mature tree", + &"ui.bill.mode_forever": "Forever", + &"ui.bill.mode_count": "Do X times", + &"ui.bill.mode_until_n": "Do until X", + &"ui.bill.target": "Target", + &"ui.bill.until_count": "Until count", + &"ui.bill.completed": "Done", + &"ui.bill.pause": "Pause", + &"ui.bill.remove": "Remove", + &"ui.bill.add_button": "Add bill", + &"ui.bill.no_bills_hint": "No bills. Add one to start crafting.", + &"ui.workbench.current_bill": "Current", + &"ui.workbench.idle": "Idle", } diff --git a/autoload/world.gd b/autoload/world.gd index 9169dac..c85dc3c 100644 --- a/autoload/world.gd +++ b/autoload/world.gd @@ -19,6 +19,7 @@ var work_providers: Array = [] # from their _ready/_exit_tree. Phase 16 will add stable IDs and persistence wiring. var trees: Array = [] # Array of Tree var rocks: Array = [] # Array of Rock +var big_rock_nodes: Array = [] # Array of BigRockNode (permanent stone outcrops) var items: Array = [] # Array of Item (on-floor stacks) var stockpiles: Array = [] # Array of StorageDestination (StockpileZone for now; containers Phase 5) @@ -124,6 +125,15 @@ func pawn_at_tile(tile: Vector2i) -> Pawn: return null +## Returns the Workbench occupying `tile`, or null if none. Used by Selection +## to route taps on a workbench to the bill-editor panel. +func workbench_at_tile(tile: Vector2i): + for w in workbenches: + if w.tile == tile: + return w + return null + + func clear_pawns() -> void: # For save-load / new-game flow in Phase 16. pawns.clear() @@ -198,6 +208,24 @@ func unregister_rock(r) -> void: rocks.erase(r) +func register_big_rock_node(n) -> void: + if not big_rock_nodes.has(n): + big_rock_nodes.append(n) + + +func unregister_big_rock_node(n) -> void: + big_rock_nodes.erase(n) + + +## Returns the BigRockNode whose 2×2 footprint covers `tile`, or null. +## Used by `paint_quarry` designation to validate the build site. +func big_rock_node_at_tile(tile: Vector2i): + for n in big_rock_nodes: + if n.is_at(tile): + return n + return null + + func register_item(it) -> void: if items.has(it): return diff --git a/docs/implementation.md b/docs/implementation.md index 6c06643..1600fe9 100644 --- a/docs/implementation.md +++ b/docs/implementation.md @@ -395,7 +395,7 @@ The five items from `memory.md` *Open questions / Audit*. None of these need cod - [x] **Day-summary emission** — Clock emits `day_ended(summary)` at dusk→night with day/weather/season/pawns_alive/tension/wolves_alive recap. AlertsLog surfaces as a single-line entry per day. Full DaySummaryCard UI deferred. - [ ] Per-pawn / per-job views layered on the matrix — deferred (current matrix has read-write only, no view layers). - [ ] Stockpile / container 4×4 chip grid UI — deferred (paint creates 1×1 zones today; filter UI is data-only). -- [ ] Bill UI for workbenches — deferred (Phase 6 stub still in use; add/edit bills programmatically only). +- [x] **Bill UI for workbenches** (shipped 2026-05-16, out-of-phase). `WorkbenchPanel` bottom-sheet mirrors PawnDetailPanel (layer 18, right-anchored 360 px); tap a workbench → bill rows with mode toggle (FOREVER/COUNT/UNTIL_N), target SpinBox, pause CheckBox, remove button; Add-bill popup filters `RecipeCatalog.all()` by `accepted_skill`. Selection chain extended: pawn wins over workbench on shared tile, mutual-exclusion via `EventBus.workbench_selected/deselected`. Closes the Phase 6 stub — player-built workbenches are now configurable. - [ ] "No stockpile accepts X" + "Bill blocked" alerts — wiring stubs ready; emit calls in HaulingProvider / CraftingProvider deferred. - [ ] Day-summary card UI — deferred (signal emits; visual card is Phase 17.5). - [x] **Acceptance:** Hand-test verified — TopBar shows Save/Load/Settings/Build/Work/Log[N]; tap Bram → right panel shows all his state; tap Build → bottom drawer with 4 tabs; tap Work → grid of pawn priorities; tap Log → scrollable alerts list including the Spring Awakens storyteller event from boot. All UIs touch-friendly (48×48+ targets). Screenshots captured for all 4 surfaces. diff --git a/memory.md b/memory.md index a40bf4c..ec0ba97 100644 --- a/memory.md +++ b/memory.md @@ -293,6 +293,23 @@ Same scope as locked in `~/claude/ideas/rimlike/plan.md`. Realistic timeline 3 - **Pattern recorded — ".import auto-generation via `godot --headless --import`".** Writing `*.png.import` companion files by hand with placeholder paths works but Godot rewrites them with proper UIDs on the first import scan. Reliable workflow: copy PNG into `art/`, write minimal .import (UID will be ignored), then run `/mnt/d/godot/Godot_v4.6.2-stable_win64_console.exe --headless --import` once to generate the .ctex binaries. Headless boot fails until the import scan runs. Same pattern from 2026-05-12 confirmed. - Delegation report (whole-day 05-15 polish sprint): **No delegation — all on Opus.** Sprite-atlas surveys, MCP runtime verification (camera positioning, dialog dismissal, stage forcing), and the iterative play→fix→play loop kept the same files hot in context; Haiku/Sonnet handoffs would have re-read every time. The `researcher` (Haiku) dispatch from earlier in the day for the job-system audit is the only delegated work. +- **Workbenches rebuilt procedural after second atlas-misidentification.** The 2026-05-12 atlas-sprite pass for Carpenter/Smelter/Hearth/Millstone again hit the same trap as Bed: the picked tiles read as chest-of-drawers (Carpenter, FG_Interior 24,20), candle base (Smelter, FG_Marketplace 8,30), 2-burner stove (Hearth, FG_Interior 16,32), and cushion stack (Millstone, FG_Interior 17,40). User: "the carpenter is a chest of draws? no idea what the mill is, looks like a bucket". Survey of all 7 plausible source tilesets (Interior / Marketplace / Houses / Village / Fortress / Mines / Dark Castle / Extras) found a 2×2 medieval stone fireplace at FG_Interior (12,13)..(13,14) for Hearth but **2-wide tiles don't fit the current single-column sprite system**, and the other three had no clean replacement. Decision: go procedural for all four, following `CremationPyre._draw_pyre` as precedent. New methods `_draw_carpenter / _draw_smelter / _draw_hearth / _draw_millstone` in workbench.gd; `_VARIANT_SPRITES` table + `_build_sprite()` Sprite2D path removed; `_complete()` no longer touches a sprite child. Hearth is the only h=2 variant — draws stone surround + mantle + arched opening + log fire + flame teardrop extending into the tile north (Y-sort handles occlusion). Commit `c97ada8`. +- **Pattern recorded — "atlas-misidentification trap, second occurrence — prefer procedural for named furniture".** Pixel-art tile atlases pack hundreds of squat 16×16 items at low resolution; without zoomed-in inspection it's easy to mistake a furniture silhouette for what its name implies. Both Bed (2026-05-12) and the four workbenches (2026-05-15) hit this failure mode. Going forward: for furniture whose silhouette is **definitional** (workbench-with-tools, bed-with-pillow, millstone-as-grindstone), **start with procedural draws** and only swap in an atlas tile after verifying via 16× zoom that the tile actually depicts the named object. Atlas-sourced sprites are still the right call for generic decorative variation (trees, walls, floors, crops with growth stages). +- **Tree variety added via 3-season palette swap.** Copied `FG_Tree_Summer.png` + `FG_Tree_Fall.png` from the Grasslands tier-1 pack into `art/sprites/` (Spring was already there). 4 silhouettes × 3 palettes = 12 visual variants; `tree.gd` hash mixes silhouette and season independently so neighbouring tiles don't all share the same palette. Winter omitted — snowy trees would look out of place in the current biome. When a season-cycle system lands (future phase), the active texture can be swapped globally by season instead of per-tree. No growth stage / sapling system yet — bundle has no growth frames for trees, and the user's "regrow / auto-grow" ask is explicit future scope (deferred to a later phase that also covers planting designation). Commit `c97ada8`. + +### 2026-05-16 +- **Pre-made SW stockpiles + crates removed** so items sit where they're produced until the player paints storage (matches Rimworld parity). Deleted `_spawn_sample_stockpiles()` (two zones at (15,55) and (15,62)) and the two SW haul-target crates at (17,60)/(18,60). Kept the interior cabin crate at (50,23) as a starting amenity. Bills' `UNTIL_N` mode counts items anywhere in world (not just stockpiles) so the Smelter's "5 stone blocks" bill still terminates. All `World.stockpiles` consumers iterate the list — empty-list no-op, no crashes. Commit `c81e817`. +- **Workbench Bill Editor plan drafted** for the deferred Phase 17 item ("Bill UI for workbenches"). 10-step plan saved to user-memory at `~/.claude/projects/-mnt-d-godot-rimlike/memory/plan_bill_ui.md` (resilient to fresh sessions). Without this UI, every player-built workbench is functionally dead — build drawer can paint them but bills can only be set in code. Recipe.id field existence needs verification in Step 4. Starting execution 2026-05-16. +- **Workbench Bill Editor shipped same day** (commit `bdd4352`). Tap a workbench → right-side panel with bill rows (mode toggle, target spin, pause, remove) + Add-bill popup filtered by `accepted_skill`. Mirrors `PawnDetailPanel` exactly: layer 18, right-anchored 360 px, procedural `_build_ui()`, sim_tick refresh of the current-bill status only (bill list rebuilt on add/remove/select to preserve scroll). Selection chain extended in `selection.gd`: pawn-first then workbench, mutual-exclusion via new `EventBus.workbench_selected/deselected` signals. Closes Phase 17 deferred item. Code-level verification: headless boot clean, no runtime errors. Visual MCP verification still pending (need editor running). +- **Delegation report — bill UI sprint.** Steps 1, 3, 4, 6 (~5 mechanical edits across 5 files) → `quick-edit` (Haiku, 1 dispatch). Step 5 (new ~432-LOC WorkbenchPanel script needing full mirror of PawnDetailPanel + 6 source files) → `gdscript-refactor` (Sonnet, 1 dispatch). Steps 2, 7 (selection chain + main mount, needed design judgment about mutual-exclusion and main.gd's typed-var pattern) handled on Opus. Strings table follow-up (`ui.bill.until_count` key missed by Sonnet, fallback was a hardcoded literal) caught and fixed on Opus before commit. +- **Pattern recorded — "main.gd typed-var pattern requires CanvasLayer.new() + set_script(), not SCRIPT.new()".** First mount attempt used `WORKBENCH_PANEL_SCRIPT.new()` — Godot 4's parser refused with "Cannot infer the type" because `Script.new()` returns generic `Object`. Switched to `var x := CanvasLayer.new(); x.set_script(SCRIPT)` matching the rest of main.gd. Cheap parse error to surface via `--headless --quit`, but worth noting: subagents writing UI mount glue need this idiom explicitly. +- **Pattern recorded — "never free a widget from within its own signal callback".** Bill editor crashed when user changed mode FOREVER → UNTIL_N. Root cause: OptionButton.item_selected lambda called `_populate_bills()` directly, which clears + rebuilds all bill rows — freeing the very OptionButton whose signal was still emitting. Same pattern in the Remove button. Fix: `call_deferred("_populate_bills")` so the rebuild runs on the next idle frame after the signal frame completes. Commit `4e09dea`. Applies to any UI where a child Control's signal handler mutates a parent container — always defer rebuilds. + +- **Pawn reskin Slice 1 shipped** (commit `b4c9541`). Pawns now render as AnimatedSprite2D children sourced from ElvGames Farming Characters Pack (Pack 1, chars 001-015). 9 anims per pawn: idle×4dirs + walk×4dirs + dead. Atlas pick is `(absi(pawn_name.hash()) % 15) + 1` — Bram=004, Cora=013, Edda=001, all visually distinct. New `_atlas_for_pawn(pawn)` helper is the **single extension point** for the future Slice 2 (armor → atlas swap when equipment+combat phase ships). Slice 1 plan archived to user-memory at `~/.claude/projects/-mnt-d-godot-rimlike/memory/plan_pawn_reskin_slice1.md`. 45 PNGs + .import companions copied to `art/sprites/characters/`. New `scenes/pawn/pawn_sprite_frames.gd` builds the SpriteFrames from a {idle, walk, dead} atlas trio. Code-level verification clean; visual MCP verification still pending (needs editor running). +- **Pattern recorded — "class_name lookups aren't reliable during cold-start parse; use preload() for sibling scripts".** First attempt referenced `PawnSpriteFrames.build()` in pawn.gd via the global class_name registry. Headless parse failed with "Identifier 'PawnSpriteFrames' not declared in the current scope" because pawn.gd was loaded before pawn_sprite_frames.gd registered its class_name globally. Fix: `const _PAWN_SPRITE_FRAMES = preload("res://scenes/pawn/pawn_sprite_frames.gd")` at the top of pawn.gd, then call `_PAWN_SPRITE_FRAMES.build(atlases)`. Class_name is fine for public API documentation; preload is the reliable runtime resolver. +- **Pattern recorded — "deferred-init data must be wired AFTER setup(), not in _ready()".** First sprite mount happened in `_ready()` and read `pawn_name` — but pawn_name is empty at _ready time (assigned later in setup()). All three pawns got atlas idx 1 (hash of empty string). Fix: moved sprite mount to `_mount_sprite()` called from setup() AND from_dict(), both of which assign pawn_name first. Idempotent (frees prior sprite). Same shape will recur for any future render that depends on per-instance saved state — always check whether the data the renderer reads is available at _ready vs after setup/from_dict. +- **Delegation report — pawn reskin Slice 1.** `quick-edit` (Haiku, 1 dispatch) handled the mechanical edits across pawn.gd (facing field, save/load, `_canonical_facing`, `_atlas_for_pawn`). `gdscript-refactor` (Sonnet, 1 dispatch) wrote the new `pawn_sprite_frames.gd` helper (~50 LOC) and wired the AnimatedSprite2D into Pawn._ready (z_index, anim switching, _facing_suffix). Opus handled the asset copy + headless --import, the two parse/runtime fixes (preload + setup-not-ready), the hash-distribution audit, and the commit. The two patterns above were caught on Opus during verification. + ## External references - **Forgejo repo:** https://git.rdx4.com/megaproxy/rimlike (private) diff --git a/scenes/ai/crafting_provider.gd b/scenes/ai/crafting_provider.gd index dcab18d..fc59865 100644 --- a/scenes/ai/crafting_provider.gd +++ b/scenes/ai/crafting_provider.gd @@ -69,14 +69,24 @@ func find_best_for(pawn) -> Job: _emit_bill_blocked(b.recipe.label, &"skill_too_low", wb) continue - # Confirm a qualifying ingredient exists on the floor. - var src = _find_ingredient_item(b.recipe.ingredient_type) - if src == null: - _emit_bill_blocked(b.recipe.label, &"missing_ingredient", wb) - continue + # If ingredient_count is 0, no ingredient is required; proceed directly. + # Otherwise, confirm a qualifying ingredient exists on the floor. + var src = null + if b.recipe.ingredient_count > 0: + src = _find_ingredient_item(b.recipe.ingredient_type) + if src == null: + _emit_bill_blocked(b.recipe.label, &"missing_ingredient", wb) + continue + + # Score: total Manhattan travel distance. + # If no ingredient (count==0), distance is just pawn → workbench. + # Otherwise, distance is pawn → ingredient → workbench. + var d: int + if b.recipe.ingredient_count > 0: + d = _manhattan(pawn.tile, src.tile) + _manhattan(src.tile, wb.tile) + else: + d = _manhattan(pawn.tile, wb.tile) - # Score: total Manhattan travel distance pawn → ingredient → workbench. - var d: int = _manhattan(pawn.tile, src.tile) + _manhattan(src.tile, wb.tile) if d < best_dist: best_dist = d best_wb = wb @@ -87,16 +97,22 @@ func find_best_for(pawn) -> Job: if best_wb == null: return null - # Re-resolve the source item in case multiple bills tied on the same item. - var src_item = _find_ingredient_item(best_bill.recipe.ingredient_type) - if src_item == null: - return null + var src_item = null + # If ingredient_count > 0, re-resolve the source item in case multiple bills tied on the same item. + if best_bill.recipe.ingredient_count > 0: + src_item = _find_ingredient_item(best_bill.recipe.ingredient_type) + if src_item == null: + return null var j := Job.new() j.label = "Craft %s at %s" % [best_bill.recipe.label, best_wb.get("label_text") if best_wb.get("label_text") != null else "workbench"] j.target_node = best_wb - j.toils.append(Toil.walk_to(src_item.tile)) - j.toils.append(Toil.pickup()) + + # Only add ingredient-haul toils if ingredient is required. + if best_bill.recipe.ingredient_count > 0: + j.toils.append(Toil.walk_to(src_item.tile)) + j.toils.append(Toil.pickup()) + j.toils.append(Toil.walk_to(best_wb.tile)) j.toils.append(Toil.craft_at(best_wb.get_path(), best_bill_index)) return j diff --git a/scenes/ai/recipe.gd b/scenes/ai/recipe.gd index 8571b38..5f7ebc8 100644 --- a/scenes/ai/recipe.gd +++ b/scenes/ai/recipe.gd @@ -19,6 +19,9 @@ var id: StringName = &"" ## Item type consumed by this recipe (single-ingredient for Phase 6). var ingredient_type: StringName = &"" +## Count of ingredient_type required by this recipe. 0 = no ingredient (work only). +var ingredient_count: int = 0 + ## Phase 14 — optional secondary ingredient. Empty string = no secondary. ## CraftingProvider Phase 14 follow-up: enforce pickup of ingredient2 before ## assigning a pawn to this bill (currently stub — only ingredient_type enforced). @@ -46,10 +49,20 @@ var label: String = "" # ── save / load ─────────────────────────────────────────────────────────────── +## Player-visible display name for this recipe. Used by the workbench bill +## editor's recipe-picker and the bill list. Falls back to `id` if `label` +## is empty (shouldn't happen for catalog recipes, but defensive). +func display_name() -> String: + if label.is_empty(): + return str(id) + return label + + func to_dict() -> Dictionary: return { "id": String(id), "ingredient_type": String(ingredient_type), + "ingredient_count": ingredient_count, "ingredient2_type": String(ingredient2_type), "ingredient2_count": ingredient2_count, "output_type": String(output_type), @@ -64,6 +77,7 @@ static func from_dict(d: Dictionary) -> Recipe: var r := Recipe.new() r.id = StringName(d.get("id", "")) r.ingredient_type = StringName(d.get("ingredient_type", "")) + r.ingredient_count = int(d.get("ingredient_count", 0)) r.ingredient2_type = StringName(d.get("ingredient2_type", "")) r.ingredient2_count = int(d.get("ingredient2_count", 0)) r.output_type = StringName(d.get("output_type", "")) diff --git a/scenes/ai/recipe_catalog.gd b/scenes/ai/recipe_catalog.gd index 0645c3a..24d84e3 100644 --- a/scenes/ai/recipe_catalog.gd +++ b/scenes/ai/recipe_catalog.gd @@ -109,3 +109,33 @@ static func cremate_corpse() -> Recipe: r.required_skill = &"manual_labor" r.skill_threshold = 0 return r + + +## Quarry workbench — no input, produces 1 stone per work cycle. Used by the +## QuarryWorkbench placed on a BigRockNode for renewable stone supply. +static func quarry_stone() -> Recipe: + var r := Recipe.new() + r.id = &"quarry_stone" + r.label = "Quarry stone" + r.ingredient_type = &"" # no input + r.ingredient_count = 0 + r.output_type = Item.TYPE_STONE + r.work_ticks = 300 + r.required_skill = &"manual_labor" + r.skill_threshold = 0 + return r + + +## Returns one fresh instance of every recipe in the catalog. Used by UI +## recipe-pickers to enumerate available bills; callers filter by +## `recipe.required_skill` against the workbench's `accepted_skill`. +static func all() -> Array[Recipe]: + return [ + plank(), + stone_block(), + flour(), + bread(), + meal_from_vegetables(), + cremate_corpse(), + quarry_stone(), + ] diff --git a/scenes/entities/big_rock_node.gd b/scenes/entities/big_rock_node.gd new file mode 100644 index 0000000..3ff6da3 --- /dev/null +++ b/scenes/entities/big_rock_node.gd @@ -0,0 +1,140 @@ +class_name BigRockNode extends Node2D +## Permanent stone outcrop — a 2×2 immovable boulder that never depletes. +## +## BigRockNode marks its footprint impassable in the pathfinder and registers +## itself with World so the designation system can locate it. A QuarryWorkbench +## can be built on one of its tiles; external code sets is_quarry_site = true +## and quarry_workbench once construction completes. +## +## Draw convention: position is stamped at the TOP-LEFT pixel corner of the +## 2×2 footprint (tile * TILE_SIZE_PX). The visual centre of the 32×32 area +## sits at local (16, 16), so all _draw_* helpers are centred there. +## +## Save/load: to_dict / from_dict round-trip tile + is_quarry_site. +## The quarry_workbench reference is an entity pointer reconstructed by the +## save layer (SaveSystem wires it after both entities are spawned). + +const TILE_SIZE_PX: int = 16 + +## Top-left tile of the 2×2 footprint. +var tile: Vector2i = Vector2i.ZERO + +## Footprint size in tiles (always 2×2; declared as a var so external code can +## read it uniformly without special-casing BigRockNode vs hypothetical future nodes). +var footprint: Vector2i = Vector2i(2, 2) + +## True once a Quarry workbench has been completed on this outcrop. +## Flipped by external code (designation / world.gd) — this file only declares it. +var is_quarry_site: bool = false + +## The QuarryWorkbench that sits on this node after construction. +## null until assigned by the designation completion handler. +var quarry_workbench = null + + +# ── lifecycle ───────────────────────────────────────────────────────────────── + +func _ready() -> void: + # Position at top-left pixel corner so footprint_tiles() aligns with world coords. + position = Vector2(tile.x * TILE_SIZE_PX, tile.y * TILE_SIZE_PX) + # Block pathfinding on every tile in the footprint. + for t in footprint_tiles(): + if World.pathfinder != null: + World.pathfinder.set_cell_walkable(t, false) + World.register_big_rock_node(self) + queue_redraw() + + +func _exit_tree() -> void: + World.unregister_big_rock_node(self) + + +# ── public API ──────────────────────────────────────────────────────────────── + +## One-shot initialiser. Call after add_child() so _ready() has fired. +func setup(p_tile: Vector2i) -> void: + tile = p_tile + position = Vector2(tile.x * TILE_SIZE_PX, tile.y * TILE_SIZE_PX) + queue_redraw() + Audit.log("big_rock_node", "spawned at tile %s" % tile) + + +## Returns the four tiles covered by this node's 2×2 footprint. +## Used by designation, save/load, and inspection code. +func footprint_tiles() -> Array[Vector2i]: + return [ + tile, + tile + Vector2i(1, 0), + tile + Vector2i(0, 1), + tile + Vector2i(1, 1), + ] + + +## True when tile_to_check falls inside the 2×2 footprint. +func is_at(tile_to_check: Vector2i) -> bool: + return ( + tile_to_check.x >= tile.x and tile_to_check.x < tile.x + footprint.x + and tile_to_check.y >= tile.y and tile_to_check.y < tile.y + footprint.y + ) + + +# ── save / load ─────────────────────────────────────────────────────────────── + +func to_dict() -> Dictionary: + return { + "class_id": &"big_rock_node", + "tile_x": tile.x, + "tile_y": tile.y, + "is_quarry_site": is_quarry_site, + } + + +## Restore from a dict produced by to_dict(). quarry_workbench is reconnected +## by the save layer after both entities are spawned. +static func from_dict(d: Dictionary) -> Dictionary: + return { + "tile_x": int(d.get("tile_x", 0)), + "tile_y": int(d.get("tile_y", 0)), + "is_quarry_site": bool(d.get("is_quarry_site", false)), + } + + +# ── render ──────────────────────────────────────────────────────────────────── + +## Draw a procedural pile of grey rocks centred at local (16, 16) — +## the geometric centre of the 32×32 footprint area. +## Three layers create depth: large base blob → medium mid rock → small cap. +func _draw() -> void: + var cx: float = 16.0 + var cy: float = 16.0 + + # Colour palette — warm grey tones suggesting weathered granite. + var base_fill := Color(0.60, 0.58, 0.55) # large base ellipse + var mid_fill := Color(0.45, 0.44, 0.42) # medium middle rock + var top_fill := Color(0.55, 0.53, 0.50) # small perched cap + var outline_col := Color(0.20, 0.18, 0.16, 0.70) # subtle dark rim + + # Bottom: large flattened blob — widest at the base to read as a ground-hugging mass. + var base_w: float = 24.0 + var base_h: float = 16.0 + var base_rect := Rect2(Vector2(cx - base_w / 2.0, cy - base_h / 2.0 + 2.0), Vector2(base_w, base_h)) + draw_rect(base_rect, base_fill) + + # Dark outline arc around the base blob. + draw_rect(base_rect, outline_col, false, 1.0) + + # Middle: slightly elevated, narrower rock sitting on top of the base. + var mid_w: float = 16.0 + var mid_h: float = 12.0 + var mid_oy: float = -4.0 # shift up from centre + var mid_rect := Rect2(Vector2(cx - mid_w / 2.0, cy - mid_h / 2.0 + mid_oy), Vector2(mid_w, mid_h)) + draw_rect(mid_rect, mid_fill) + draw_rect(mid_rect, outline_col, false, 1.0) + + # Top: small cap perched on the very top. + var top_w: float = 8.0 + var top_h: float = 6.0 + var top_oy: float = -10.0 # above the mid rock + var top_rect := Rect2(Vector2(cx - top_w / 2.0, cy - top_h / 2.0 + top_oy), Vector2(top_w, top_h)) + draw_rect(top_rect, top_fill) + draw_rect(top_rect, outline_col, false, 1.0) diff --git a/scenes/entities/big_rock_node.gd.uid b/scenes/entities/big_rock_node.gd.uid new file mode 100644 index 0000000..af363c5 --- /dev/null +++ b/scenes/entities/big_rock_node.gd.uid @@ -0,0 +1 @@ +uid://bhn0lknhgn1od diff --git a/scenes/entities/big_rock_node.tscn b/scenes/entities/big_rock_node.tscn new file mode 100644 index 0000000..f93f19f --- /dev/null +++ b/scenes/entities/big_rock_node.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://big_rock_node_entity"] + +[ext_resource type="Script" path="res://scenes/entities/big_rock_node.gd" id="1_big_rock_node"] + +[node name="BigRockNode" type="Node2D"] +script = ExtResource("1_big_rock_node") diff --git a/scenes/entities/crop.gd b/scenes/entities/crop.gd index f47ed74..494d585 100644 --- a/scenes/entities/crop.gd +++ b/scenes/entities/crop.gd @@ -109,6 +109,11 @@ func on_harvest_tick() -> void: var it: Item = ITEM_SCENE.instantiate() get_parent().add_child(it) it.setup(item_type, 1, tile) + # Carry the crop_kind through as a visual subtype so wheat / corn / + # potato / strawberry each render distinctly, while item_type stays + # generic (TYPE_GRAIN / TYPE_VEGETABLE) for stockpile filter purposes. + it.subtype = crop_kind + it.queue_redraw() stage = Stage.TILLED stage_progress = 0 _refresh_sprite_region() diff --git a/scenes/entities/item.gd b/scenes/entities/item.gd index e54cdce..465d151 100644 --- a/scenes/entities/item.gd +++ b/scenes/entities/item.gd @@ -84,6 +84,13 @@ enum Quality { SHODDY, NORMAL, EXCELLENT, MASTERWORK, LEGENDARY } @export var stack_size: int = 1 @export var quality: Quality = Quality.NORMAL +## Visual subtype within the broader item_type. Lets multiple harvested produce +## share one storage-filter category (TYPE_GRAIN matches both wheat AND corn) +## while still rendering distinctly. Empty string = use item_type's default +## shape. Set by the spawning code (Crop.on_harvest_tick assigns "wheat"/"corn" +## /"potato"/"strawberry" based on crop_kind). +@export var subtype: StringName = &"" + var tile: Vector2i = Vector2i.ZERO ## When true the on-floor visual is suppressed; the carrying pawn renders the @@ -154,6 +161,7 @@ func to_dict() -> Dictionary: return { "class_id": &"item", "type": String(item_type), + "subtype": String(subtype), "stack_size": stack_size, "tile_x": tile.x, "tile_y": tile.y, @@ -167,6 +175,7 @@ func to_dict() -> Dictionary: static func from_dict(d: Dictionary) -> Dictionary: return { "type": StringName(d.get("type", "wood")), + "subtype": StringName(d.get("subtype", "")), "stack_size": int(d.get("stack_size", 1)), "tile_x": int(d.get("tile_x", 0)), "tile_y": int(d.get("tile_y", 0)), @@ -176,20 +185,288 @@ static func from_dict(d: Dictionary) -> Dictionary: # ── render ──────────────────────────────────────────────────────────────────── +## Procedural shape per item type, drawn onto an arbitrary CanvasItem. All +## shapes draw inside a -6..+6 box so the quality border (half=6) wraps the +## result, and the carry indicator (Pawn._draw) can scale the same shapes down +## via draw_set_transform. Returns true if the type was handled; false sends +## the caller to its hue-hashed fallback. +## +## Shape language is loose silhouette + a category-appropriate colour: bread = +## brown loaf, grain = wheat-amber stalks, vegetable = green disc, meal = bowl, +## etc. Atlas-backed types (wood / stone / plank / iron_ore / gold) also have +## a shape here so the carry indicator works for them — the on-floor visual +## still uses the bundle icon via the Sprite2D child path. +static func draw_item_shape(target: CanvasItem, t: StringName, sub: StringName = &"") -> bool: + var dark := Color(0.10, 0.07, 0.05, 0.85) # shared outline + # Subtype dispatch — lets wheat / corn / potato / strawberry render + # distinctly even though they share TYPE_GRAIN or TYPE_VEGETABLE for + # storage-filter purposes. Falls through to the type dispatch if subtype + # is unrecognised so existing items don't blank out. + match sub: + &"wheat": + # Yellow stalks with grain heads — same as default TYPE_GRAIN shape. + var stalk := Color(0.85, 0.70, 0.20) + var tie := Color(0.55, 0.35, 0.10) + target.draw_rect(Rect2(-4.0, -6.0, 1.5, 12.0), stalk) + target.draw_rect(Rect2(-0.75, -6.0, 1.5, 12.0), stalk) + target.draw_rect(Rect2(2.5, -6.0, 1.5, 12.0), stalk) + target.draw_circle(Vector2(-3.2, -5.0), 1.2, Color(0.95, 0.78, 0.25)) + target.draw_circle(Vector2(0.0, -5.5), 1.2, Color(0.95, 0.78, 0.25)) + target.draw_circle(Vector2(3.2, -5.0), 1.2, Color(0.95, 0.78, 0.25)) + target.draw_rect(Rect2(-5.0, 0.0, 10.0, 2.0), tie) + return true + &"corn": + # Corn cob — yellow body with rows of kernel dots + green husk. + var husk := Color(0.30, 0.65, 0.20) + var cob := Color(0.95, 0.82, 0.30) + var kernel_dark := Color(0.70, 0.55, 0.15) + # Husk leaves splayed out at the top. + var husk_l: PackedVector2Array = PackedVector2Array([Vector2(-4.0, -5.0), Vector2(-2.0, -2.0), Vector2(-5.0, -1.0)]) + var husk_r: PackedVector2Array = PackedVector2Array([Vector2(4.0, -5.0), Vector2(2.0, -2.0), Vector2(5.0, -1.0)]) + target.draw_colored_polygon(husk_l, husk) + target.draw_colored_polygon(husk_r, husk) + # Cob body — vertical oval/rect with rounded corners. + target.draw_rect(Rect2(-3.0, -4.0, 6.0, 10.0), cob) + target.draw_circle(Vector2(0.0, 5.5), 3.0, cob) + # Kernel dots — 2 columns × 3 rows for texture. + for y in [-2.0, 0.0, 2.0, 4.0]: + target.draw_rect(Rect2(-2.0, y, 1.5, 1.5), kernel_dark) + target.draw_rect(Rect2(0.5, y, 1.5, 1.5), kernel_dark) + # Cob outline (open at top because of husk). + target.draw_arc(Vector2(0.0, 5.5), 3.0, 0.0, PI, 12, dark, 1.0) + return true + &"potato": + # Two brown lumps with sprout-eye dots — pile of potatoes. + var skin := Color(0.62, 0.45, 0.25) + var skin_dark := Color(0.42, 0.28, 0.15) + var eye := Color(0.25, 0.15, 0.05) + # Two overlapping potato ovals. + target.draw_circle(Vector2(-2.0, 1.0), 4.0, skin) + target.draw_circle(Vector2(2.5, -0.5), 3.5, skin) + # Outline. + target.draw_arc(Vector2(-2.0, 1.0), 4.0, 0.0, TAU, 16, skin_dark, 1.0) + target.draw_arc(Vector2(2.5, -0.5), 3.5, 0.0, TAU, 12, skin_dark, 1.0) + # Eye dots. + target.draw_circle(Vector2(-3.0, 0.0), 0.7, eye) + target.draw_circle(Vector2(-0.5, 2.0), 0.6, eye) + target.draw_circle(Vector2(3.5, -1.5), 0.7, eye) + return true + &"strawberry": + # Classic red strawberry — heart-shape body with green calyx on top + # and tiny yellow seed dots scattered on the surface. + var berry := Color(0.88, 0.18, 0.20) + var berry_dark := Color(0.62, 0.10, 0.10) + var leaf := Color(0.25, 0.60, 0.20) + var seed := Color(0.95, 0.85, 0.30) + # Body — wider top tapering to a point at the bottom. + var body: PackedVector2Array = PackedVector2Array([ + Vector2(-5.0, -1.0), Vector2(-3.5, -3.0), Vector2(3.5, -3.0), + Vector2(5.0, -1.0), Vector2(3.0, 4.0), Vector2(0.0, 6.0), Vector2(-3.0, 4.0), + ]) + target.draw_colored_polygon(body, berry) + # Body outline. + target.draw_polyline(body + PackedVector2Array([body[0]]), berry_dark, 1.0) + # Green calyx (leaves) on top. + target.draw_rect(Rect2(-3.0, -5.0, 6.0, 2.0), leaf) + var leaf_top: PackedVector2Array = PackedVector2Array([Vector2(-2.0, -5.0), Vector2(0.0, -7.0), Vector2(2.0, -5.0)]) + target.draw_colored_polygon(leaf_top, leaf) + # Seed speckles. + target.draw_circle(Vector2(-2.0, 1.0), 0.5, seed) + target.draw_circle(Vector2(2.0, 1.0), 0.5, seed) + target.draw_circle(Vector2(0.0, 3.0), 0.5, seed) + return true + # Fall through to type dispatch. + match t: + TYPE_BREAD: + var crust := Color(0.62, 0.40, 0.18) + var glaze := Color(0.82, 0.58, 0.30) + target.draw_rect(Rect2(-6.0, -1.0, 12.0, 5.0), crust) + target.draw_rect(Rect2(-5.0, -5.0, 10.0, 4.0), glaze) + target.draw_line(Vector2(-3.0, -4.0), Vector2(0.0, -2.0), dark, 1.0) + target.draw_line(Vector2(0.0, -4.0), Vector2(3.0, -2.0), dark, 1.0) + target.draw_rect(Rect2(-6.0, -5.0, 12.0, 9.0), dark, false, 1.0) + return true + TYPE_GRAIN: + var stalk := Color(0.85, 0.70, 0.20) + var tie := Color(0.55, 0.35, 0.10) + target.draw_rect(Rect2(-4.0, -6.0, 1.5, 12.0), stalk) + target.draw_rect(Rect2(-0.75, -6.0, 1.5, 12.0), stalk) + target.draw_rect(Rect2(2.5, -6.0, 1.5, 12.0), stalk) + target.draw_circle(Vector2(-3.2, -5.0), 1.2, Color(0.95, 0.78, 0.25)) + target.draw_circle(Vector2(0.0, -5.5), 1.2, Color(0.95, 0.78, 0.25)) + target.draw_circle(Vector2(3.2, -5.0), 1.2, Color(0.95, 0.78, 0.25)) + target.draw_rect(Rect2(-5.0, 0.0, 10.0, 2.0), tie) + return true + TYPE_FLOUR: + var sack := Color(0.93, 0.90, 0.78) + var shadow := Color(0.78, 0.74, 0.60) + target.draw_rect(Rect2(-5.0, -3.0, 10.0, 8.0), sack) + target.draw_rect(Rect2(3.0, -3.0, 2.0, 8.0), shadow) + target.draw_rect(Rect2(-4.0, -5.0, 8.0, 2.0), Color(0.45, 0.30, 0.10)) + target.draw_circle(Vector2(0.0, -5.5), 1.0, sack) + target.draw_rect(Rect2(-5.0, -6.0, 10.0, 11.0), dark, false, 1.0) + return true + TYPE_VEGETABLE: + var leaf := Color(0.25, 0.65, 0.20) + var root := Color(0.92, 0.88, 0.70) + target.draw_circle(Vector2(0.0, 1.0), 5.0, root) + target.draw_arc(Vector2(0.0, 1.0), 5.0, 0.0, TAU, 16, dark, 1.0) + target.draw_rect(Rect2(-3.0, -5.0, 2.0, 3.0), leaf) + target.draw_rect(Rect2(-1.0, -6.0, 2.0, 4.0), leaf) + target.draw_rect(Rect2( 1.0, -5.0, 2.0, 3.0), leaf) + return true + TYPE_MEAL: + var bowl := Color(0.50, 0.30, 0.12) + var bowl_rim := Color(0.30, 0.18, 0.08) + var food := Color(0.85, 0.55, 0.20) + target.draw_circle(Vector2(0.0, 1.0), 6.0, bowl) + target.draw_arc(Vector2(0.0, 1.0), 6.0, 0.0, PI, 16, bowl_rim, 1.0) + target.draw_line(Vector2(-6.0, 1.0), Vector2(6.0, 1.0), bowl_rim, 1.0) + target.draw_circle(Vector2(0.0, 0.0), 3.5, food) + target.draw_line(Vector2(-2.0, -5.0), Vector2(-1.0, -3.0), Color(0.9, 0.9, 0.9, 0.7), 1.0) + target.draw_line(Vector2(2.0, -5.0), Vector2(1.0, -3.0), Color(0.9, 0.9, 0.9, 0.7), 1.0) + return true + TYPE_MEAT: + var meat := Color(0.78, 0.20, 0.20) + var fat := Color(0.95, 0.85, 0.70) + target.draw_rect(Rect2(-5.0, -3.0, 10.0, 7.0), meat) + target.draw_line(Vector2(-5.0, 0.0), Vector2(5.0, 0.5), fat, 1.0) + target.draw_rect(Rect2(-5.0, -3.0, 10.0, 7.0), dark, false, 1.0) + return true + TYPE_CLOTH: + var cloth := Color(0.50, 0.65, 0.85) + var pleat := Color(0.32, 0.42, 0.58) + target.draw_rect(Rect2(-5.0, -4.0, 10.0, 8.0), cloth) + target.draw_line(Vector2(-5.0, -1.5), Vector2(5.0, -1.5), pleat, 1.0) + target.draw_line(Vector2(-5.0, 1.5), Vector2(5.0, 1.5), pleat, 1.0) + target.draw_rect(Rect2(-5.0, -4.0, 10.0, 8.0), dark, false, 1.0) + return true + TYPE_MEDICINE: + var phial := Color(0.95, 0.95, 0.95) + var cross := Color(0.80, 0.15, 0.15) + target.draw_rect(Rect2(-4.0, -5.0, 8.0, 10.0), phial) + target.draw_rect(Rect2(-1.0, -3.0, 2.0, 6.0), cross) + target.draw_rect(Rect2(-3.0, -1.0, 6.0, 2.0), cross) + target.draw_rect(Rect2(-4.0, -5.0, 8.0, 10.0), dark, false, 1.0) + return true + TYPE_TOOL: + var handle := Color(0.50, 0.30, 0.12) + var head := Color(0.45, 0.45, 0.48) + target.draw_rect(Rect2(-1.0, -2.0, 2.0, 8.0), handle) + target.draw_rect(Rect2(-5.0, -5.0, 10.0, 4.0), head) + target.draw_rect(Rect2(-5.0, -5.0, 10.0, 4.0), dark, false, 1.0) + return true + TYPE_WEAPON: + var blade := Color(0.78, 0.80, 0.85) + var guard := Color(0.45, 0.30, 0.10) + var pts: PackedVector2Array = PackedVector2Array([Vector2(0.0, -6.0), Vector2(2.5, 1.0), Vector2(-2.5, 1.0)]) + target.draw_colored_polygon(pts, blade) + target.draw_rect(Rect2(-4.0, 1.0, 8.0, 2.0), guard) + target.draw_rect(Rect2(-1.0, 3.0, 2.0, 3.0), guard) + return true + TYPE_ARMOR: + var steel := Color(0.65, 0.65, 0.70) + var visor := Color(0.20, 0.20, 0.25) + target.draw_circle(Vector2(0.0, 0.0), 5.5, steel) + target.draw_rect(Rect2(-1.0, -2.0, 2.0, 4.0), visor) + target.draw_rect(Rect2(-6.0, 4.0, 12.0, 2.0), steel) + target.draw_arc(Vector2(0.0, 0.0), 5.5, 0.0, TAU, 16, dark, 1.0) + return true + TYPE_STONE_BLOCK: + var stone := Color(0.62, 0.60, 0.58) + var stone_hi := Color(0.78, 0.76, 0.72) + target.draw_rect(Rect2(-6.0, -4.0, 12.0, 8.0), stone) + target.draw_rect(Rect2(-6.0, -4.0, 12.0, 2.0), stone_hi) + target.draw_line(Vector2(-6.0, 0.0), Vector2(6.0, 0.0), Color(0.42, 0.40, 0.38), 1.0) + target.draw_rect(Rect2(-6.0, -4.0, 12.0, 8.0), dark, false, 1.0) + return true + TYPE_COPPER_ORE: + var copper := Color(0.65, 0.35, 0.18) + var hi := Color(0.92, 0.55, 0.20) + target.draw_circle(Vector2(-2.0, 1.0), 3.5, copper) + target.draw_circle(Vector2(2.0, -1.0), 2.8, copper) + target.draw_circle(Vector2(-2.0, 1.0), 1.5, hi) + target.draw_circle(Vector2(2.0, -1.0), 1.0, hi) + return true + TYPE_SILVER: + var silver := Color(0.78, 0.80, 0.85) + var hi := Color(0.98, 0.98, 1.00) + target.draw_circle(Vector2(-2.0, 1.0), 3.5, silver) + target.draw_circle(Vector2(2.0, -1.0), 2.8, silver) + target.draw_circle(Vector2(-2.0, 0.5), 1.2, hi) + return true + TYPE_ASH: + var ash := Color(0.55, 0.55, 0.55) + var ash_hi := Color(0.78, 0.78, 0.78) + var pts: PackedVector2Array = PackedVector2Array([Vector2(-6.0, 4.0), Vector2(6.0, 4.0), Vector2(0.0, -2.0)]) + target.draw_colored_polygon(pts, ash) + target.draw_line(Vector2(-3.0, 1.0), Vector2(3.0, 1.0), ash_hi, 1.0) + target.draw_line(Vector2(-1.0, -3.0), Vector2(0.0, -5.0), Color(0.85, 0.85, 0.85, 0.6), 1.0) + target.draw_line(Vector2(1.5, -3.0), Vector2(2.5, -5.0), Color(0.85, 0.85, 0.85, 0.6), 1.0) + return true + # Atlas-backed types — the on-floor visual is the bundle icon (Sprite2D + # child), but the carry indicator needs a simple shape since pawns can't + # draw an AtlasTexture inline. Shapes below approximate the atlas look. + TYPE_WOOD: + var wood := Color(0.55, 0.35, 0.18) + var wood_hi := Color(0.75, 0.50, 0.25) + target.draw_rect(Rect2(-6.0, -3.0, 12.0, 6.0), wood) + target.draw_line(Vector2(-6.0, -1.0), Vector2(6.0, -1.0), wood_hi, 1.0) + target.draw_line(Vector2(-6.0, 1.0), Vector2(6.0, 1.0), Color(0.40, 0.25, 0.10), 1.0) + target.draw_rect(Rect2(-6.0, -3.0, 12.0, 6.0), dark, false, 1.0) + return true + TYPE_PLANK: + var plank := Color(0.80, 0.60, 0.35) + var plank_grain := Color(0.55, 0.38, 0.20) + target.draw_rect(Rect2(-6.0, -3.0, 12.0, 6.0), plank) + target.draw_line(Vector2(-5.0, -1.0), Vector2(5.0, -1.0), plank_grain, 1.0) + target.draw_line(Vector2(-5.0, 1.0), Vector2(5.0, 1.0), plank_grain, 1.0) + target.draw_rect(Rect2(-6.0, -3.0, 12.0, 6.0), dark, false, 1.0) + return true + TYPE_STONE: + var stone := Color(0.60, 0.58, 0.55) + var stone_hi := Color(0.78, 0.76, 0.72) + target.draw_circle(Vector2(-1.5, 1.0), 4.0, stone) + target.draw_circle(Vector2(2.0, -0.5), 3.0, stone) + target.draw_circle(Vector2(-1.5, 1.0), 1.5, stone_hi) + return true + TYPE_IRON_ORE: + var ore := Color(0.42, 0.42, 0.50) + var ore_hi := Color(0.60, 0.62, 0.72) + target.draw_circle(Vector2(-1.5, 1.0), 4.0, ore) + target.draw_circle(Vector2(2.0, -0.5), 3.0, ore) + target.draw_circle(Vector2(2.0, -0.5), 1.2, ore_hi) + return true + TYPE_GOLD: + var gold := Color(0.92, 0.78, 0.20) + var gold_hi := Color(1.00, 0.95, 0.55) + target.draw_circle(Vector2(-1.5, 1.0), 4.0, gold) + target.draw_circle(Vector2(2.0, -0.5), 3.0, gold) + target.draw_circle(Vector2(-1.5, 0.5), 1.5, gold_hi) + return true + _: + return false + + func _draw() -> void: - # Two render paths: types in _ITEM_SPRITES are painted by the Sprite2D child - # (built in setup()) — _draw() then only adds the quality border + stack - # count badge on top. Other types still use the procedural hue square so - # stockpile filtering remains visually unique while we expand the sprite set. + # Three render paths: + # 1. Atlas sprite (_ITEM_SPRITES): a Sprite2D child paints the icon; + # _draw() adds quality border + stack badge on top. + # 2. Procedural shape (_draw_item_shape returns true): bread/grain/ + # vegetable/meal/flour/meat/cloth/medicine/etc. get a recognisable + # silhouette in their category colour. + # 3. Unknown fallback: hue-hashed coloured square. Should be unreachable + # once every ALL_TYPES entry is handled above — kept for safety. var has_sprite: bool = _ITEM_SPRITES.has(item_type) var half: int = 6 if not has_sprite else 8 # border hugs the 16×16 sprite var square := Rect2(Vector2(-half, -half), Vector2(half * 2, half * 2)) if not has_sprite: - var hue := float(item_type.hash() % 360) / 360.0 - var fill := Color.from_hsv(hue, 0.6, 0.85) - draw_rect(square, fill) - draw_rect(square, Color(0.0, 0.0, 0.0, 0.75), false, 1.0) + if not Item.draw_item_shape(self, item_type, subtype): + var hue := float(item_type.hash() % 360) / 360.0 + var fill := Color.from_hsv(hue, 0.6, 0.85) + draw_rect(square, fill) + draw_rect(square, Color(0.0, 0.0, 0.0, 0.75), false, 1.0) # Quality border — drawn over the dark outline (or sprite), colour per quality tier. # NORMAL has no extra border. diff --git a/scenes/entities/quarry_workbench.gd b/scenes/entities/quarry_workbench.gd new file mode 100644 index 0000000..91ef0f7 --- /dev/null +++ b/scenes/entities/quarry_workbench.gd @@ -0,0 +1,80 @@ +class_name QuarryWorkbench extends Workbench +## Quarry workbench — built on a BigRockNode tile, produces stone indefinitely. +## +## Subclasses Workbench to reuse the build + bill machinery. A default +## FOREVER bill with the quarry_stone recipe is auto-added in _ready() so the +## workbench begins dripping stone as soon as construction completes. +## +## No-ingredient recipe: quarry_stone uses ingredient_count = 0. CraftingProvider +## must handle the zero-ingredient path before this workbench produces anything +## (see plan_quarry_bigrock.md Step 1 — handled separately). +## +## Variant appearance: overrides _draw() unconditionally so that the quarry +## silhouette is always shown regardless of label_text (mirrors CremationPyre). +## accepted_skill = manual_labor — any labourer can work the quarry. +## +## World registration: inherited from Workbench._ready / _exit_tree. + + +func _init() -> void: + label_text = "Quarry" + accepted_skill = &"manual_labor" + + +func _ready() -> void: + label_text = "Quarry" + accepted_skill = &"manual_labor" + super._ready() + # Auto-populate a default FOREVER bill so the bench is immediately usable + # once construction completes. Mirrors CremationPyre's bill wiring. + var b := Bill.new() + b.recipe = RecipeCatalog.quarry_stone() + b.mode = Bill.Mode.FOREVER + bills.append(b) + Audit.log("quarry", "QuarryWorkbench ready at %s — bill added" % tile) + + +# ── render ──────────────────────────────────────────────────────────────────── + +## Override Workbench._draw() to always dispatch to _draw_quarry(). +## Alpha is 0.4 while under construction, 1.0 once complete (same as all benches). +func _draw() -> void: + var alpha: float = 1.0 if is_completed() else 0.4 + _draw_quarry(alpha) + + +## Procedural quarry appearance: a wooden frame base with a large stone block +## on top, chisel-mark details, and a small pile of freshly cut stones to the +## right side. Local coords follow Workbench convention — (0, 0) is the +## bottom-right of the tile; the bench draws UP into negative-y space. +func _draw_quarry(alpha: float) -> void: + var frame_top := Color(0.55, 0.36, 0.18, alpha) # light wood top face + var frame_front := Color(0.42, 0.26, 0.12, alpha) # darker wood front + var frame_edge := Color(0.25, 0.14, 0.06, alpha) # wood grain seam + var stone_top := Color(0.60, 0.58, 0.55, alpha) # stone block top face + var stone_front := Color(0.44, 0.43, 0.41, alpha) # stone block front + var chisel_mark := Color(0.28, 0.27, 0.25, alpha) # cut line on stone + var pile_light := Color(0.62, 0.61, 0.59, alpha) # loose stone pile + var pile_dark := Color(0.38, 0.37, 0.35, alpha) # pile shadow + var outline := Color(0.20, 0.18, 0.16, 0.70 * alpha) + + # Wooden frame base — front face + top lip. + draw_rect(Rect2(Vector2(-8.0, -7.0), Vector2(16.0, 7.0)), frame_front) + draw_rect(Rect2(Vector2(-8.0, -10.0), Vector2(16.0, 3.0)), frame_top) + draw_line(Vector2(-8.0, -7.0), Vector2(8.0, -7.0), frame_edge, 1.0) + + # Stone block sitting on the frame — occupies most of the upper half. + draw_rect(Rect2(Vector2(-7.0, -15.0), Vector2(11.0, 5.0)), stone_top) + draw_rect(Rect2(Vector2(-7.0, -10.5), Vector2(11.0, 0.5)), stone_front) # thin visible front edge + + # Chisel marks on the stone top — short diagonal cuts suggesting work in progress. + draw_line(Vector2(-4.0, -13.0), Vector2(-2.0, -11.5), chisel_mark, 1.0) + draw_line(Vector2(-1.0, -14.0), Vector2( 1.0, -12.5), chisel_mark, 1.0) + draw_line(Vector2( 2.0, -13.0), Vector2( 4.0, -11.5), chisel_mark, 1.0) + + # Small pile of cut stones on the right — two rounded rects stacked. + draw_rect(Rect2(Vector2(5.0, -9.0), Vector2(4.0, 3.0)), pile_dark) + draw_rect(Rect2(Vector2(5.0, -11.0), Vector2(3.0, 2.0)), pile_light) + + # Tile outline. + draw_rect(Rect2(Vector2(-8.0, -16.0), Vector2(16.0, 16.0)), outline, false, 1.0) diff --git a/scenes/entities/quarry_workbench.gd.uid b/scenes/entities/quarry_workbench.gd.uid new file mode 100644 index 0000000..a3ef967 --- /dev/null +++ b/scenes/entities/quarry_workbench.gd.uid @@ -0,0 +1 @@ +uid://bpbcwr4dh1736 diff --git a/scenes/entities/quarry_workbench.tscn b/scenes/entities/quarry_workbench.tscn new file mode 100644 index 0000000..ec28b7a --- /dev/null +++ b/scenes/entities/quarry_workbench.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://quarry_workbench_entity"] + +[ext_resource type="Script" path="res://scenes/entities/quarry_workbench.gd" id="1_quarry"] + +[node name="QuarryWorkbench" type="Node2D"] +script = ExtResource("1_quarry") diff --git a/scenes/entities/tree.gd b/scenes/entities/tree.gd index 52177bf..8d5919f 100644 --- a/scenes/entities/tree.gd +++ b/scenes/entities/tree.gd @@ -1,10 +1,15 @@ ## Tree entity — choppable by a pawn with a Chop job. Drops wood Item nodes -## when felled. +## when felled. Trees also grow through four stages (Sapling → Young → Growing +## → Mature); only Mature trees can be chopped. ## ## Chopping model (docs/implementation.md Phase 4): ## A ChopProvider creates a Job whose INTERACT toil calls on_chop_tick() once ## per sim tick via JobRunner. After CHOP_TICKS ticks the tree is felled. ## +## Growth model: on_sim_tick() is called once per sim tick by world.gd's sweep. +## After STAGE_TICKS ticks at each sub-mature stage the tree advances one stage +## and _refresh_sprite() updates the visual. +## ## World registration (World.register_tree / World.unregister_tree) is called ## here but the methods land in World during Opus integration. @@ -22,6 +27,19 @@ const WOOD_DROPS_ON_FELL: int = 3 ## Stack size per dropped Item (Phase 4 simplicity: 3 items of stack 1 each). const STACK_SIZE_PER_DROP: int = 1 +# ── growth stage constants ───────────────────────────────────────────────────── + +## Growth stage indices. +const STAGE_SAPLING: int = 0 +const STAGE_YOUNG: int = 1 +const STAGE_GROWING: int = 2 +const STAGE_MATURE: int = 3 + +## Sim ticks spent in each sub-mature stage before advancing. +## 5 in-game hours per stage at 20 Hz = 1200 ticks/hour × 5 = 6000 ticks. +## At default 5× speed that is ~5 min real time per stage, ~15 min seed → mature. +const STAGE_TICKS: int = 6000 + # ── state ───────────────────────────────────────────────────────────────────── var tile: Vector2i = Vector2i.ZERO @@ -31,48 +49,98 @@ var chop_progress: int = 0 ## ignores undesignated trees (Rimworld parity — pawns don't auto-chop). var chop_designated: bool = false +## Current growth stage (STAGE_SAPLING..STAGE_MATURE). Default MATURE so +## existing seed trees load at full size with no visual regression. +var growth_stage: int = STAGE_MATURE +## Ticks elapsed within the current sub-mature stage. +var growth_progress: int = 0 + +## Set to true on a ghost tree spawned by the plant_tree designation. The +## ConstructionProvider will issue a build job; on completion this flag clears +## and the tree grows normally. +var pending_plant: bool = false + # Preloaded scene for spawned wood items. const ITEM_SCENE: PackedScene = preload("res://scenes/entities/item.tscn") -## ElvGames Grasslands tree pack — 4 variants laid out left-to-right. -## Each variant is 64×80 px; trunk base sits in the bottom ~10 rows. We anchor -## the sprite center 32 px above tile origin so the trunk bottom lands at the -## tile's bottom edge and the canopy rises into the cells above. -const _TREE_TEX: Texture2D = preload("res://art/sprites/FG_Tree_Spring.png") +## ElvGames Grasslands tree pack — 4 silhouettes laid out left-to-right (64×80 +## each). Trunk base sits in the bottom ~10 rows; we anchor the sprite centre +## 32 px above tile origin so the trunk bottom lands at the tile's bottom edge +## and the canopy rises into the cells above. +## +## Three season palettes (Spring / Summer / Fall) give 12 visual variants from +## the same silhouette set. Winter is omitted — snowy trees look out of place +## in the current biome. When a season-cycle system lands later, swap the +## active texture by season globally instead of per-tree. +const _TREE_TEXES: Array[Texture2D] = [ + preload("res://art/sprites/FG_Tree_Spring.png"), + preload("res://art/sprites/FG_Tree_Summer.png"), + preload("res://art/sprites/FG_Tree_Fall.png"), +] const _TREE_VARIANT_W: int = 64 const _TREE_VARIANT_H: int = 80 -const _TREE_VARIANT_COUNT: int = 4 +const _TREE_SILHOUETTES: int = 4 # silhouettes per atlas (columns) + +## Growth-stage atlas — 128×32, 8 columns × 1 row of 16×32 cells, left-to-right +## progressively larger trees. Used for sub-mature stages so the player sees +## a proper sapling → small tree silhouette change instead of a scaled-down +## mature canopy. Stage MATURE keeps using _TREE_TEXES above (the full 64×80 +## canopy). +const _STAGE_TEX: Texture2D = preload("res://art/sprites/FG_Tree_Stages.png") +const _STAGE_CELL_W: int = 16 +const _STAGE_CELL_H: int = 32 +## Atlas column to use per growth_stage. Stage 3 (MATURE) is unused here. +const _STAGE_COLS: Array[int] = [0, 1, 3, -1] # ── lifecycle ───────────────────────────────────────────────────────────────── func _ready() -> void: position = _tile_to_world(tile) - _build_sprite() + _refresh_sprite() # Y-sort so the canopy draws behind walls/pawns that are visually south of # the trunk base. Position.y is the trunk-base row. y_sort_enabled = true World.register_tree(self) -## Adds a Sprite2D child painted with one of the 4 ElvGames tree variants. -## Variant chosen deterministically from the tile coord so the same tile always -## gets the same tree silhouette across boots and load/save. -func _build_sprite() -> void: +## Rebuild the Sprite2D child to match the current growth_stage. +## Stages 0-2 use _STAGE_TEX (a dedicated growth-stage atlas with progressively +## larger trees per cell). Stage 3 (Mature) uses the seasonal full-canopy +## atlases. Any existing "Sprite" child is removed first so re-calls don't stack. +func _refresh_sprite() -> void: + var old := get_node_or_null("Sprite") + if old != null: + old.queue_free() var sprite := Sprite2D.new() sprite.name = "Sprite" - sprite.texture = _TREE_TEX sprite.region_enabled = true - var variant: int = (tile.x * 31 + tile.y * 17) % _TREE_VARIANT_COUNT - sprite.region_rect = Rect2(variant * _TREE_VARIANT_W, 0, _TREE_VARIANT_W, _TREE_VARIANT_H) sprite.centered = true - # Lift the sprite up so its bottom edge sits at the tile's bottom row. - # Sprite center is at offset.y; sprite half-height is _TREE_VARIANT_H/2 = 40. - # We want bottom edge at +8 (tile bottom) → center at 8 - 40 = -32. - sprite.offset = Vector2(0, -32) - # Render behind pawns/items that are at higher z_index; trees live at z=0. sprite.z_index = 0 + if growth_stage < STAGE_MATURE: + # Sub-mature: 16×32 cell from _STAGE_TEX. Bottom edge at tile bottom + # (+8); sprite half-height 16 → centre offset y = 8 - 16 = -8. + var col: int = _STAGE_COLS[growth_stage] + sprite.texture = _STAGE_TEX + sprite.region_rect = Rect2(col * _STAGE_CELL_W, 0, _STAGE_CELL_W, _STAGE_CELL_H) + sprite.offset = Vector2(0, -8) + else: + # Mature: full 64×80 seasonal canopy. Bottom at +8 → centre at -32. + var hash_seed: int = tile.x * 31 + tile.y * 17 + var silhouette: int = hash_seed % _TREE_SILHOUETTES + var season: int = ((hash_seed / _TREE_SILHOUETTES) + tile.x * 7 + tile.y * 11) % _TREE_TEXES.size() + sprite.texture = _TREE_TEXES[season] + sprite.region_rect = Rect2(silhouette * _TREE_VARIANT_W, 0, _TREE_VARIANT_W, _TREE_VARIANT_H) + sprite.offset = Vector2(0, -32) add_child(sprite) + queue_redraw() + + +## Adds a Sprite2D child painted with one of the 12 ElvGames tree variants +## (4 silhouettes × 3 season palettes). Kept for call-site compatibility but +## now delegates to _refresh_sprite(). New code should call _refresh_sprite(). +func _build_sprite() -> void: + _refresh_sprite() func _exit_tree() -> void: @@ -82,17 +150,81 @@ func _exit_tree() -> void: # ── public API ──────────────────────────────────────────────────────────────── ## One-shot initialiser. Call after add_child() so _ready() already fired. -func setup(start_tile: Vector2i) -> void: +## start_stage defaults to STAGE_MATURE for backward compatibility. +func setup(start_tile: Vector2i, start_stage: int = STAGE_MATURE) -> void: tile = start_tile chop_progress = 0 + growth_stage = start_stage + growth_progress = 0 position = _tile_to_world(tile) + _refresh_sprite() queue_redraw() - Audit.log("tree", "spawned at %s" % tile) + Audit.log("tree", "spawned at %s (stage=%d)" % [tile, growth_stage]) -## True when the tree hasn't been fully chopped yet. +## True when the tree is mature, unChopped, and not a pending-plant ghost. +## Only mature trees yield wood — saplings/young/growing cannot be felled. func is_choppable() -> bool: - return chop_progress < CHOP_TICKS + return chop_progress < CHOP_TICKS and growth_stage == STAGE_MATURE and not pending_plant + + +## Called by world.gd's sim-tick sweep once per sim tick. +## Advances growth_progress and promotes the stage on threshold. No-op for +## mature trees and for pending-plant ghosts (ghost must be built first). +func on_sim_tick() -> void: + if growth_stage >= STAGE_MATURE or pending_plant: + return + growth_progress += 1 + if growth_progress >= STAGE_TICKS: + growth_stage += 1 + growth_progress = 0 + _refresh_sprite() + Audit.log("tree", "grew to stage %d at %s" % [growth_stage, tile]) + + +# ── pending-plant / build-site duck-type API ────────────────────────────────── +# ConstructionProvider requires is_buildable() / on_build_tick() / label() +# on every entity in World.build_queue. A pending_plant tree satisfies this +# interface so the provider can assign a pawn to "build" (plant) it. + +## Ticks of pawn work needed to complete a manual planting job. +const PLANT_TICKS: int = 30 + +## Progress counter within the planting job (0..PLANT_TICKS). +var _plant_progress: int = 0 + + +## True while the tree is a pending-plant ghost awaiting pawn work. +func is_buildable() -> bool: + return pending_plant and _plant_progress < PLANT_TICKS + + +## Human-readable label for the ConstructionProvider job entry. +func label() -> String: + return "Plant tree" + + +## Called by JobRunner's BUILD toil once per sim tick while a pawn works this +## site. After PLANT_TICKS the ghost becomes a real sapling. +func on_build_tick() -> void: + if not is_buildable(): + return + _plant_progress += 1 + queue_redraw() + if _plant_progress >= PLANT_TICKS: + _complete_plant() + + +## Finish the planting job: clear the pending flag, register as a real sapling, +## remove from World.build_queue, and clear the designation highlight. +func _complete_plant() -> void: + pending_plant = false + _plant_progress = 0 + World.unregister_build_site(self) + World.clear_designation_at(tile) + _refresh_sprite() + queue_redraw() + Audit.log("tree", "planted at %s — sapling begins growing" % tile) ## Called by the INTERACT toil in JobRunner once per sim tick while the pawn @@ -132,6 +264,10 @@ func to_dict() -> Dictionary: "tile_y": tile.y, "chop_progress": chop_progress, "chop_designated": chop_designated, + "growth_stage": growth_stage, + "growth_progress": growth_progress, + "pending_plant": pending_plant, + "plant_progress": _plant_progress, } @@ -141,15 +277,27 @@ static func from_dict(d: Dictionary) -> Dictionary: "tile_y": int(d.get("tile_y", 0)), "chop_progress": int(d.get("chop_progress", 0)), "chop_designated": bool(d.get("chop_designated", false)), + # Default to STAGE_MATURE (3) so pre-growth-system saves load as mature trees. + "growth_stage": int(d.get("growth_stage", 3)), + "growth_progress": int(d.get("growth_progress", 0)), + "pending_plant": bool(d.get("pending_plant", false)), + "plant_progress": int(d.get("plant_progress", 0)), } # ── render ──────────────────────────────────────────────────────────────────── func _draw() -> void: - # Canopy + trunk now come from the Sprite2D child (see _build_sprite). + # Ghost tint for pending-plant saplings — apply via modulate on the sprite + # child instead of drawing extra overlay shapes here. + if pending_plant and growth_stage == STAGE_SAPLING: + var s := get_node_or_null("Sprite") + if s != null: + s.modulate = Color(1, 1, 1, 0.55) + + # Mature / growing stages: canopy + trunk come from the Sprite2D child. # This _draw renders only the chop-progress notch overlaid on the trunk. - if chop_progress > 0: + if chop_progress > 0 and growth_stage == STAGE_MATURE: var ratio := float(chop_progress) / float(CHOP_TICKS) var notch_depth := ratio * 3.0 draw_line( diff --git a/scenes/entities/workbench.gd b/scenes/entities/workbench.gd index 51af0fd..a108c24 100644 --- a/scenes/entities/workbench.gd +++ b/scenes/entities/workbench.gd @@ -1,13 +1,15 @@ class_name Workbench extends Node2D ## Workbench entity — buildable structure where pawns craft items per bills. ## -## Rendered as a bottom-anchored sprite (Y-sorted) matching the 3/4-perspective -## convention from Wall/Door. Ghost state (40% alpha) while construction is -## in progress; solid once _completed. +## Rendered procedurally (Y-sorted) matching the 3/4-perspective convention +## from Wall/Door. Ghost state (40% alpha) while construction is in progress; +## solid once _completed. ## ## Variant appearance is driven by label_text: -## "Carpenter" → warm-brown wood bench with a vise detail -## "Smelter" → dark grey stone block with an orange ember glow +## "Carpenter" → wooden workbench with saw + log slabs on top +## "Smelter" → dark stone furnace with chimney and ember glow +## "Hearth" → tall stone fireplace with mantle + log fire (h=2 tiles) +## "Millstone" → wooden frame supporting a round grindstone wheel ## Other → generic warm-grey fallback ## ## Bill model (architecture.md "Production: workbenches, recipes, bills"): @@ -42,45 +44,34 @@ const HEARTH_LIGHT_RADIUS: int = 5 ## Pixel size of the procedural radial gradient used for PointLight2D. const LIGHT_TEXTURE_SIZE: int = 64 -# ── sprite atlas (replaces procedural _draw for the four named variants) ───── -## Variant → (texture, atlas top-left coord, height in tiles). Selected from -## the ElvGames House Interior + Marketplace tilesets in the 2026-05-12 visual -## pass; see /tmp/workbench_candidates_v2.png from that session for the diff. +# ── variant rendering ───────────────────────────────────────────────────────── +## All four named workbench variants render procedurally via _draw(). The +## atlas-sprite approach was abandoned in the 2026-05-15 polish pass after +## visual review: the chosen ElvGames atlas tiles read as a chest-of-drawers +## (Carpenter), a tiny candle base (Smelter), a 2-burner stove (Hearth), and +## a stack of cushions (Millstone). Procedural draws give us shape control to +## hit the silhouettes those names imply. See CremationPyre._draw_pyre() for +## precedent — same pattern, local coords centered at (0, 0) at the BOTTOM of +## the workbench tile, drawing UP into negative y. ## -## h_tiles = 2 sprites bottom-anchor and extend UP into the tile above (Bed -## pattern) so the carpenter's tall cabinet reads as a piece of furniture -## standing in the room rather than a flat decal. h_tiles = 1 sprites stay -## within the workbench tile (anvil, stove top, barrel — squat shapes). -## -## Unrecognised label_texts fall through to procedural _draw_generic, so -## ad-hoc workbench variants keep rendering until a sprite is picked for them. -const _INTERIOR_TEX: Texture2D = preload("res://art/tiles/FG_Interior.png") -const _MARKETPLACE_TEX: Texture2D = preload("res://art/tiles/FG_Marketplace.png") -const _VARIANT_SPRITES: Dictionary = { - "Carpenter": {"tex": _INTERIOR_TEX, "coord": Vector2i(24, 20), "h_tiles": 2}, - "Smelter": {"tex": _MARKETPLACE_TEX, "coord": Vector2i(8, 30), "h_tiles": 1}, - "Hearth": {"tex": _INTERIOR_TEX, "coord": Vector2i(16, 32), "h_tiles": 1}, - "Millstone": {"tex": _INTERIOR_TEX, "coord": Vector2i(17, 40), "h_tiles": 1}, -} +## Tall variants (Hearth, h=2 logically) draw above y=-16 into the tile north +## of the bench; pawns stand correctly behind them because y_sort_enabled is +## on and position.y is anchored at the bench-tile's bottom edge. # ── exports ─────────────────────────────────────────────────────────────────── ## Tile position of this workbench in world-tile coordinates. @export var tile: Vector2i = Vector2i.ZERO -## Player-visible label. Also drives the sprite variant (see _VARIANT_SPRITES) -## and procedural _draw fallback for unrecognised values. -## Setter rebuilds the sprite child idempotently — callers can assign -## label_text either before OR after setup() and end up with the right sprite. +## Player-visible label. Also drives the procedural _draw() variant dispatch. +## Setter triggers a redraw + lazy light build — callers can assign label_text +## either before OR after setup() and the visual catches up. ## (World.gd assigns it after setup(); SaveSystem._spawn_workbench too.) @export var label_text: String = "Workbench": set(value): label_text = value - # Setter fires from .tscn property initialisation BEFORE _ready, so - # guard the rebuild until the node is actually in the tree (children - # can't be added safely before then). if is_inside_tree(): - _build_sprite() + queue_redraw() # Hearth-light catch-up: _ready() builds the PointLight2D only when # label_text is already "Hearth", but the project's call pattern # (add_child first, then set label_text) means _ready always saw the @@ -165,8 +156,6 @@ func _exit_tree() -> void: ## One-shot initialiser. Call after add_child() so _ready() has fired. -## Builds the variant sprite using the current label_text — if the caller -## hasn't assigned label_text yet, the setter rebuilds the sprite on assignment. ## Idempotent (safe under save-load's instantiate → setup → from_dict → setup chain). func setup(p_tile: Vector2i) -> void: tile = p_tile @@ -174,52 +163,12 @@ func setup(p_tile: Vector2i) -> void: tile.x * TILE_SIZE_PX + TILE_SIZE_PX / 2.0, tile.y * TILE_SIZE_PX + TILE_SIZE_PX ) - # Y-sort so a 16×32 Carpenter sprite (which rises into the tile north of - # the bench) occludes pawns standing behind it. Matches Bed / Wall. + # Y-sort so tall variants (Hearth) drawing into the tile north of the bench + # occlude pawns standing behind. Matches Bed / Wall. y_sort_enabled = true - _build_sprite() queue_redraw() -## Build the variant Sprite2D child (or no-op when label_text isn't in the -## sprite table — those fall through to procedural _draw rendering). -## Idempotent: frees any previous Sprite child first. Called from setup() AND -## from the label_text setter, so the sprite always matches the current variant. -func _build_sprite() -> void: - var prev := get_node_or_null("Sprite") - if prev != null: - prev.queue_free() - var data = _VARIANT_SPRITES.get(label_text) - if data == null: - # Generic / unknown variants keep procedural rendering. _draw_generic - # fires through the existing match in _draw(). - return - var sprite := Sprite2D.new() - sprite.name = "Sprite" - sprite.texture = data["tex"] - sprite.region_enabled = true - var coord: Vector2i = data["coord"] - var h_tiles: int = data["h_tiles"] - var pixels_h: int = TILE_SIZE_PX * h_tiles - sprite.region_rect = Rect2( - coord.x * TILE_SIZE_PX, - coord.y * TILE_SIZE_PX, - TILE_SIZE_PX, - pixels_h, - ) - sprite.centered = true - # Parent position.y is at the BOTTOM of the workbench tile (see setup()). - # Bottom-anchor the sprite by offsetting it up by half its height, so a - # 16×16 sprite spans local y −16..0 (within the bench tile) and a 16×32 - # sprite spans local y −32..0 (bench tile + the tile above it, like Bed - # but extending UPWARD — workbenches don't have a "foot tile"). - sprite.offset = Vector2(0.0, -float(pixels_h) / 2.0) - sprite.z_index = 0 - # Ghost state — translucent until built. Solidified in _complete(). - sprite.modulate.a = 1.0 if _completed else 0.4 - add_child(sprite) - - # ── BuildJob interface ──────────────────────────────────────────────────────── ## True while the workbench still needs construction work. @@ -257,6 +206,15 @@ func add_bill(b) -> void: Audit.log("workbench", "%s: bill added — recipe '%s'" % [label_text, b.recipe.id]) +## Remove a bill from this workbench's queue. If the bill is currently being +## crafted, the active toil is interrupted cleanly so the pawn re-decides. +func remove_bill(b) -> void: + if current_bill == b: + on_craft_interrupted() + bills.erase(b) + Audit.log("workbench", "%s: bill removed — recipe '%s'" % [label_text, b.recipe.id]) + + ## Return the first bill that is active and whose required_skill matches ## this bench's accepted_skill. Returns null when none qualify. ## CraftingProvider calls this; JobRunner also calls it when the current_bill @@ -367,29 +325,156 @@ func from_dict(d: Dictionary) -> void: # ── render ───────────────────────────────────────────────────────────────────── func _draw() -> void: - # Sprite-backed variants (Carpenter / Smelter / Hearth) render entirely - # through their Sprite2D child — no procedural fallback needed. Millstone - # also has a sprite but keeps a small dark-grey wheel overlay so the - # wood barrel below reads as "grinding station" rather than a plain barrel. - # Unrecognised label_texts fall through to _draw_generic so ad-hoc - # benches still render until a sprite is picked for them. var alpha: float = 1.0 if _completed else 0.4 - if label_text == "Millstone": - _draw_millstone_overlay(alpha) - return - if _VARIANT_SPRITES.has(label_text): - return - _draw_generic(alpha) + match label_text: + "Carpenter": _draw_carpenter(alpha) + "Smelter": _draw_smelter(alpha) + "Hearth": _draw_hearth(alpha) + "Millstone": _draw_millstone(alpha) + _: _draw_generic(alpha) -## Stone-wheel overlay drawn on top of the Millstone barrel sprite. Without -## this, the barrel reads as "water/grain storage" rather than a millstone. -## The circle sits inside the top half of the barrel's tile. -func _draw_millstone_overlay(alpha: float) -> void: - var wheel := Color(0.40, 0.40, 0.36, alpha) - var rim := Color(0.22, 0.22, 0.20, alpha) - draw_circle(Vector2(0.0, -10.0), 4.5, wheel) - draw_arc(Vector2(0.0, -10.0), 4.5, 0.0, TAU, 12, rim, 1.0) +## Carpenter — wooden plank top with two visible legs and a hand-saw + log +## slabs on top. Reads as a workshop bench at 16×16 thanks to the saw blade +## silhouette breaking the plain top. +func _draw_carpenter(alpha: float) -> void: + var plank_top := Color(0.70, 0.50, 0.30, alpha) + var plank_front := Color(0.55, 0.38, 0.22, alpha) + var plank_edge := Color(0.35, 0.22, 0.12, alpha) + var leg := Color(0.30, 0.20, 0.10, alpha) + var saw_blade := Color(0.78, 0.78, 0.82, alpha) + var saw_handle := Color(0.55, 0.30, 0.15, alpha) + var log_face := Color(0.62, 0.42, 0.24, alpha) + var log_ring := Color(0.42, 0.27, 0.14, alpha) + var outline := Color(0.15, 0.10, 0.05, 0.7 * alpha) + + # Two legs at the corners (front face). + draw_rect(Rect2(Vector2(-7.0, -10.0), Vector2(2.0, 10.0)), leg) + draw_rect(Rect2(Vector2( 5.0, -10.0), Vector2(2.0, 10.0)), leg) + # Plank front face — thick band. + draw_rect(Rect2(Vector2(-8.0, -12.0), Vector2(16.0, 5.0)), plank_front) + # Plank top — slimmer band above the front, suggesting depth. + draw_rect(Rect2(Vector2(-8.0, -15.0), Vector2(16.0, 3.0)), plank_top) + # Edge highlight between top and front. + draw_line(Vector2(-8.0, -12.0), Vector2(8.0, -12.0), plank_edge, 1.0) + # Two short log slabs sitting on the left side of the top. + draw_rect(Rect2(Vector2(-6.0, -17.0), Vector2(3.0, 2.0)), log_face) + draw_rect(Rect2(Vector2(-3.0, -17.0), Vector2(3.0, 2.0)), log_face) + draw_line(Vector2(-4.5, -17.0), Vector2(-4.5, -15.0), log_ring, 1.0) + # Saw on the right — handle + blade silhouette. + draw_rect(Rect2(Vector2(1.0, -16.0), Vector2(6.0, 1.5)), saw_blade) + draw_rect(Rect2(Vector2(5.5, -17.0), Vector2(2.0, 2.0)), saw_handle) + # Outline. + draw_rect(Rect2(Vector2(-8.0, -16.0), Vector2(16.0, 16.0)), outline, false, 1.0) + + +## Smelter — stone furnace block with a stubby chimney puffing smoke and a +## bright ember-glow opening on the front face. Stone-grey base separates it +## visually from the Carpenter's warm wood. +func _draw_smelter(alpha: float) -> void: + var stone_top := Color(0.55, 0.55, 0.55, alpha) + var stone_front := Color(0.42, 0.42, 0.43, alpha) + var stone_shad := Color(0.30, 0.30, 0.32, alpha) + var ember := Color(0.98, 0.55, 0.10, alpha) + var ember_core := Color(1.00, 0.85, 0.30, alpha) + var chimney := Color(0.32, 0.30, 0.30, alpha) + var smoke := Color(0.75, 0.73, 0.70, alpha * 0.7) + var outline := Color(0.15, 0.12, 0.10, 0.7 * alpha) + + # Stone front body. + draw_rect(Rect2(Vector2(-8.0, -12.0), Vector2(16.0, 12.0)), stone_front) + # Top face — slightly lighter band. + draw_rect(Rect2(Vector2(-8.0, -15.0), Vector2(16.0, 3.0)), stone_top) + # Furnace mouth — dark recess with bright ember inside. + draw_rect(Rect2(Vector2(-4.0, -9.0), Vector2(8.0, 5.0)), stone_shad) + draw_rect(Rect2(Vector2(-3.0, -8.0), Vector2(6.0, 3.0)), ember) + draw_rect(Rect2(Vector2(-2.0, -7.0), Vector2(4.0, 1.0)), ember_core) + # Mortar lines across the front for stone-block feel. + draw_line(Vector2(-8.0, -8.0), Vector2(-4.0, -8.0), stone_shad, 1.0) + draw_line(Vector2( 4.0, -8.0), Vector2( 8.0, -8.0), stone_shad, 1.0) + # Chimney + smoke wisps rising above. + draw_rect(Rect2(Vector2(2.0, -19.0), Vector2(3.0, 4.0)), chimney) + draw_rect(Rect2(Vector2(3.0, -22.0), Vector2(1.0, 3.0)), smoke) + draw_rect(Rect2(Vector2(2.0, -24.0), Vector2(1.0, 2.0)), smoke) + # Outline. + draw_rect(Rect2(Vector2(-8.0, -16.0), Vector2(16.0, 16.0)), outline, false, 1.0) + + +## Hearth — tall (h=2) stone fireplace with mantle, arched opening, log fire +## with embers, and a flame licking up. Draws above y=-16 into the tile north +## of the bench (y_sort handles occlusion). Light-emitting via _maybe_build_light. +func _draw_hearth(alpha: float) -> void: + var stone := Color(0.60, 0.58, 0.55, alpha) + var stone_dark := Color(0.42, 0.40, 0.38, alpha) + var mantle := Color(0.50, 0.34, 0.20, alpha) + var mantle_edge := Color(0.32, 0.20, 0.10, alpha) + var opening := Color(0.08, 0.04, 0.02, alpha) + var log_wood := Color(0.55, 0.32, 0.15, alpha) + var ember := Color(0.98, 0.55, 0.10, alpha) + var flame_inner := Color(1.00, 0.85, 0.30, alpha) + var flame_outer := Color(0.95, 0.40, 0.05, alpha) + var outline := Color(0.15, 0.10, 0.05, 0.7 * alpha) + + # Stone surround — fills the bench tile (y −16..0) and the tile above + # (y −32..-16) so the fireplace is a 16×32 silhouette. + draw_rect(Rect2(Vector2(-8.0, -32.0), Vector2(16.0, 32.0)), stone) + # Stone block mortar — a couple of horizontal seams. + draw_line(Vector2(-8.0, -22.0), Vector2(8.0, -22.0), stone_dark, 1.0) + draw_line(Vector2(-8.0, -28.0), Vector2(8.0, -28.0), stone_dark, 1.0) + draw_line(Vector2(-2.0, -32.0), Vector2(-2.0, -28.0), stone_dark, 1.0) + draw_line(Vector2( 3.0, -28.0), Vector2( 3.0, -22.0), stone_dark, 1.0) + # Wooden mantle — horizontal beam across the middle. + draw_rect(Rect2(Vector2(-8.0, -19.0), Vector2(16.0, 3.0)), mantle) + draw_line(Vector2(-8.0, -19.0), Vector2(8.0, -19.0), mantle_edge, 1.0) + draw_line(Vector2(-8.0, -16.0), Vector2(8.0, -16.0), mantle_edge, 1.0) + # Arched opening — dark recess in the lower stone block. + draw_rect(Rect2(Vector2(-6.0, -14.0), Vector2(12.0, 14.0)), opening) + # Two stacked logs sitting in the opening. + draw_rect(Rect2(Vector2(-5.0, -4.0), Vector2(10.0, 2.0)), log_wood) + draw_rect(Rect2(Vector2(-4.0, -6.0), Vector2(8.0, 2.0)), log_wood) + # Ember strip glowing under the logs. + draw_rect(Rect2(Vector2(-4.0, -2.0), Vector2(8.0, 2.0)), ember) + # Flame — tapered teardrop above the logs. + draw_rect(Rect2(Vector2(-3.0, -10.0), Vector2(6.0, 4.0)), flame_outer) + draw_rect(Rect2(Vector2(-2.0, -12.0), Vector2(4.0, 2.0)), flame_outer) + draw_rect(Rect2(Vector2(-1.0, -13.0), Vector2(2.0, 1.0)), flame_outer) + draw_rect(Rect2(Vector2(-2.0, -9.0), Vector2(4.0, 2.0)), flame_inner) + draw_rect(Rect2(Vector2(-1.0, -11.0), Vector2(2.0, 2.0)), flame_inner) + # Outline around the full 16×32 silhouette. + draw_rect(Rect2(Vector2(-8.0, -32.0), Vector2(16.0, 32.0)), outline, false, 1.0) + + +## Millstone — wooden frame supporting a large round grindstone, viewed +## 3/4-perspective so the wheel reads as both round (top) and solid (front). +func _draw_millstone(alpha: float) -> void: + var frame_top := Color(0.55, 0.36, 0.18, alpha) + var frame_front := Color(0.42, 0.26, 0.12, alpha) + var frame_edge := Color(0.25, 0.14, 0.06, alpha) + var wheel := Color(0.55, 0.53, 0.50, alpha) + var wheel_dark := Color(0.34, 0.32, 0.30, alpha) + var wheel_rim := Color(0.18, 0.16, 0.14, alpha) + var groove := Color(0.28, 0.26, 0.24, alpha) + var pin := Color(0.20, 0.18, 0.16, alpha) + var outline := Color(0.15, 0.10, 0.05, 0.7 * alpha) + + # Wooden frame base — front + top faces. + draw_rect(Rect2(Vector2(-8.0, -7.0), Vector2(16.0, 7.0)), frame_front) + draw_rect(Rect2(Vector2(-8.0, -10.0), Vector2(16.0, 3.0)), frame_top) + draw_line(Vector2(-8.0, -7.0), Vector2(8.0, -7.0), frame_edge, 1.0) + # Grindstone — large dark-grey disc, rim slightly darker. Centred over + # the top of the frame, sticking up into the tile above only slightly. + var c := Vector2(0.0, -12.0) + draw_circle(c, 7.0, wheel_rim) + draw_circle(c, 6.0, wheel) + # Front-face shadow band across the lower half of the disc. + draw_rect(Rect2(Vector2(-6.0, -12.0), Vector2(12.0, 5.0)), wheel_dark) + # Two radial grooves — pie-slice indicators that the stone spins. + draw_line(c, c + Vector2(5.0, -3.5), groove, 1.0) + draw_line(c, c + Vector2(-5.0, -3.5), groove, 1.0) + # Centre pin / spindle. + draw_circle(c, 1.2, pin) + # Outline. + draw_rect(Rect2(Vector2(-8.0, -19.0), Vector2(16.0, 19.0)), outline, false, 1.0) func _draw_generic(alpha: float) -> void: @@ -410,11 +495,7 @@ func _draw_generic(alpha: float) -> void: func _complete() -> void: _completed = true - # Solidify the ghost: sprite child (if any) goes from 40% to full opacity. - # Procedural-only variants reread alpha through _draw() via queue_redraw. - var sprite: Sprite2D = get_node_or_null("Sprite") - if sprite != null: - sprite.modulate.a = 1.0 + # Procedural-only variants re-read alpha through _draw() via queue_redraw. # Phase 11: enable PointLight2D for light-emitting workbenches on completion. if _light != null: _light.enabled = is_on() diff --git a/scenes/main/main.gd b/scenes/main/main.gd index 692a27d..b3e6686 100644 --- a/scenes/main/main.gd +++ b/scenes/main/main.gd @@ -19,6 +19,12 @@ const LOAD_MENU_SCRIPT: Script = preload("res://scenes/ui/load_menu. const RESUME_TOAST_SCRIPT: Script = preload("res://scenes/ui/resume_toast.gd") # Phase 17 — PawnDetailPanel (layer 18) and SettingsMenu (layer 26). const PAWN_DETAIL_PANEL_SCRIPT: Script = preload("res://scenes/ui/pawn_detail_panel.gd") +const WORKBENCH_PANEL_SCRIPT: Script = preload("res://scenes/ui/workbench_panel.gd") +const MEDIEVAL_THEME_SCRIPT: Script = preload("res://scenes/ui/medieval_theme.gd") + +# Built once in _ready and re-applied to any CanvasLayer-rooted Control because +# CanvasLayer doesn't propagate the root-Window theme cascade. +var _app_theme: Theme = null const SETTINGS_MENU_SCRIPT: Script = preload("res://scenes/ui/settings_menu.gd") # Phase 17 (Agent B) — BuildDrawer bottom-sheet (layer 16). const BUILD_DRAWER_SCRIPT: Script = preload("res://scenes/ui/build_drawer.gd") @@ -39,6 +45,13 @@ func _ready() -> void: assert(SaveSystem != null, "SaveSystem autoload missing") assert(Autosave != null, "Autosave autoload missing") + # Medieval-warm Theme — assigned on the root Window first. Cascade alone + # doesn't reach Controls inside CanvasLayers (CanvasLayer has no theme + # property), so we also walk the tree post-mount and apply to every Control + # encountered. (2026-05-16 polish pass.) + _app_theme = MEDIEVAL_THEME_SCRIPT.build() + get_tree().root.theme = _app_theme + # Phase 15 — Storyteller UI layers. Runtime-instantiated so no .tscn edit is # needed. CanvasLayer ensures correct draw order above World/TopBar regardless # of parent-node position. @@ -79,6 +92,13 @@ func _ready() -> void: pawn_detail_panel.name = "PawnDetailPanel" add_child(pawn_detail_panel) + # Bill-editor bottom-sheet for workbenches. Same shape as PawnDetailPanel + # (right-anchored 360 px, layer 18); mutually exclusive with it via Selection. + var workbench_panel := CanvasLayer.new() + workbench_panel.set_script(WORKBENCH_PANEL_SCRIPT) + workbench_panel.name = "WorkbenchPanel" + add_child(workbench_panel) + var settings_menu := CanvasLayer.new() settings_menu.set_script(SETTINGS_MENU_SCRIPT) settings_menu.name = "SettingsMenu" @@ -91,7 +111,7 @@ func _ready() -> void: if top_bar.has_method("_add_settings_btn"): top_bar._add_settings_btn() - Audit.log("main", "Phase 17 — PawnDetailPanel + SettingsMenu mounted.") + Audit.log("main", "Phase 17 — PawnDetailPanel + WorkbenchPanel + SettingsMenu mounted.") # Phase 17 (Agent B) — BuildDrawer bottom-sheet (layer 16). # Must mount AFTER the World node is ready (World._ready seeds designation_ctl). @@ -148,3 +168,33 @@ func _ready() -> void: top_bar._add_work_log_btns() Audit.log("main", "Phase 17 (Agent C) — WorkPriorityMatrix + AlertsLog mounted.") + + # Apply the medieval theme to every Control under each CanvasLayer. + # CanvasLayers interrupt the root-Window theme cascade so we have to seed + # each one explicitly. Defer one frame so panels that build their UI in + # _ready (PawnDetailPanel, WorkbenchPanel, BuildDrawer) finish first. + call_deferred("_apply_theme_to_canvas_layers") + + +## Walks the scene tree and assigns _app_theme to every Control directly under +## a CanvasLayer (the topmost Control in each layer's branch). From there the +## standard Control-to-Control cascade carries the theme to all descendants. +## Also catches popups and modals that mount later via child_entered_tree. +func _apply_theme_to_canvas_layers() -> void: + for c in get_children(): + if c is CanvasLayer: + _seed_layer_theme(c) + # Watch for late additions (popup menus, modals). + if not c.child_entered_tree.is_connected(_on_layer_child_added): + c.child_entered_tree.connect(_on_layer_child_added) + + +func _seed_layer_theme(layer: CanvasLayer) -> void: + for c in layer.get_children(): + if c is Control and c.theme == null: + c.theme = _app_theme + + +func _on_layer_child_added(node: Node) -> void: + if node is Control and node.theme == null: + node.theme = _app_theme diff --git a/scenes/pawn/pawn.gd b/scenes/pawn/pawn.gd index c925b2d..c3a838a 100644 --- a/scenes/pawn/pawn.gd +++ b/scenes/pawn/pawn.gd @@ -26,6 +26,10 @@ class_name Pawn ## Phase 14 — corpse scene instantiated on death. const CORPSE_SCENE: PackedScene = preload("res://scenes/entities/corpse.tscn") +## Slice-1 pawn sprite helper. Preloaded (not class_name) because the global +## class registry isn't reliable during cold-start parse for sibling scripts. +const _PAWN_SPRITE_FRAMES: Script = preload("res://scenes/pawn/pawn_sprite_frames.gd") + const STEP_TICKS: int = 10 const TILE_SIZE_PX: int = 16 # Mirrors World.TILE_SIZE_PX; standalone so Pawn needs no World reference. @@ -79,6 +83,11 @@ signal arrived_at_destination(tile: Vector2i) @export var pawn_name: String = "" +## Last-known facing direction as a unit Vector2i (one of (0,1) down, (-1,0) +## left, (1,0) right, (0,-1) up). Updated when walking; persists when idle +## so the idle sprite faces the last direction walked. Defaults to down. +var facing: Vector2i = Vector2i(0, 1) + var tile: Vector2i = Vector2i.ZERO # Phase 7 — hunger need (design.md "Hungry" status). Full at spawn. @@ -180,6 +189,10 @@ var _cold_accum: float = 0.0 const SHELTER_DEBUG: bool = false var _shelter_prev: bool = false +## AnimatedSprite2D child painted with peasant atlases. Built in _ready(); +## animation switched by _update_anim() each _process tick. +var _sprite: AnimatedSprite2D = null + var _path: Array[Vector2i] = [] var _step_progress: float = 0.0 var _selected: bool = false @@ -204,6 +217,10 @@ func _ready() -> void: if EventBus.has_signal("corpse_cremated"): EventBus.corpse_cremated.connect(_on_corpse_cremated) + # Sprite mount is deferred to setup() / from_dict() because the atlas pick + # depends on pawn_name (deterministic name-hash), which isn't assigned yet + # when _ready() fires. See _mount_sprite() below. + func setup(p_name: String, start_tile: Vector2i) -> void: pawn_name = p_name @@ -215,9 +232,34 @@ func setup(p_name: String, start_tile: Vector2i) -> void: # Same formula as _draw() body disc: deterministic hue from name hash. var hue := float(pawn_name.hash() % 360) / 360.0 portrait_color = Color.from_hsv(hue, 0.7, 0.85) + # Slice-1 character sprite: depends on pawn_name, so mount here (not _ready). + _mount_sprite() Audit.log("pawn", "%s spawned at %s" % [pawn_name, start_tile]) +## Build the AnimatedSprite2D child from the peasant atlas trio picked +## deterministically from pawn_name. Idempotent — safe to call from setup() +## AND from_dict() (the save-load path also re-enters setup). +func _mount_sprite() -> void: + if _sprite != null: + _sprite.queue_free() + _sprite = null + var atlases := _atlas_for_pawn(self) + var sf: SpriteFrames = _PAWN_SPRITE_FRAMES.build(atlases) + _sprite = AnimatedSprite2D.new() + _sprite.name = "Sprite" + _sprite.sprite_frames = sf + _sprite.centered = true + _sprite.offset = Vector2(0, -8) # bottom-anchor: feet ≈ tile bottom edge + # show_behind_parent lets the sprite render at the same z_index as the + # parent Pawn (so it stays above the floor TileMap), while ALSO drawing + # beneath the parent's _draw() callback — so selection ring + carry + # indicator overlay on top. z_index=-1 would sink below the floor too. + _sprite.show_behind_parent = true + _sprite.play(&"idle_down") + add_child(_sprite) + + # ── public API ────────────────────────────────────────────────────────────── func walk_along_path(new_path: Array[Vector2i]) -> void: @@ -919,6 +961,8 @@ func to_dict() -> Dictionary: # Phase 14 — bleed-out timeout counter. Default 0 for pre-Phase-14 saves. "bleed_ticks": _bleed_ticks, "last_damage_source": String(_last_damage_source), + "facing_x": facing.x, + "facing_y": facing.y, # Phase 17 — per-pawn work-priority matrix. Keys stored as plain Strings for # JSON round-trip safety (StringName keys survive the cast back via StringName()). "work_priorities": _serialise_work_priorities(), @@ -928,6 +972,9 @@ func to_dict() -> Dictionary: func from_dict(d: Dictionary) -> void: pawn_name = d.get("name", "") tile = Vector2i(int(d.get("tile_x", 0)), int(d.get("tile_y", 0))) + facing = Vector2i(int(d.get("facing_x", 0)), int(d.get("facing_y", 1))) + # Re-mount sprite now that pawn_name is set (atlas pick is name-hash driven). + _mount_sprite() _path.clear() for entry in d.get("path", []): @@ -1070,6 +1117,9 @@ func _advance_walk() -> void: job_runner.cancel_job() Audit.log("pawn", "%s walk aborted: %s became impassable" % [pawn_name, next_tile]) return + var delta := next_tile - tile + if delta != Vector2i.ZERO: + facing = _canonical_facing(delta) tile = next_tile _path.remove_at(0) _step_progress = 0.0 @@ -1087,37 +1137,59 @@ func _process(_delta: float) -> void: var next := _path[0] if is_walking() else tile var to_world := _tile_to_world(next) position = from_world.lerp(to_world, _step_progress) + _update_anim() + + +## Pick the right animation each tick based on walk state, facing, and downed. +## Called from _process(). Cheap: only calls _sprite.play() when the target +## animation differs from the current one (AnimatedSprite2D restarts from +## frame 0 on every play() call, so we must guard). +func _update_anim() -> void: + if _sprite == null: + return + if is_downed(): + if _sprite.animation != &"dead": + _sprite.play(&"dead") + return + var prefix: StringName = &"walk" if is_walking() else &"idle" + var dir_suffix: StringName = _facing_suffix() + var target: StringName = StringName("%s_%s" % [prefix, dir_suffix]) + if _sprite.animation != target: + _sprite.play(target) + + +## Map `facing` Vector2i to the animation-name suffix (down/left/right/up). +func _facing_suffix() -> StringName: + if facing == Vector2i(0, -1): + return &"up" + if facing == Vector2i(-1, 0): + return &"left" + if facing == Vector2i(1, 0): + return &"right" + return &"down" func _draw() -> void: - # Phase 14 — use the stored portrait_color (computed once in setup()/from_dict()). - # This is the same formula as the old inline hue derivation; consolidating here - # removes the duplication and ensures the corpse head-dot matches exactly. - var body_colour := portrait_color - - if is_downed(): - # Phase 9 — Downed pawn: rotated 90° (lying on ground) + desaturated. - # draw_set_transform applies to all subsequent draw_* calls in this _draw. - draw_set_transform(Vector2.ZERO, PI / 2.0, Vector2.ONE) - draw_circle(Vector2.ZERO, 6.0, body_colour.lerp(Color(0.5, 0.5, 0.5), 0.6)) - draw_arc(Vector2.ZERO, 7.0, 0.0, TAU, 24, Color(0.0, 0.0, 0.0, 0.4), 1.0) - # Reset transform so selection ring and carry indicator render upright. - draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE) - else: - draw_circle(Vector2.ZERO, 6.0, body_colour) - # Dark outline ring. - draw_arc(Vector2.ZERO, 7.0, 0.0, TAU, 24, Color(0.0, 0.0, 0.0, 0.6), 1.0) - - # Selection ring — drawn after body regardless of downed state. + # Body is the AnimatedSprite2D child (see _ready). _draw() is now overlay-only. + # Selection ring — drawn on top of the sprite (parent _draw runs after the + # child's z_index=-1 sprite draws). if _selected: draw_arc(Vector2.ZERO, 10.0, 0.0, TAU, 32, Color(1.0, 0.9, 0.2, 0.85), 2.0) - # Phase 4 — carry indicator: small coloured square at upper-right of body. + # Carry indicator — draw a shrunk version of the actual item shape at the + # pawn's upper-right ("held out at chest height"). Uses Item.draw_item_shape + # at 0.55× scale (the shapes are authored for a 12×12 box; 0.55× → ~7×7). + # Falls back to a hue-hashed square if the item type isn't shape-registered. if carried_item != null: - var ci_hue := float(carried_item.item_type.hash() % 360) / 360.0 - var ci_color := Color.from_hsv(ci_hue, 0.6, 0.85) - draw_rect(Rect2(6, -10, 7, 7), ci_color) - draw_rect(Rect2(6, -10, 7, 7), Color(0, 0, 0, 0.7), false, 1.0) + var ci_center := Vector2(7.0, -12.0) + var ci_scale := 0.55 + draw_set_transform(ci_center, 0.0, Vector2(ci_scale, ci_scale)) + if not Item.draw_item_shape(self, carried_item.item_type, carried_item.subtype): + var ci_hue := float(carried_item.item_type.hash() % 360) / 360.0 + var ci_color := Color.from_hsv(ci_hue, 0.6, 0.85) + draw_rect(Rect2(-6, -6, 12, 12), ci_color) + draw_rect(Rect2(-6, -6, 12, 12), Color(0, 0, 0, 0.7), false, 1.0) + draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE) # ── helpers ───────────────────────────────────────────────────────────────── @@ -1127,3 +1199,30 @@ func _tile_to_world(t: Vector2i) -> Vector2: t.x * TILE_SIZE_PX + TILE_SIZE_PX / 2.0, t.y * TILE_SIZE_PX + TILE_SIZE_PX / 2.0 ) + + +## Maps any tile-delta to a cardinal Vector2i facing direction. Prefers the +## axis with larger absolute magnitude; ties favor horizontal. Returns down +## (0, 1) for zero delta as a safe default. +static func _canonical_facing(delta: Vector2i) -> Vector2i: + if delta == Vector2i.ZERO: + return Vector2i(0, 1) + if abs(delta.x) >= abs(delta.y): + return Vector2i(sign(delta.x), 0) if delta.x != 0 else Vector2i(0, 1) + return Vector2i(0, sign(delta.y)) + + +## Returns the {idle, walk, dead} atlas trio for a pawn. Slice 1: always +## peasant, picked deterministically from name hash (mod 15, +1 for 001-015 +## naming). Slice 2 will branch on equipped armor (helm + cuirass + boots → +## knight atlas, etc.) at this single extension point. +const _PEASANT_COUNT: int = 15 + +static func _atlas_for_pawn(pawn) -> Dictionary: + var idx: int = (absi(pawn.pawn_name.hash()) % _PEASANT_COUNT) + 1 + var n: String = "%03d" % idx + return { + "idle": load("res://art/sprites/characters/Character_%s_Idle.png" % n), + "walk": load("res://art/sprites/characters/Character_%s_Walk.png" % n), + "dead": load("res://art/sprites/characters/Character_%s_Dead.png" % n), + } diff --git a/scenes/pawn/pawn.tscn b/scenes/pawn/pawn.tscn index 87e312b..1862d14 100644 --- a/scenes/pawn/pawn.tscn +++ b/scenes/pawn/pawn.tscn @@ -6,7 +6,7 @@ script = ExtResource("1_pawn") [node name="NameLabel" type="Label" parent="."] -position = Vector2(-20, -18) +position = Vector2(-20, -38) size = Vector2(40, 12) theme_override_font_sizes/font_size = 8 horizontal_alignment = 1 diff --git a/scenes/pawn/pawn_sprite_frames.gd b/scenes/pawn/pawn_sprite_frames.gd new file mode 100644 index 0000000..ca5593d --- /dev/null +++ b/scenes/pawn/pawn_sprite_frames.gd @@ -0,0 +1,51 @@ +class_name PawnSpriteFrames extends RefCounted +## Builds a SpriteFrames resource from a {idle, walk, dead} atlas trio for a +## peasant character. Atlases are 128×128 with 4 rows (down/left/right/up) +## × 4 frames (32×32 cells). Idle + Walk produce 4 directional animations +## each (loop=true); Dead is a single frame (loop=false) from row 0. +## +## Created via PawnSpriteFrames.build(atlases) from Pawn._ready(). The +## returned SpriteFrames is assigned to an AnimatedSprite2D's sprite_frames. + +const CELL: int = 32 +const DIRS: Array[StringName] = [&"down", &"left", &"right", &"up"] + +## Build a SpriteFrames containing: +## idle_down/left/right/up — 4 frames each, loop, 4 fps +## walk_down/left/right/up — 4 frames each, loop, 8 fps +## dead — 1 frame, no loop (from idle/dead row 0 col 0) +## +## `atlases` is a Dictionary with three Texture2D values keyed by "idle", +## "walk", "dead". Each texture is 128×128. +static func build(atlases: Dictionary) -> SpriteFrames: + var sf := SpriteFrames.new() + # AnimatedSprite2D auto-creates a `default` animation; remove it so the + # scene doesn't render an empty placeholder if a caller mistypes an anim name. + if sf.has_animation(&"default"): + sf.remove_animation(&"default") + + _add_directional(sf, &"idle", atlases["idle"], true, 4.0) + _add_directional(sf, &"walk", atlases["walk"], true, 8.0) + + # Dead — single 32×32 frame from row 0 (down-facing) of the dead atlas. + sf.add_animation(&"dead") + sf.set_animation_loop(&"dead", false) + var dead_at := AtlasTexture.new() + dead_at.atlas = atlases["dead"] + dead_at.region = Rect2(0, 0, CELL, CELL) + sf.add_frame(&"dead", dead_at) + + return sf + + +static func _add_directional(sf: SpriteFrames, prefix: StringName, tex: Texture2D, loop: bool, fps: float) -> void: + for row in 4: + var anim_name := StringName("%s_%s" % [prefix, DIRS[row]]) + sf.add_animation(anim_name) + sf.set_animation_loop(anim_name, loop) + sf.set_animation_speed(anim_name, fps) + for col in 4: + var at := AtlasTexture.new() + at.atlas = tex + at.region = Rect2(col * CELL, row * CELL, CELL, CELL) + sf.add_frame(anim_name, at) diff --git a/scenes/pawn/pawn_sprite_frames.gd.uid b/scenes/pawn/pawn_sprite_frames.gd.uid new file mode 100644 index 0000000..42721ff --- /dev/null +++ b/scenes/pawn/pawn_sprite_frames.gd.uid @@ -0,0 +1 @@ +uid://c4otxk5jg0kxr diff --git a/scenes/ui/build_drawer.gd b/scenes/ui/build_drawer.gd index bacfa28..ea85b1b 100644 --- a/scenes/ui/build_drawer.gd +++ b/scenes/ui/build_drawer.gd @@ -180,10 +180,11 @@ func _build_designate_tab() -> Control: var flow := _make_flow_grid() box.add_child(flow) - _add_tool_btn(flow, Strings.t(&"tool.chop"), Color(0.3, 0.7, 0.2), func() -> void: _activate(&"chop", &"", Strings.t(&"tool.chop"))) - _add_tool_btn(flow, Strings.t(&"tool.mine"), Color(0.6, 0.6, 0.6), func() -> void: _activate(&"mine", &"", Strings.t(&"tool.mine"))) - _add_tool_btn(flow, Strings.t(&"tool.dig_grave"),Color(0.4, 0.3, 0.2), func() -> void: _activate(&"dig_grave",&"", Strings.t(&"tool.dig_grave"))) - _add_tool_btn(flow, Strings.t(&"tool.no_roof"), Color(0.7, 0.7, 0.9), func() -> void: _activate(&"no_roof", &"", Strings.t(&"tool.no_roof"))) + _add_tool_btn(flow, Strings.t(&"tool.chop"), &"chop", func() -> void: _activate(&"chop", &"", Strings.t(&"tool.chop"))) + _add_tool_btn(flow, Strings.t(&"tool.mine"), &"mine", func() -> void: _activate(&"mine", &"", Strings.t(&"tool.mine"))) + _add_tool_btn(flow, Strings.t(&"tool.dig_grave"), &"dig_grave", func() -> void: _activate(&"dig_grave", &"", Strings.t(&"tool.dig_grave"))) + _add_tool_btn(flow, Strings.t(&"tool.no_roof"), &"no_roof", func() -> void: _activate(&"no_roof", &"", Strings.t(&"tool.no_roof"))) + _add_tool_btn(flow, Strings.t(&"tool.plant_tree"), &"plant_tree", func() -> void: _activate(&"plant_tree", &"", Strings.t(&"tool.plant_tree"))) return box @@ -197,46 +198,52 @@ func _build_build_tab() -> Control: box.add_child(flow) # Wall — show material chooser on first tap. - _add_tool_btn(flow, Strings.t(&"tool.build_wall_stone"), Color(0.55, 0.55, 0.55), + _add_tool_btn(flow, Strings.t(&"tool.build_wall_stone"), &"build_wall_stone", func() -> void: _activate_wall(&"stone")) - _add_tool_btn(flow, Strings.t(&"tool.build_wall_wood"), Color(0.65, 0.45, 0.25), + _add_tool_btn(flow, Strings.t(&"tool.build_wall_wood"), &"build_wall_wood", func() -> void: _activate_wall(&"wood")) # Floor. - _add_tool_btn(flow, Strings.t(&"tool.build_floor_wood"), Color(0.60, 0.40, 0.20), + _add_tool_btn(flow, Strings.t(&"tool.build_floor_wood"), &"build_floor_wood", func() -> void: _activate_floor(&"wood")) - _add_tool_btn(flow, Strings.t(&"tool.build_floor_stone"), Color(0.60, 0.60, 0.55), + _add_tool_btn(flow, Strings.t(&"tool.build_floor_stone"), &"build_floor_stone", func() -> void: _activate_floor(&"stone")) # Door + Crate. - _add_tool_btn(flow, Strings.t(&"tool.build_door"), Color(0.55, 0.35, 0.15), + _add_tool_btn(flow, Strings.t(&"tool.build_door"), &"build_door", func() -> void: _activate(&"build_door", &"", Strings.t(&"tool.build_door"))) - _add_tool_btn(flow, Strings.t(&"tool.build_crate"), Color(0.65, 0.45, 0.10), + _add_tool_btn(flow, Strings.t(&"tool.build_crate"), &"build_crate", func() -> void: _activate(&"build_crate", &"", Strings.t(&"tool.build_crate"))) # Bed + Torch. - _add_tool_btn(flow, Strings.t(&"tool.build_bed"), Color(0.40, 0.40, 0.80), + _add_tool_btn(flow, Strings.t(&"tool.build_bed"), &"build_bed", func() -> void: _activate(&"build_bed", &"", Strings.t(&"tool.build_bed"))) - _add_tool_btn(flow, Strings.t(&"tool.build_torch"), Color(0.90, 0.70, 0.20), + _add_tool_btn(flow, Strings.t(&"tool.build_torch"), &"build_torch", func() -> void: _activate(&"build_torch", &"", Strings.t(&"tool.build_torch"))) # Workbenches. _add_tool_btn(flow, Strings.t(&"tool.workbench_carpenter"), - Color(0.50, 0.35, 0.15), + &"build_workbench_carpenter", func() -> void: _activate(&"build_workbench_carpenter", &"", Strings.t(&"tool.workbench_carpenter"))) _add_tool_btn(flow, Strings.t(&"tool.workbench_smelter"), - Color(0.60, 0.55, 0.45), + &"build_workbench_smelter", func() -> void: _activate(&"build_workbench_smelter", &"", Strings.t(&"tool.workbench_smelter"))) _add_tool_btn(flow, Strings.t(&"tool.workbench_millstone"), - Color(0.55, 0.55, 0.55), + &"build_workbench_millstone", func() -> void: _activate(&"build_workbench_millstone", &"", Strings.t(&"tool.workbench_millstone"))) _add_tool_btn(flow, Strings.t(&"tool.workbench_hearth"), - Color(0.80, 0.35, 0.15), + &"build_workbench_hearth", func() -> void: _activate(&"build_workbench_hearth", &"", Strings.t(&"tool.workbench_hearth"))) _add_tool_btn(flow, Strings.t(&"tool.workbench_cremation_pyre"), - Color(0.30, 0.25, 0.20), + &"build_workbench_cremation_pyre", func() -> void: _activate(&"build_workbench_cremation_pyre", &"", Strings.t(&"tool.workbench_cremation_pyre"))) + # Quarry — must be painted on a stone outcrop (BigRockNode); world.gd + # rejects placements on plain ground. + _add_tool_btn(flow, Strings.t(&"tool.paint_quarry"), + &"paint_quarry", + func() -> void: _activate(&"paint_quarry", &"", Strings.t(&"tool.paint_quarry"))) + return box @@ -248,9 +255,9 @@ func _build_stockpile_tab() -> Control: var flow := _make_flow_grid() box.add_child(flow) - _add_tool_btn(flow, Strings.t(&"tool.stockpile_general"), Color(0.30, 0.60, 0.30), + _add_tool_btn(flow, Strings.t(&"tool.stockpile_general"), &"paint_stockpile", func() -> void: _activate(&"paint_stockpile", &"", Strings.t(&"tool.stockpile_general"))) - _add_tool_btn(flow, Strings.t(&"tool.graveyard"), Color(0.25, 0.20, 0.15), + _add_tool_btn(flow, Strings.t(&"tool.graveyard"), &"graveyard", func() -> void: _activate(&"graveyard", &"", Strings.t(&"tool.graveyard"))) return box @@ -291,10 +298,13 @@ func _make_flow_grid() -> GridContainer: return g -## Add a single tool button to `container`. Each button is a VBoxContainer of -## [ColorRect icon area + Label] wrapped in a Button so the whole cell is one -## touch target. -func _add_tool_btn(container: Control, label_text: String, icon_color: Color, callback: Callable) -> void: +const _THUMB_SCRIPT: Script = preload("res://scenes/ui/build_drawer_thumb.gd") + + +## Add a single tool button to `container`. The button is a VBoxContainer of +## [thumb preview + Label] wrapped in a Button so the whole cell is one touch +## target. `tool_id` drives the procedural preview shape (BuildDrawerThumb). +func _add_tool_btn(container: Control, label_text: String, tool_id: StringName, callback: Callable) -> void: var btn := Button.new() btn.custom_minimum_size = Vector2(BTN_SIZE, BTN_SIZE + LABEL_HEIGHT) btn.focus_mode = Control.FOCUS_NONE @@ -303,13 +313,16 @@ func _add_tool_btn(container: Control, label_text: String, icon_color: Color, ca vb.mouse_filter = Control.MOUSE_FILTER_IGNORE vb.add_theme_constant_override("separation", 2) - # Icon area — procedural colored rect (real sprites land with Phase 17 art pass). - var icon := ColorRect.new() - icon.color = icon_color - icon.custom_minimum_size = Vector2(BTN_SIZE - 8, BTN_SIZE - LABEL_HEIGHT - 8) - icon.size_flags_horizontal = Control.SIZE_SHRINK_CENTER - icon.mouse_filter = Control.MOUSE_FILTER_IGNORE - vb.add_child(icon) + # Procedural preview of the entity this tool builds. + var thumb := Control.new() + thumb.set_script(_THUMB_SCRIPT) + # Use .set() — the static type is Control (set_script doesn't refine it), + # but the runtime instance has the tool_id property from the script. + thumb.set("tool_id", tool_id) + thumb.custom_minimum_size = Vector2(BTN_SIZE - 8, BTN_SIZE - LABEL_HEIGHT - 8) + thumb.size_flags_horizontal = Control.SIZE_SHRINK_CENTER + thumb.mouse_filter = Control.MOUSE_FILTER_IGNORE + vb.add_child(thumb) # Label. var lbl := Label.new() diff --git a/scenes/ui/build_drawer_thumb.gd b/scenes/ui/build_drawer_thumb.gd new file mode 100644 index 0000000..d6c07d9 --- /dev/null +++ b/scenes/ui/build_drawer_thumb.gd @@ -0,0 +1,378 @@ +class_name BuildDrawerThumb extends Control +## Procedural preview thumbnail for one BuildDrawer tool button. +## +## Each tool_id dispatches to a small _draw call that renders a recognisable +## silhouette of the entity that tool builds. All shapes live inside a 40×40 +## box centred in this control's size — the parent button supplies the panel +## frame; this widget only paints the icon. +## +## Cheaper than instantiating live entity scenes into SubViewports; pure draw +## calls, no allocations, recomputes only when the button repaints. + +## Tool identifier — drives the shape dispatch in _draw. +var tool_id: StringName = &"" + + +func _draw() -> void: + var c := size / 2.0 + Vector2(0, -2) # slight upward bias for label clearance + # Soft shadow under every thumb (subtle depth cue against the parchment). + draw_circle(c + Vector2(0, 12), 14.0, Color(0, 0, 0, 0.08)) + var outline := Color(0.10, 0.07, 0.05, 0.85) + match tool_id: + &"build_wall_stone": + # Grey brick wall with staggered courses. + var brick := Color(0.62, 0.60, 0.58) + var brick_hi := Color(0.78, 0.76, 0.72) + var mortar := Color(0.30, 0.28, 0.26) + var r := Rect2(c.x - 16, c.y - 16, 32, 32) + draw_rect(r, brick) + # Three brick courses, staggered seams. + for i in 3: + var y: float = r.position.y + i * 11 + 4 + draw_line(Vector2(r.position.x, y), Vector2(r.end.x, y), mortar, 1.0) + # Vertical seams (staggered per row). + for i in 3: + var y: float = r.position.y + i * 11 + 4 + var stagger: float = 0.0 if i % 2 == 0 else 8.0 + var x_offs: Array[float] = [-8.0, 0.0, 8.0] + for x_off in x_offs: + var x: float = c.x + x_off + stagger + if x >= r.position.x and x <= r.end.x: + draw_line(Vector2(x, y - 11), Vector2(x, y), mortar, 1.0) + # Top edge highlight. + draw_rect(Rect2(r.position.x, r.position.y, r.size.x, 3), brick_hi) + draw_rect(r, outline, false, 1.0) + &"build_wall_wood": + # Brown wood-plank wall, 3 vertical planks. + var wood := Color(0.65, 0.45, 0.25) + var wood_dark := Color(0.42, 0.27, 0.12) + var r := Rect2(c.x - 16, c.y - 16, 32, 32) + draw_rect(r, wood) + draw_line(Vector2(c.x - 5.5, r.position.y), Vector2(c.x - 5.5, r.end.y), wood_dark, 1.0) + draw_line(Vector2(c.x + 5.5, r.position.y), Vector2(c.x + 5.5, r.end.y), wood_dark, 1.0) + # Knot details (small dots). + draw_circle(Vector2(c.x - 11, c.y - 6), 1.2, wood_dark) + draw_circle(Vector2(c.x + 1, c.y + 4), 1.2, wood_dark) + draw_rect(r, outline, false, 1.0) + &"build_floor_wood": + # Tan floorboard plan-view — 4 planks horizontal with grain. + var board := Color(0.78, 0.58, 0.32) + var board_dark := Color(0.50, 0.32, 0.16) + var r := Rect2(c.x - 16, c.y - 12, 32, 24) + draw_rect(r, board) + for i in 1.0: + pass # placeholder loop + draw_line(Vector2(r.position.x, c.y - 6), Vector2(r.end.x, c.y - 6), board_dark, 1.0) + draw_line(Vector2(r.position.x, c.y), Vector2(r.end.x, c.y), board_dark, 1.0) + draw_line(Vector2(r.position.x, c.y + 6), Vector2(r.end.x, c.y + 6), board_dark, 1.0) + draw_rect(r, outline, false, 1.0) + &"build_floor_stone": + # Grey paving stones — 2×2 grid with cross-mortar. + var stone := Color(0.65, 0.63, 0.60) + var mortar := Color(0.35, 0.33, 0.30) + var r := Rect2(c.x - 16, c.y - 12, 32, 24) + draw_rect(r, stone) + draw_line(Vector2(c.x, r.position.y), Vector2(c.x, r.end.y), mortar, 1.0) + draw_line(Vector2(r.position.x, c.y), Vector2(r.end.x, c.y), mortar, 1.0) + draw_rect(r, outline, false, 1.0) + &"build_door": + # Wooden door — rounded-top arch silhouette with a handle dot. + var door := Color(0.55, 0.35, 0.15) + var door_hi := Color(0.75, 0.50, 0.25) + var handle := Color(0.95, 0.80, 0.20) + # Body + var pts: PackedVector2Array = PackedVector2Array([ + Vector2(c.x - 10, c.y + 16), Vector2(c.x - 10, c.y - 6), + Vector2(c.x - 8, c.y - 12), Vector2(c.x, c.y - 16), + Vector2(c.x + 8, c.y - 12), Vector2(c.x + 10, c.y - 6), + Vector2(c.x + 10, c.y + 16), + ]) + draw_colored_polygon(pts, door) + # Inner plank seam. + draw_line(Vector2(c.x, c.y - 14), Vector2(c.x, c.y + 14), door_hi, 1.0) + # Handle. + draw_circle(Vector2(c.x + 5, c.y + 4), 1.5, handle) + draw_polyline(pts + PackedVector2Array([pts[0]]), outline, 1.0) + &"build_crate": + # Wooden crate — brown box with X cross-bracing on the front. + var wood := Color(0.65, 0.42, 0.18) + var wood_dark := Color(0.42, 0.27, 0.10) + var r := Rect2(c.x - 14, c.y - 12, 28, 24) + draw_rect(r, wood) + # Top edge bevel. + draw_rect(Rect2(r.position.x, r.position.y, r.size.x, 3), Color(0.85, 0.62, 0.30)) + # X cross-bracing. + draw_line(r.position, r.end, wood_dark, 2.0) + draw_line(Vector2(r.position.x, r.end.y), Vector2(r.end.x, r.position.y), wood_dark, 2.0) + draw_rect(r, outline, false, 1.0) + &"build_bed": + # Top-down bed view — wood frame, pillow top, blanket body. + var frame := Color(0.55, 0.35, 0.15) + var blanket := Color(0.55, 0.42, 0.78) + var pillow := Color(0.95, 0.93, 0.85) + var r := Rect2(c.x - 12, c.y - 14, 24, 28) + # Frame. + draw_rect(r, frame) + # Blanket inside (slightly inset). + draw_rect(Rect2(r.position.x + 2, r.position.y + 8, r.size.x - 4, r.size.y - 10), blanket) + # Pillow at top. + draw_rect(Rect2(r.position.x + 2, r.position.y + 2, r.size.x - 4, 5), pillow) + # Outline. + draw_rect(r, outline, false, 1.0) + &"build_torch": + # Wall torch — vertical brown shaft with orange flame above. + var shaft := Color(0.45, 0.28, 0.12) + var bracket := Color(0.35, 0.35, 0.38) + var flame_outer := Color(0.95, 0.40, 0.05) + var flame_inner := Color(1.00, 0.85, 0.30) + # Wall bracket + draw_rect(Rect2(c.x - 8, c.y + 6, 16, 4), bracket) + # Torch shaft. + draw_rect(Rect2(c.x - 2, c.y - 6, 4, 14), shaft) + # Flame teardrop. + draw_circle(Vector2(c.x, c.y - 10), 5.0, flame_outer) + draw_circle(Vector2(c.x, c.y - 12), 2.5, flame_inner) + # Smoke wisp. + draw_line(Vector2(c.x, c.y - 16), Vector2(c.x + 2, c.y - 20), Color(0.70, 0.70, 0.70, 0.6), 1.0) + &"build_workbench_carpenter": + # Wood bench with saw on top, two legs visible. + var plank_top := Color(0.78, 0.55, 0.30) + var plank_front := Color(0.55, 0.38, 0.22) + var leg := Color(0.32, 0.22, 0.10) + var saw_blade := Color(0.82, 0.82, 0.85) + var saw_handle := Color(0.55, 0.30, 0.15) + # Legs. + draw_rect(Rect2(c.x - 14, c.y, 3, 16), leg) + draw_rect(Rect2(c.x + 11, c.y, 3, 16), leg) + # Bench body. + draw_rect(Rect2(c.x - 16, c.y - 4, 32, 8), plank_front) + draw_rect(Rect2(c.x - 16, c.y - 8, 32, 4), plank_top) + # Saw blade. + draw_rect(Rect2(c.x - 4, c.y - 12, 12, 2), saw_blade) + draw_rect(Rect2(c.x + 8, c.y - 13, 4, 4), saw_handle) + draw_rect(Rect2(c.x - 16, c.y - 12, 32, 16), outline, false, 1.0) + &"build_workbench_smelter": + # Stone furnace with ember opening + smoke from chimney. + var stone := Color(0.55, 0.55, 0.55) + var stone_front := Color(0.42, 0.42, 0.43) + var ember := Color(0.98, 0.55, 0.10) + var ember_core := Color(1.00, 0.85, 0.30) + var chimney := Color(0.32, 0.30, 0.30) + var smoke := Color(0.75, 0.73, 0.70, 0.7) + # Stone body. + draw_rect(Rect2(c.x - 14, c.y - 8, 28, 22), stone_front) + draw_rect(Rect2(c.x - 14, c.y - 11, 28, 4), stone) + # Ember mouth. + draw_rect(Rect2(c.x - 7, c.y - 2, 14, 8), Color(0.18, 0.10, 0.06)) + draw_rect(Rect2(c.x - 5, c.y, 10, 4), ember) + draw_rect(Rect2(c.x - 3, c.y + 1, 6, 2), ember_core) + # Chimney + smoke. + draw_rect(Rect2(c.x + 4, c.y - 16, 5, 6), chimney) + draw_line(Vector2(c.x + 6, c.y - 18), Vector2(c.x + 5, c.y - 23), smoke, 1.5) + draw_rect(Rect2(c.x - 14, c.y - 11, 28, 25), outline, false, 1.0) + &"build_workbench_millstone": + # Wood frame + round grindstone wheel. + var frame_front := Color(0.42, 0.26, 0.12) + var frame_top := Color(0.55, 0.36, 0.18) + var wheel := Color(0.55, 0.53, 0.50) + var wheel_rim := Color(0.20, 0.18, 0.16) + var wheel_dark := Color(0.34, 0.32, 0.30) + # Frame. + draw_rect(Rect2(c.x - 14, c.y + 2, 28, 12), frame_front) + draw_rect(Rect2(c.x - 14, c.y - 1, 28, 5), frame_top) + # Wheel. + draw_circle(c + Vector2(0, -4), 11.0, wheel_rim) + draw_circle(c + Vector2(0, -4), 9.5, wheel) + # Front-face shadow (lower half). + draw_rect(Rect2(c.x - 9, c.y - 4, 18, 9), wheel_dark) + # Center pin. + draw_circle(c + Vector2(0, -4), 2.0, Color(0.18, 0.16, 0.14)) + draw_rect(Rect2(c.x - 14, c.y - 15, 28, 29), outline, false, 1.0) + &"build_workbench_hearth": + # Tall stone fireplace with wood mantle + flame. + var stone := Color(0.60, 0.58, 0.55) + var stone_dark := Color(0.42, 0.40, 0.38) + var mantle := Color(0.50, 0.34, 0.20) + var opening := Color(0.10, 0.05, 0.02) + var flame_outer := Color(0.95, 0.40, 0.05) + var flame_inner := Color(1.00, 0.85, 0.30) + var log_wood := Color(0.55, 0.32, 0.15) + # Stone surround. + draw_rect(Rect2(c.x - 14, c.y - 16, 28, 32), stone) + draw_line(Vector2(c.x - 14, c.y - 8), Vector2(c.x + 14, c.y - 8), stone_dark, 1.0) + # Mantle band. + draw_rect(Rect2(c.x - 14, c.y - 6, 28, 3), mantle) + # Opening. + draw_rect(Rect2(c.x - 9, c.y - 2, 18, 18), opening) + # Log. + draw_rect(Rect2(c.x - 6, c.y + 10, 12, 3), log_wood) + # Flame. + draw_rect(Rect2(c.x - 4, c.y + 4, 8, 6), flame_outer) + draw_rect(Rect2(c.x - 3, c.y + 1, 6, 4), flame_outer) + draw_rect(Rect2(c.x - 2, c.y + 4, 4, 4), flame_inner) + draw_rect(Rect2(c.x - 14, c.y - 16, 28, 32), outline, false, 1.0) + &"build_workbench_cremation_pyre": + # Charred wood pile with ember glow + ash smoke. + var base_top := Color(0.30, 0.22, 0.12) + var base_front := Color(0.22, 0.15, 0.08) + var ember := Color(0.95, 0.45, 0.10) + var ash_grey := Color(0.70, 0.68, 0.65, 0.7) + draw_rect(Rect2(c.x - 14, c.y - 4, 28, 16), base_front) + draw_rect(Rect2(c.x - 14, c.y - 8, 28, 4), base_top) + draw_rect(Rect2(c.x - 9, c.y + 2, 18, 4), ember) + # Smoke wisps. + draw_rect(Rect2(c.x - 5, c.y - 14, 2, 6), ash_grey) + draw_rect(Rect2(c.x + 1, c.y - 16, 2, 8), ash_grey) + draw_rect(Rect2(c.x + 5, c.y - 12, 2, 4), ash_grey) + draw_rect(Rect2(c.x - 14, c.y - 8, 28, 20), outline, false, 1.0) + &"paint_stockpile": + # Green tile with dashed boundary — a designated stockpile zone. + var fill := Color(0.35, 0.65, 0.30, 0.45) + var border := Color(0.20, 0.45, 0.15) + var r := Rect2(c.x - 14, c.y - 12, 28, 24) + draw_rect(r, fill) + # Dashed border: 4 dashes per side. + for i in 4: + draw_line(Vector2(r.position.x + i * 7, r.position.y), + Vector2(r.position.x + i * 7 + 4, r.position.y), border, 2.0) + draw_line(Vector2(r.position.x + i * 7, r.end.y), + Vector2(r.position.x + i * 7 + 4, r.end.y), border, 2.0) + for i in 3: + draw_line(Vector2(r.position.x, r.position.y + i * 8), + Vector2(r.position.x, r.position.y + i * 8 + 4), border, 2.0) + draw_line(Vector2(r.end.x, r.position.y + i * 8), + Vector2(r.end.x, r.position.y + i * 8 + 4), border, 2.0) + &"graveyard": + # Dark earth tile + grave cross marker. + var earth := Color(0.35, 0.28, 0.20) + var earth_hi := Color(0.50, 0.40, 0.28) + var cross := Color(0.78, 0.78, 0.76) + var r := Rect2(c.x - 14, c.y - 12, 28, 24) + draw_rect(r, earth) + draw_line(Vector2(r.position.x, r.position.y + 3), Vector2(r.end.x, r.position.y + 3), earth_hi, 1.0) + # Cross (gravestone marker). + draw_rect(Rect2(c.x - 1.5, c.y - 8, 3, 18), cross) + draw_rect(Rect2(c.x - 6, c.y - 4, 12, 3), cross) + draw_rect(r, outline, false, 1.0) + &"chop": + # Axe head + handle silhouette over a green target tile. + var grass := Color(0.35, 0.65, 0.30, 0.35) + var handle := Color(0.55, 0.35, 0.15) + var blade := Color(0.78, 0.80, 0.85) + var blade_dark := Color(0.45, 0.48, 0.52) + draw_rect(Rect2(c.x - 14, c.y - 12, 28, 24), grass) + # Handle (diagonal). + draw_line(Vector2(c.x - 8, c.y + 10), Vector2(c.x + 6, c.y - 8), handle, 3.0) + # Axe head. + var ax_pts: PackedVector2Array = PackedVector2Array([ + Vector2(c.x + 2, c.y - 12), Vector2(c.x + 12, c.y - 6), + Vector2(c.x + 8, c.y), Vector2(c.x - 2, c.y - 4), + ]) + draw_colored_polygon(ax_pts, blade) + draw_polyline(ax_pts + PackedVector2Array([ax_pts[0]]), blade_dark, 1.0) + &"mine": + # Pickaxe over a grey stone tile. + var stone := Color(0.62, 0.60, 0.58, 0.4) + var handle := Color(0.55, 0.35, 0.15) + var head := Color(0.48, 0.48, 0.52) + var head_dark := Color(0.28, 0.28, 0.32) + draw_rect(Rect2(c.x - 14, c.y - 12, 28, 24), stone) + # Handle. + draw_line(Vector2(c.x - 8, c.y + 10), Vector2(c.x + 6, c.y - 8), handle, 3.0) + # Two-pointed pickaxe head. + var pk_pts: PackedVector2Array = PackedVector2Array([ + Vector2(c.x - 6, c.y - 12), Vector2(c.x + 12, c.y - 6), + Vector2(c.x + 4, c.y - 2), Vector2(c.x, c.y - 6), + Vector2(c.x - 4, c.y - 4), + ]) + draw_colored_polygon(pk_pts, head) + draw_polyline(pk_pts + PackedVector2Array([pk_pts[0]]), head_dark, 1.0) + &"dig_grave": + # Shovel + earth mound. + var earth := Color(0.35, 0.25, 0.15) + var earth_hi := Color(0.55, 0.40, 0.25) + var handle := Color(0.55, 0.35, 0.15) + var blade := Color(0.55, 0.55, 0.60) + # Earth mound at bottom. + var mound: PackedVector2Array = PackedVector2Array([ + Vector2(c.x - 14, c.y + 12), Vector2(c.x + 14, c.y + 12), + Vector2(c.x + 8, c.y + 4), Vector2(c.x - 8, c.y + 4), + ]) + draw_colored_polygon(mound, earth) + draw_line(Vector2(c.x - 6, c.y + 6), Vector2(c.x + 6, c.y + 6), earth_hi, 1.0) + # Shovel handle. + draw_line(Vector2(c.x - 8, c.y + 10), Vector2(c.x + 4, c.y - 10), handle, 3.0) + # Shovel blade. + var sh_pts: PackedVector2Array = PackedVector2Array([ + Vector2(c.x + 3, c.y - 12), Vector2(c.x + 10, c.y - 10), + Vector2(c.x + 10, c.y - 4), Vector2(c.x + 4, c.y - 4), + ]) + draw_colored_polygon(sh_pts, blade) + &"no_roof": + # Open square with up-arrow (cancel-roof designation). + var sky := Color(0.55, 0.75, 0.95, 0.4) + var border := Color(0.20, 0.40, 0.65) + var arrow := Color(0.95, 0.95, 0.95) + # Square outline (dashed corners) representing the cell. + draw_rect(Rect2(c.x - 14, c.y - 12, 28, 24), sky) + # Corner brackets. + var corners: Array = [ + [Vector2(c.x - 14, c.y - 12), Vector2(1, 0), Vector2(0, 1)], + [Vector2(c.x + 14, c.y - 12), Vector2(-1, 0), Vector2(0, 1)], + [Vector2(c.x - 14, c.y + 12), Vector2(1, 0), Vector2(0, -1)], + [Vector2(c.x + 14, c.y + 12), Vector2(-1, 0), Vector2(0, -1)], + ] + for corner in corners: + var pos: Vector2 = corner[0] + var dx: Vector2 = corner[1] + var dy: Vector2 = corner[2] + draw_line(pos, pos + dx * 5.0, border, 2.0) + draw_line(pos, pos + dy * 5.0, border, 2.0) + # Up arrow centred. + draw_line(Vector2(c.x, c.y + 6), Vector2(c.x, c.y - 6), arrow, 2.0) + draw_line(Vector2(c.x, c.y - 6), Vector2(c.x - 4, c.y - 2), arrow, 2.0) + draw_line(Vector2(c.x, c.y - 6), Vector2(c.x + 4, c.y - 2), arrow, 2.0) + &"paint_quarry": + # Stone block on wood frame + small pile of cut stones beside. + var frame := Color(0.42, 0.26, 0.12) + var frame_top := Color(0.55, 0.36, 0.18) + var stone := Color(0.65, 0.63, 0.60) + var stone_hi := Color(0.82, 0.80, 0.76) + var stone_dark := Color(0.32, 0.30, 0.28) + var chisel_steel := Color(0.78, 0.80, 0.85) + # Wood frame base. + draw_rect(Rect2(c.x - 14, c.y + 6, 28, 8), frame) + draw_rect(Rect2(c.x - 14, c.y + 3, 28, 3), frame_top) + # Large stone block on top. + draw_rect(Rect2(c.x - 10, c.y - 8, 14, 11), stone) + draw_rect(Rect2(c.x - 10, c.y - 8, 14, 3), stone_hi) + # Chisel marks (3 short dark lines on the stone face). + draw_line(Vector2(c.x - 6, c.y - 3), Vector2(c.x - 4, c.y - 3), stone_dark, 1.0) + draw_line(Vector2(c.x - 2, c.y - 3), Vector2(c.x, c.y - 3), stone_dark, 1.0) + draw_line(Vector2(c.x + 2, c.y - 3), Vector2(c.x - 0, c.y - 3), stone_dark, 1.0) + # Pile of cut stones beside the block. + draw_rect(Rect2(c.x + 6, c.y, 4, 3), stone) + draw_rect(Rect2(c.x + 8, c.y - 3, 3, 3), stone_hi) + # Chisel tool on top of block. + draw_rect(Rect2(c.x - 4, c.y - 12, 2, 4), chisel_steel) + draw_rect(Rect2(c.x - 5, c.y - 13, 4, 2), frame_top) + &"plant_tree": + # Green sapling sprout rising from dirt — signals manual tree planting. + var dirt := Color(0.45, 0.32, 0.18) + var dirt_hi := Color(0.62, 0.46, 0.28) + var stem := Color(0.35, 0.22, 0.10) + var leaf_a := Color(0.30, 0.65, 0.20) + var leaf_b := Color(0.25, 0.55, 0.18) + # Dirt base rectangle. + draw_rect(Rect2(c.x - 14, c.y + 2, 28, 14), dirt) + draw_line(Vector2(c.x - 14, c.y + 5), Vector2(c.x + 14, c.y + 5), dirt_hi, 1.0) + # Stem. + draw_line(Vector2(c.x, c.y + 2), Vector2(c.x, c.y - 8), stem, 2.0) + # Three leaf dots at top of stem. + draw_circle(Vector2(c.x, c.y - 10), 4.0, leaf_a) + draw_circle(Vector2(c.x - 5, c.y - 6), 3.0, leaf_b) + draw_circle(Vector2(c.x + 5, c.y - 5), 3.0, leaf_b) + _: + # Unknown tool — small grey placeholder. + draw_rect(Rect2(c.x - 12, c.y - 12, 24, 24), Color(0.50, 0.50, 0.50)) + draw_rect(Rect2(c.x - 12, c.y - 12, 24, 24), outline, false, 1.0) diff --git a/scenes/ui/build_drawer_thumb.gd.uid b/scenes/ui/build_drawer_thumb.gd.uid new file mode 100644 index 0000000..6d94197 --- /dev/null +++ b/scenes/ui/build_drawer_thumb.gd.uid @@ -0,0 +1 @@ +uid://w802akkpbc6l diff --git a/scenes/ui/inspect_tooltip.gd b/scenes/ui/inspect_tooltip.gd index c3a716d..da793cd 100644 --- a/scenes/ui/inspect_tooltip.gd +++ b/scenes/ui/inspect_tooltip.gd @@ -264,12 +264,36 @@ func _describe_wolf(w) -> String: func _describe_tree(t) -> String: + # Growth stage label. + var stage: int = int(t.get("growth_stage")) if "growth_stage" in t else 3 + var stage_key_map: Array[StringName] = [ + &"tree.stage.sapling", + &"tree.stage.young", + &"tree.stage.growing", + &"tree.stage.mature", + ] + var stage_label: String = Strings.t(stage_key_map[clamp(stage, 0, 3)]) + + # Pending-plant ghost indicator. + var pending: bool = bool(t.get("pending_plant")) if "pending_plant" in t else false + if pending: + return "[b]%s[/b]\n[color=#aaa]awaiting pawn[/color]" % stage_label + + # Growth progress for sub-mature trees. + var is_mature: bool = (stage >= 3) + if not is_mature: + var progress: int = int(t.get("growth_progress")) if "growth_progress" in t else 0 + var stage_ticks: int = int(t.get("STAGE_TICKS")) if "STAGE_TICKS" in t else 1 + var pct_grow: int = int(100.0 * float(progress) / float(max(stage_ticks, 1))) + return "[b]%s[/b]\nGrowing %d%%" % [stage_label, pct_grow] + + # Mature tree — show chop progress if any. var pct: int = int(100.0 * float(t.chop_progress) / float(t.CHOP_TICKS)) var designated: bool = bool(t.get("chop_designated")) var tag := " · [color=#fc6]marked[/color]" if designated else "" if pct > 0: - return "[b]Tree[/b]\nChop %d%%%s" % [pct, tag] - return "[b]Tree[/b]%s" % tag + return "[b]%s[/b]\nChop %d%%%s" % [stage_label, pct, tag] + return "[b]%s[/b]%s" % [stage_label, tag] func _describe_rock(r) -> String: diff --git a/scenes/ui/medieval_theme.gd b/scenes/ui/medieval_theme.gd new file mode 100644 index 0000000..afecbf7 --- /dev/null +++ b/scenes/ui/medieval_theme.gd @@ -0,0 +1,198 @@ +class_name MedievalTheme extends RefCounted +## Builds a Theme resource implementing the "Medieval warm" palette: +## Panel: tan (198, 168, 128) +## Border: dark wood (90, 55, 30) +## Button: parchment (230, 210, 170), hover lighter, pressed inset shadow +## Text: ink black with off-white on dark panels +## +## Applied on Main scene root so it cascades to every Control descendant. +## Per-panel overrides are still possible — only nodes that don't explicitly +## override a style pick up the global Theme. + +# ── palette ────────────────────────────────────────────────────────────────── +const C_PANEL := Color(0.776, 0.659, 0.502) # tan +const C_PANEL_DARK := Color(0.353, 0.216, 0.118) # dark wood +const C_BUTTON := Color(0.902, 0.824, 0.667) # parchment +const C_BUTTON_HOV := Color(0.961, 0.902, 0.769) # warm parchment +const C_BUTTON_PRESS:= Color(0.776, 0.694, 0.529) # pressed shadow +const C_BUTTON_DIS := Color(0.690, 0.620, 0.510) # disabled +const C_INK := Color(0.106, 0.078, 0.039) +const C_INK_DIM := Color(0.353, 0.275, 0.196) +const C_ACCENT := Color(0.580, 0.180, 0.110) # wax-seal red, for selected tabs +const C_ACCENT_GOLD := Color(0.831, 0.620, 0.149) + + +static func build() -> Theme: + var theme := Theme.new() + # Default font size — small UI, but legible on phone. + theme.default_font_size = 14 + + # ── Button (drives OptionButton + MenuButton too) ───────────────────────── + theme.set_stylebox("normal", "Button", _btn_box(C_BUTTON, false)) + theme.set_stylebox("hover", "Button", _btn_box(C_BUTTON_HOV, false)) + theme.set_stylebox("pressed", "Button", _btn_box(C_BUTTON_PRESS, true)) + theme.set_stylebox("disabled", "Button", _btn_box(C_BUTTON_DIS, false)) + theme.set_stylebox("focus", "Button", _focus_box()) + theme.set_color("font_color", "Button", C_INK) + theme.set_color("font_hover_color", "Button", C_INK) + theme.set_color("font_pressed_color", "Button", C_INK) + theme.set_color("font_disabled_color", "Button", C_INK_DIM) + theme.set_constant("h_separation", "Button", 6) + + # OptionButton inherits Button styling automatically via class fallback in + # Godot, but we set explicit copies in case overrides land. + theme.set_stylebox("normal", "OptionButton", _btn_box(C_BUTTON, false)) + theme.set_stylebox("hover", "OptionButton", _btn_box(C_BUTTON_HOV, false)) + theme.set_stylebox("pressed", "OptionButton", _btn_box(C_BUTTON_PRESS, true)) + theme.set_stylebox("focus", "OptionButton", _focus_box()) + theme.set_color("font_color", "OptionButton", C_INK) + + # ── CheckBox ────────────────────────────────────────────────────────────── + theme.set_color("font_color", "CheckBox", C_INK) + theme.set_color("font_hover_color", "CheckBox", C_INK) + theme.set_color("font_pressed_color", "CheckBox", C_INK) + + # ── PanelContainer ──────────────────────────────────────────────────────── + theme.set_stylebox("panel", "PanelContainer", _panel_box()) + + # ── Panel (raw) — same look as PanelContainer ───────────────────────────── + theme.set_stylebox("panel", "Panel", _panel_box()) + + # ── PopupMenu (recipe picker) ───────────────────────────────────────────── + theme.set_stylebox("panel", "PopupMenu", _panel_box()) + theme.set_stylebox("hover", "PopupMenu", _btn_box(C_BUTTON_HOV, false)) + theme.set_color("font_color", "PopupMenu", C_INK) + theme.set_color("font_hover_color", "PopupMenu", C_INK) + + # ── Label — defaults to ink-on-tan; per-label modulate still works ──────── + theme.set_color("font_color", "Label", C_INK) + + # ── SpinBox / LineEdit ──────────────────────────────────────────────────── + theme.set_stylebox("normal", "LineEdit", _btn_box(C_BUTTON, true)) + theme.set_stylebox("focus", "LineEdit", _focus_box()) + theme.set_color("font_color", "LineEdit", C_INK) + theme.set_color("caret_color", "LineEdit", C_INK) + + # ── Slider (audio sliders in SettingsMenu) ──────────────────────────────── + theme.set_stylebox("slider", "HSlider", _slider_track()) + theme.set_stylebox("grabber_area", "HSlider", _slider_fill()) + theme.set_stylebox("grabber_area_highlight", "HSlider", _slider_fill()) + + # ── ScrollContainer scrollbar ───────────────────────────────────────────── + theme.set_stylebox("scroll", "VScrollBar", _scrollbar_track()) + theme.set_stylebox("grabber", "VScrollBar", _btn_box(C_PANEL_DARK, false)) + theme.set_stylebox("grabber_pressed","VScrollBar", _btn_box(C_PANEL_DARK, true)) + + # ── HSeparator / VSeparator (used between bill rows in WorkbenchPanel) ──── + var sep := StyleBoxFlat.new() + sep.bg_color = C_PANEL_DARK + sep.content_margin_top = 1 + sep.content_margin_bottom = 1 + theme.set_stylebox("separator", "HSeparator", sep) + theme.set_stylebox("separator", "VSeparator", sep) + + return theme + + +# ── helpers ────────────────────────────────────────────────────────────────── + +static func _btn_box(fill: Color, pressed: bool) -> StyleBoxFlat: + var s := StyleBoxFlat.new() + s.bg_color = fill + s.border_color = C_PANEL_DARK + s.border_width_left = 1 + s.border_width_right = 1 + s.border_width_top = 1 + s.border_width_bottom = 1 + s.corner_radius_top_left = 4 + s.corner_radius_top_right = 4 + s.corner_radius_bottom_left = 4 + s.corner_radius_bottom_right = 4 + if pressed: + # Inset shadow on top + left (pressed-in look). + s.shadow_color = Color(0, 0, 0, 0.35) + s.shadow_size = 0 + # Visually offset content slightly down/right when pressed. + s.content_margin_top = 4 + s.content_margin_left = 7 + s.content_margin_right = 5 + s.content_margin_bottom = 2 + else: + # Subtle drop shadow for depth. + s.shadow_color = Color(0, 0, 0, 0.20) + s.shadow_size = 2 + s.shadow_offset = Vector2(0, 1) + s.content_margin_top = 3 + s.content_margin_left = 6 + s.content_margin_right = 6 + s.content_margin_bottom = 3 + return s + + +static func _panel_box() -> StyleBoxFlat: + var s := StyleBoxFlat.new() + s.bg_color = C_PANEL + s.border_color = C_PANEL_DARK + s.border_width_left = 2 + s.border_width_right = 2 + s.border_width_top = 2 + s.border_width_bottom = 2 + s.corner_radius_top_left = 6 + s.corner_radius_top_right = 6 + s.corner_radius_bottom_left = 6 + s.corner_radius_bottom_right = 6 + s.shadow_color = Color(0, 0, 0, 0.35) + s.shadow_size = 4 + s.shadow_offset = Vector2(0, 2) + s.content_margin_top = 8 + s.content_margin_left = 10 + s.content_margin_right = 10 + s.content_margin_bottom = 8 + return s + + +static func _focus_box() -> StyleBoxFlat: + var s := StyleBoxFlat.new() + s.bg_color = Color(0, 0, 0, 0) # transparent + s.border_color = C_ACCENT_GOLD + s.border_width_left = 2 + s.border_width_right = 2 + s.border_width_top = 2 + s.border_width_bottom = 2 + s.corner_radius_top_left = 4 + s.corner_radius_top_right = 4 + s.corner_radius_bottom_left = 4 + s.corner_radius_bottom_right = 4 + return s + + +static func _slider_track() -> StyleBoxFlat: + var s := StyleBoxFlat.new() + s.bg_color = C_PANEL_DARK + s.corner_radius_top_left = 4 + s.corner_radius_top_right = 4 + s.corner_radius_bottom_left = 4 + s.corner_radius_bottom_right = 4 + s.content_margin_top = 4 + s.content_margin_bottom = 4 + return s + + +static func _slider_fill() -> StyleBoxFlat: + var s := StyleBoxFlat.new() + s.bg_color = C_ACCENT_GOLD + s.corner_radius_top_left = 4 + s.corner_radius_top_right = 4 + s.corner_radius_bottom_left = 4 + s.corner_radius_bottom_right = 4 + return s + + +static func _scrollbar_track() -> StyleBoxFlat: + var s := StyleBoxFlat.new() + s.bg_color = Color(C_PANEL_DARK.r, C_PANEL_DARK.g, C_PANEL_DARK.b, 0.3) + s.corner_radius_top_left = 3 + s.corner_radius_top_right = 3 + s.corner_radius_bottom_left = 3 + s.corner_radius_bottom_right = 3 + return s diff --git a/scenes/ui/medieval_theme.gd.uid b/scenes/ui/medieval_theme.gd.uid new file mode 100644 index 0000000..cb1f529 --- /dev/null +++ b/scenes/ui/medieval_theme.gd.uid @@ -0,0 +1 @@ +uid://c26o807ldrrrx diff --git a/scenes/ui/top_bar.gd b/scenes/ui/top_bar.gd index c78ee4a..d778e8d 100644 --- a/scenes/ui/top_bar.gd +++ b/scenes/ui/top_bar.gd @@ -44,6 +44,10 @@ var _log_btn: Button = null func _ready() -> void: + var button_row: HBoxContainer = get_node_or_null("Anchor/ButtonRow") + if button_row != null: + button_row.add_theme_constant_override("separation", 4) + pause_btn.text = Strings.t(&"speed.pause") normal_btn.text = Strings.t(&"speed.normal") fast_btn.text = Strings.t(&"speed.fast") @@ -152,8 +156,8 @@ func _add_build_btn() -> void: return var build_btn := Button.new() build_btn.name = "BuildBtn" - build_btn.text = Strings.t(&"ui.build") - build_btn.custom_minimum_size = Vector2(60, 48) + build_btn.text = "🔨" + build_btn.custom_minimum_size = Vector2(40, 40) build_btn.focus_mode = Control.FOCUS_NONE build_btn.pressed.connect(_on_build_pressed) button_row.add_child(build_btn) @@ -176,8 +180,8 @@ func _add_settings_btn() -> void: return var settings_btn := Button.new() settings_btn.name = "SettingsBtn" - settings_btn.text = Strings.t(&"ui.settings.btn") - settings_btn.custom_minimum_size = Vector2(80, 48) + settings_btn.text = "⚙" + settings_btn.custom_minimum_size = Vector2(40, 40) settings_btn.focus_mode = Control.FOCUS_NONE settings_btn.pressed.connect(_on_settings_pressed) button_row.add_child(settings_btn) @@ -201,16 +205,16 @@ func _add_work_log_btns() -> void: var work_btn := Button.new() work_btn.name = "WorkBtn" - work_btn.text = "Work" - work_btn.custom_minimum_size = Vector2(60, 48) + work_btn.text = "👷" + work_btn.custom_minimum_size = Vector2(40, 40) work_btn.focus_mode = Control.FOCUS_NONE work_btn.pressed.connect(_on_work_pressed) button_row.add_child(work_btn) _log_btn = Button.new() _log_btn.name = "LogBtn" - _log_btn.text = "Log" - _log_btn.custom_minimum_size = Vector2(60, 48) + _log_btn.text = "🔔" + _log_btn.custom_minimum_size = Vector2(40, 40) _log_btn.focus_mode = Control.FOCUS_NONE _log_btn.pressed.connect(_on_log_pressed) button_row.add_child(_log_btn) diff --git a/scenes/ui/top_bar.tscn b/scenes/ui/top_bar.tscn index ae00ee1..bb20de2 100644 --- a/scenes/ui/top_bar.tscn +++ b/scenes/ui/top_bar.tscn @@ -20,29 +20,33 @@ offset_bottom = 40.0 [node name="PauseBtn" type="Button" parent="Anchor/ButtonRow"] focus_mode = 0 +custom_minimum_size = Vector2(36, 40) text = "‖" [node name="NormalBtn" type="Button" parent="Anchor/ButtonRow"] focus_mode = 0 +custom_minimum_size = Vector2(36, 40) text = "1×" [node name="FastBtn" type="Button" parent="Anchor/ButtonRow"] focus_mode = 0 +custom_minimum_size = Vector2(36, 40) text = "5×" [node name="UltraBtn" type="Button" parent="Anchor/ButtonRow"] focus_mode = 0 +custom_minimum_size = Vector2(36, 40) text = "12×" [node name="SaveBtn" type="Button" parent="Anchor/ButtonRow"] focus_mode = 0 -custom_minimum_size = Vector2(48, 48) +custom_minimum_size = Vector2(40, 40) text = "💾" [node name="LoadBtn" type="Button" parent="Anchor/ButtonRow"] focus_mode = 0 -custom_minimum_size = Vector2(48, 48) -text = "Load" +custom_minimum_size = Vector2(40, 40) +text = "📂" [node name="ClockLabel" type="Label" parent="Anchor"] anchor_left = 0.5 diff --git a/scenes/ui/workbench_panel.gd b/scenes/ui/workbench_panel.gd new file mode 100644 index 0000000..0bfe0b5 --- /dev/null +++ b/scenes/ui/workbench_panel.gd @@ -0,0 +1,434 @@ +class_name WorkbenchPanel extends CanvasLayer +## Phase 17 — Right-side bottom-sheet workbench bill editor. +## +## Layer 18: same level as PawnDetailPanel (only one is visible at a time). +## Opens when EventBus.workbench_selected fires; closes on workbench_deselected +## or when a pawn is selected (mutual-exclusion with PawnDetailPanel). +## +## Refresh model (matches PawnDetailPanel): +## - Full UI rebuild: only on workbench_selected, add_bill, remove_bill. +## - Status-line refresh: every 5 sim ticks while open (_on_sim_tick). +## - Bill rows are NOT touched during refresh to preserve scroll + dropdown state. +## +## Touch targets: all interactive controls are at least 48×48 px. +## Background elements use MOUSE_FILTER_IGNORE so world taps pass through. + +const PANEL_WIDTH: int = 360 +const REFRESH_TICKS: int = 5 # update status line every N sim ticks +const LAYER: int = 18 + +# ── internal state ──────────────────────────────────────────────────────────── +var current_workbench: Workbench = null +var _tick_counter: int = 0 + +# ── node refs (built in _build_ui) ─────────────────────────────────────────── +var _panel: PanelContainer = null +var _wb_name_label: Label = null +var _close_btn: Button = null +var _status_label: Label = null +var _bills_vbox: VBoxContainer = null +var _add_btn: Button = null +var _no_bills_label: Label = null +var _recipe_popup: PopupMenu = null + + +func _ready() -> void: + layer = LAYER + + _build_ui() + _set_visible(false) + + EventBus.workbench_selected.connect(_on_workbench_selected) + EventBus.workbench_deselected.connect(_on_workbench_deselected) + EventBus.pawn_selected.connect(_on_pawn_selected) + EventBus.sim_tick.connect(_on_sim_tick) + + Audit.log("workbench_panel", "WorkbenchPanel ready (layer %d)" % layer) + + +func _exit_tree() -> void: + if EventBus.workbench_selected.is_connected(_on_workbench_selected): + EventBus.workbench_selected.disconnect(_on_workbench_selected) + if EventBus.workbench_deselected.is_connected(_on_workbench_deselected): + EventBus.workbench_deselected.disconnect(_on_workbench_deselected) + if EventBus.pawn_selected.is_connected(_on_pawn_selected): + EventBus.pawn_selected.disconnect(_on_pawn_selected) + if EventBus.sim_tick.is_connected(_on_sim_tick): + EventBus.sim_tick.disconnect(_on_sim_tick) + + +# ── UI construction ─────────────────────────────────────────────────────────── + +func _build_ui() -> void: + # Right-side sheet anchored to the right edge, full height. + _panel = PanelContainer.new() + _panel.name = "WorkbenchSheet" + _panel.anchor_left = 1.0 + _panel.anchor_right = 1.0 + _panel.anchor_top = 0.0 + _panel.anchor_bottom = 1.0 + _panel.offset_left = -PANEL_WIDTH + _panel.offset_right = 0.0 + _panel.offset_top = 0.0 + _panel.offset_bottom = 0.0 + _panel.mouse_filter = Control.MOUSE_FILTER_PASS + add_child(_panel) + + # Scrollable inner container so content survives small screens. + var scroll := ScrollContainer.new() + scroll.name = "Scroll" + scroll.set_anchors_preset(Control.PRESET_FULL_RECT) + scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED + _panel.add_child(scroll) + + var vbox := VBoxContainer.new() + vbox.name = "Content" + vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL + vbox.add_theme_constant_override("separation", 6) + scroll.add_child(vbox) + + # ── Header ──────────────────────────────────────────────────────────────── + var header := HBoxContainer.new() + header.name = "Header" + header.add_theme_constant_override("separation", 8) + vbox.add_child(header) + + _wb_name_label = Label.new() + _wb_name_label.name = "WorkbenchName" + _wb_name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _wb_name_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + _wb_name_label.mouse_filter = Control.MOUSE_FILTER_IGNORE + header.add_child(_wb_name_label) + + _close_btn = Button.new() + _close_btn.name = "CloseBtn" + _close_btn.text = Strings.t(&"ui.detail.close") + _close_btn.custom_minimum_size = Vector2(48, 48) + _close_btn.focus_mode = Control.FOCUS_NONE + _close_btn.pressed.connect(_on_close_pressed) + header.add_child(_close_btn) + + _add_separator(vbox) + + # ── Status line (live-refreshed) ────────────────────────────────────────── + _status_label = Label.new() + _status_label.name = "StatusLabel" + _status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + _status_label.mouse_filter = Control.MOUSE_FILTER_IGNORE + vbox.add_child(_status_label) + + _add_separator(vbox) + + # ── Bill list ───────────────────────────────────────────────────────────── + var bills_header := Label.new() + bills_header.text = Strings.t(&"ui.bill.no_bills_hint") + bills_header.name = "BillsHeader" + bills_header.mouse_filter = Control.MOUSE_FILTER_IGNORE + # Header is just section spacing; actual content is in _bills_vbox below. + # Repurpose this as the "no bills" hint — hidden when bills exist. + _no_bills_label = bills_header + vbox.add_child(_no_bills_label) + + _bills_vbox = VBoxContainer.new() + _bills_vbox.name = "BillList" + _bills_vbox.add_theme_constant_override("separation", 8) + _bills_vbox.mouse_filter = Control.MOUSE_FILTER_IGNORE + vbox.add_child(_bills_vbox) + + _add_separator(vbox) + + # ── Add-bill footer ─────────────────────────────────────────────────────── + var footer := HBoxContainer.new() + footer.name = "Footer" + footer.add_theme_constant_override("separation", 8) + vbox.add_child(footer) + + _add_btn = Button.new() + _add_btn.name = "AddBillBtn" + _add_btn.text = Strings.t(&"ui.bill.add_button") + _add_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _add_btn.custom_minimum_size = Vector2(0, 48) + _add_btn.focus_mode = Control.FOCUS_NONE + _add_btn.pressed.connect(_on_add_bill_pressed) + footer.add_child(_add_btn) + + # PopupMenu for recipe selection — populated lazily in _on_add_bill_pressed. + _recipe_popup = PopupMenu.new() + _recipe_popup.name = "RecipePopup" + _recipe_popup.id_pressed.connect(_on_recipe_chosen) + _panel.add_child(_recipe_popup) + + +func _add_separator(parent: VBoxContainer) -> void: + var sep := HSeparator.new() + sep.mouse_filter = Control.MOUSE_FILTER_IGNORE + parent.add_child(sep) + + +func _clear_children(node: Node) -> void: + for child in node.get_children(): + child.queue_free() + + +# ── event handlers ──────────────────────────────────────────────────────────── + +func _on_workbench_selected(wb: Workbench) -> void: + current_workbench = wb + _tick_counter = 0 + _wb_name_label.text = wb.label_text + _refresh_status() + _populate_bills() + _set_visible(true) + Audit.log("workbench_panel", "opened for %s" % wb.label_text) + + +func _on_workbench_deselected() -> void: + current_workbench = null + _set_visible(false) + Audit.log("workbench_panel", "closed (deselected)") + + +func _on_pawn_selected(_pawn) -> void: + # Mutual exclusion: pawn panel and workbench panel are never both open. + if current_workbench == null: + return + current_workbench = null + _set_visible(false) + EventBus.workbench_deselected.emit() + Audit.log("workbench_panel", "closed (pawn selected)") + + +func _on_close_pressed() -> void: + current_workbench = null + _set_visible(false) + EventBus.workbench_deselected.emit() + Audit.log("workbench_panel", "closed (X button)") + + +func _on_sim_tick(_tick_number: int) -> void: + if current_workbench == null or not _panel.visible: + return + if not is_instance_valid(current_workbench): + current_workbench = null + _set_visible(false) + return + _tick_counter += 1 + if _tick_counter >= REFRESH_TICKS: + _tick_counter = 0 + _refresh_status() + + +# ── status-line refresh (called every REFRESH_TICKS, NOT rebuild) ───────────── + +func _refresh_status() -> void: + if current_workbench == null: + return + var cb = current_workbench.current_bill + if cb != null and cb.recipe != null: + var work_ticks: int = cb.recipe.work_ticks + _status_label.text = "%s: %s %d/%d" % [ + Strings.t(&"ui.workbench.current_bill"), + cb.recipe.display_name(), + current_workbench.current_work_progress, + work_ticks + ] + else: + _status_label.text = Strings.t(&"ui.workbench.idle") + + +# ── bill list population (called on open / add / remove only) ───────────────── + +func _populate_bills() -> void: + if current_workbench == null: + return + + _clear_children(_bills_vbox) + + var has_bills: bool = current_workbench.bills.size() > 0 + _no_bills_label.visible = not has_bills + + for bill in current_workbench.bills: + _bills_vbox.add_child(_make_bill_row(bill)) + + # Enable/disable the add button based on whether any filtered recipes exist. + var filtered: Array[Recipe] = _filtered_recipes() + _add_btn.disabled = filtered.is_empty() + + +## Build and return a VBoxContainer widget for a single bill. +## All controls mutate bill fields directly; remove triggers _populate_bills(). +func _make_bill_row(bill: Bill) -> VBoxContainer: + var row_vbox := VBoxContainer.new() + row_vbox.add_theme_constant_override("separation", 4) + row_vbox.mouse_filter = Control.MOUSE_FILTER_IGNORE + + # Row 1: recipe name (bold via theme; we use a plain Label — theme handles weight). + var name_lbl := Label.new() + name_lbl.text = bill.recipe.display_name() if bill.recipe != null else "???" + name_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE + row_vbox.add_child(name_lbl) + + # Row 2: mode OptionButton (Forever / Do X times / Do until X). + var mode_row := HBoxContainer.new() + mode_row.add_theme_constant_override("separation", 6) + row_vbox.add_child(mode_row) + + var mode_lbl := Label.new() + mode_lbl.text = "Mode:" + mode_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE + mode_lbl.custom_minimum_size = Vector2(40, 0) + mode_row.add_child(mode_lbl) + + var mode_btn := OptionButton.new() + mode_btn.add_item(Strings.t(&"ui.bill.mode_forever"), Bill.Mode.FOREVER) + mode_btn.add_item(Strings.t(&"ui.bill.mode_count"), Bill.Mode.COUNT) + mode_btn.add_item(Strings.t(&"ui.bill.mode_until_n"), Bill.Mode.UNTIL_N) + mode_btn.selected = bill.mode as int + mode_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL + mode_btn.focus_mode = Control.FOCUS_NONE + mode_btn.custom_minimum_size = Vector2(0, 48) + # Capture bill reference in closure; repopulate on mode change so conditional + # rows (count spinner, done label) appear/disappear correctly. + mode_btn.item_selected.connect(func(idx: int) -> void: + bill.mode = idx as Bill.Mode + Audit.log("workbench_ui", "%s: bill mode → %d" % [current_workbench.label_text, idx]) + # Defer the rebuild — we must NOT free mode_btn while its item_selected + # signal is still emitting (instant crash). call_deferred runs the + # repopulate after the signal frame completes. + call_deferred("_populate_bills") + ) + mode_row.add_child(mode_btn) + + # Row 3 (conditional): SpinBox for target_count. Shown when mode != FOREVER. + if bill.mode != Bill.Mode.FOREVER: + var count_row := HBoxContainer.new() + count_row.add_theme_constant_override("separation", 6) + row_vbox.add_child(count_row) + + var count_lbl := Label.new() + count_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE + count_lbl.custom_minimum_size = Vector2(80, 0) + if bill.mode == Bill.Mode.COUNT: + count_lbl.text = Strings.t(&"ui.bill.target") + else: + count_lbl.text = Strings.t(&"ui.bill.until_count") + count_row.add_child(count_lbl) + + var spin := SpinBox.new() + spin.min_value = 1 + spin.max_value = 999 + spin.step = 1 + spin.value = max(1, bill.target_count) + spin.size_flags_horizontal = Control.SIZE_EXPAND_FILL + spin.focus_mode = Control.FOCUS_NONE + spin.value_changed.connect(func(v: float) -> void: + bill.target_count = int(v) + Audit.log("workbench_ui", "%s: bill target_count → %d" % [current_workbench.label_text, bill.target_count]) + ) + count_row.add_child(spin) + + # Row 4 (COUNT only): "Done: X/Y" progress label. + if bill.mode == Bill.Mode.COUNT: + var done_lbl := Label.new() + done_lbl.text = "%s: %d/%d" % [Strings.t(&"ui.bill.completed"), bill.completed_count, bill.target_count] + done_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE + row_vbox.add_child(done_lbl) + + # Row 5: pause CheckBox. + var pause_check := CheckBox.new() + pause_check.text = Strings.t(&"ui.bill.pause") + pause_check.button_pressed = bill.paused + pause_check.focus_mode = Control.FOCUS_NONE + pause_check.custom_minimum_size = Vector2(0, 40) + pause_check.toggled.connect(func(on: bool) -> void: + bill.paused = on + Audit.log("workbench_ui", "%s: bill paused → %s" % [current_workbench.label_text, str(on)]) + ) + row_vbox.add_child(pause_check) + + # Row 6: "Remove" button, right-aligned via HSpacer + HBox. + var remove_row := HBoxContainer.new() + row_vbox.add_child(remove_row) + + var spacer := Control.new() + spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL + spacer.mouse_filter = Control.MOUSE_FILTER_IGNORE + remove_row.add_child(spacer) + + var remove_btn := Button.new() + remove_btn.text = Strings.t(&"ui.bill.remove") + remove_btn.custom_minimum_size = Vector2(80, 40) + remove_btn.focus_mode = Control.FOCUS_NONE + remove_btn.pressed.connect(func() -> void: + if current_workbench == null: + return + current_workbench.remove_bill(bill) + Audit.log("workbench_ui", "%s: bill removed — recipe '%s'" % [ + current_workbench.label_text, + bill.recipe.id if bill.recipe != null else "null" + ]) + # Defer — same reason as mode_btn: don't free this button mid-emit. + call_deferred("_populate_bills") + ) + remove_row.add_child(remove_btn) + + # Thin separator below each bill row for visual grouping. + var sep := HSeparator.new() + sep.mouse_filter = Control.MOUSE_FILTER_IGNORE + row_vbox.add_child(sep) + + return row_vbox + + +# ── add-bill popup ──────────────────────────────────────────────────────────── + +func _on_add_bill_pressed() -> void: + if current_workbench == null: + return + + var recipes: Array[Recipe] = _filtered_recipes() + if recipes.is_empty(): + return + + _recipe_popup.clear() + for i in recipes.size(): + _recipe_popup.add_item(recipes[i].display_name(), i) + + # Position the popup just above the add button. + var btn_rect: Rect2 = _add_btn.get_global_rect() + _recipe_popup.position = Vector2i(int(btn_rect.position.x), int(btn_rect.position.y) - _recipe_popup.size.y - 4) + _recipe_popup.popup() + + +func _on_recipe_chosen(id: int) -> void: + if current_workbench == null: + return + var recipes: Array[Recipe] = _filtered_recipes() + if id < 0 or id >= recipes.size(): + return + var picked: Recipe = recipes[id] + var b := Bill.new() + b.recipe = picked + b.mode = Bill.Mode.FOREVER + current_workbench.add_bill(b) + Audit.log("workbench_ui", "%s: bill added — recipe '%s'" % [current_workbench.label_text, picked.id]) + _populate_bills() + + +## Returns all catalog recipes whose required_skill matches the workbench's +## accepted_skill. Returns an empty array when no workbench is set. +func _filtered_recipes() -> Array[Recipe]: + if current_workbench == null: + return [] + var result: Array[Recipe] = [] + for r in RecipeCatalog.all(): + if r.required_skill == current_workbench.accepted_skill: + result.append(r) + return result + + +# ── visibility ──────────────────────────────────────────────────────────────── + +func _set_visible(v: bool) -> void: + if _panel != null: + _panel.visible = v diff --git a/scenes/ui/workbench_panel.gd.uid b/scenes/ui/workbench_panel.gd.uid new file mode 100644 index 0000000..5c26183 --- /dev/null +++ b/scenes/ui/workbench_panel.gd.uid @@ -0,0 +1 @@ +uid://bsdc4o12x1v41 diff --git a/scenes/world/designation.gd b/scenes/world/designation.gd index 5958268..b22ba5d 100644 --- a/scenes/world/designation.gd +++ b/scenes/world/designation.gd @@ -40,6 +40,10 @@ const TOOL_BUILD_WORKBENCH_HEARTH: StringName = &"build_workbench_hearth" const TOOL_BUILD_WORKBENCH_CREMATION_PYRE: StringName = &"build_workbench_cremation_pyre" # Phase 17 — Stockpile tab. const TOOL_PAINT_STOCKPILE: StringName = &"paint_stockpile" +# Tree planting — ghost sapling that ConstructionProvider will fulfil. +const TOOL_PLANT_TREE: StringName = &"plant_tree" +# Quarry — must paint on a BigRockNode tile; spawns a QuarryWorkbench ghost. +const TOOL_PAINT_QUARRY: StringName = &"paint_quarry" # ── tool → material override ───────────────────────────────────────────────── # For build_wall and build_floor the tool is shared but the material differs. @@ -73,6 +77,8 @@ const _ATLAS_BY_TOOL: Dictionary = { &"build_workbench_hearth": Vector2i(1, 0), &"build_workbench_cremation_pyre":Vector2i(3, 0), &"paint_stockpile": Vector2i(0, 0), + &"plant_tree": Vector2i(0, 0), # grass ghost — tinted green + &"paint_quarry": Vector2i(2, 0), # stone-grey ghost } # Placeholder source ID — mirrors World.PLACEHOLDER_SOURCE_ID. @@ -120,6 +126,8 @@ func set_active_tool(tool: StringName) -> void: TOOL_BUILD_WORKBENCH_MILLSTONE, TOOL_BUILD_WORKBENCH_HEARTH, TOOL_BUILD_WORKBENCH_CREMATION_PYRE, TOOL_PAINT_STOCKPILE, + TOOL_PLANT_TREE, + TOOL_PAINT_QUARRY, ], "Designation.set_active_tool: unknown tool '%s'" % tool ) diff --git a/scenes/world/selection.gd b/scenes/world/selection.gd index ec04c64..dbd0aca 100644 --- a/scenes/world/selection.gd +++ b/scenes/world/selection.gd @@ -14,6 +14,9 @@ const CLICK_MAX_DURATION_MS: int = 300 var _pathfinder: Pathfinder = null var _selected_pawn: Pawn = null +## Currently selected workbench, or null. Mutually exclusive with _selected_pawn — +## selecting one clears the other (see _select / _select_workbench). +var _selected_workbench: Workbench = null var _camera = null # Camera2D (CameraRig) — set via bind_camera(); duck-typed to avoid circular preload # When Designation paint mode is active this flag is raised by Designation so @@ -62,23 +65,32 @@ func _unhandled_input(event: InputEvent) -> void: # ── Keyboard: Escape → deselect (lowest-priority; consumed last) ───────────── # Designation._input handles Escape first; panels handle it in _unhandled_input # before reaching here. If we still see it and have a selection, consume it. - if event.is_action_pressed("cancel") and _selected_pawn != null: - _deselect() - get_viewport().set_input_as_handled() - Audit.log("selection", "escape: deselected") - return + if event.is_action_pressed("cancel"): + if _selected_pawn != null: + _deselect() + get_viewport().set_input_as_handled() + Audit.log("selection", "escape: deselected pawn") + return + if _selected_workbench != null: + _deselect_workbench() + get_viewport().set_input_as_handled() + Audit.log("selection", "escape: deselected workbench") + return # ── Mouse: only handle button events below ─────────────────────────────────── if not (event is InputEventMouseButton): return - # ── Right-click: cancel designation (if active) or deselect pawn ───────────── + # ── Right-click: cancel designation (if active) or deselect pawn / workbench ── if event.button_index == MOUSE_BUTTON_RIGHT and event.pressed: # Designation cancellation is handled by Designation._input; if we see - # this right-click, no designation was active. Deselect any selected pawn. + # this right-click, no designation was active. Deselect whatever is selected. if _selected_pawn != null: _deselect() get_viewport().set_input_as_handled() + elif _selected_workbench != null: + _deselect_workbench() + get_viewport().set_input_as_handled() return if event.button_index != MOUSE_BUTTON_LEFT: @@ -114,14 +126,23 @@ func _handle_click(screen_pos: Vector2) -> void: floori(world_pos.y / float(Pawn.TILE_SIZE_PX)), ) - # Click on a pawn → select. + # Click on a pawn → select. Pawn wins over workbench when they share a tile + # (a pawn working at a bench is selectable; tap empty bench tile to inspect bills). var hit_pawn: Pawn = World.pawn_at_tile(tile) if hit_pawn != null: _select(hit_pawn) return - # Empty tile with no current selection → no-op. + # Click on a workbench → open the bill-editor panel. + var hit_workbench = World.workbench_at_tile(tile) + if hit_workbench != null: + _select_workbench(hit_workbench) + return + + # Empty tile with no current pawn selection → also clear any workbench selection. if _selected_pawn == null: + if _selected_workbench != null: + _deselect_workbench() return # Empty walkable tile with a selection → queue a forced job. Decision picks @@ -140,6 +161,9 @@ func _handle_click(screen_pos: Vector2) -> void: func _select(pawn: Pawn) -> void: if _selected_pawn == pawn: return + # Mutual exclusion with workbench selection: clear it before promoting pawn. + if _selected_workbench != null: + _deselect_workbench() if _selected_pawn != null: _selected_pawn.set_selected(false) EventBus.pawn_deselected.emit() @@ -159,6 +183,28 @@ func _deselect() -> void: _selected_pawn = null +## Select a workbench → opens the bill-editor panel via EventBus. +## Mutually exclusive with pawn selection: clears _selected_pawn first. +func _select_workbench(wb) -> void: + if _selected_workbench == wb: + return + if _selected_pawn != null: + _deselect() + if _selected_workbench != null: + EventBus.workbench_deselected.emit() + _selected_workbench = wb + EventBus.workbench_selected.emit(wb) + Audit.log("selection", "selected workbench %s at %s" % [wb.label_text, wb.tile]) + + +func _deselect_workbench() -> void: + if _selected_workbench == null: + return + Audit.log("selection", "deselected workbench %s" % _selected_workbench.label_text) + _selected_workbench = null + EventBus.workbench_deselected.emit() + + ## Cycle the selection forward (dir=1) or backward (dir=-1) through World.pawns. ## Wraps around. If no pawn currently selected, picks World.pawns[0]. ## Pans the camera to the newly selected pawn's tile. diff --git a/scenes/world/world.gd b/scenes/world/world.gd index 734cc1c..d36e2b0 100644 --- a/scenes/world/world.gd +++ b/scenes/world/world.gd @@ -1,8 +1,9 @@ extends Node2D ## Phase 4 world view. 80×80 TileMap, 6 layers, 3 pawns, full AI pipeline: ## RestProvider → ChopProvider → MineProvider → HaulingProvider → idle -## plus sample trees, rocks, and two stockpile zones with different priorities -## for the haul-cascade demo. +## plus sample trees and rocks. No pre-made stockpiles — items sit where they +## are produced until the player paints storage (a stockpile zone or builds +## a crate). This matches Rimworld parity: storage is a player decision. ## ## TileMap layer indices follow docs/architecture.md: ## 0 Terrain · 1 Floor · 2 Wall · 3 Designation · 4 Roof · 5 Fog @@ -31,6 +32,8 @@ const PAWN_SCENE: PackedScene = preload("res://scenes/pawn/pawn.tscn") const TREE_SCENE: PackedScene = preload("res://scenes/entities/tree.tscn") const ROCK_SCENE: PackedScene = preload("res://scenes/entities/rock.tscn") const BIG_ROCK_SCENE: PackedScene = preload("res://scenes/entities/big_rock.tscn") +const BIG_ROCK_NODE_SCENE: PackedScene = preload("res://scenes/entities/big_rock_node.tscn") +const QUARRY_WORKBENCH_SCENE: PackedScene = preload("res://scenes/entities/quarry_workbench.tscn") const STOCKPILE_SCENE: PackedScene = preload("res://scenes/world/stockpile_zone.tscn") const WALL_SCENE: PackedScene = preload("res://scenes/entities/wall.tscn") const FLOOR_SCENE: PackedScene = preload("res://scenes/entities/floor.tscn") @@ -59,10 +62,15 @@ const SAMPLE_PAWNS: Array[Dictionary] = [ ] # Phase 4 — sample harvestables. Trees clustered east, rocks south-east. +# Mix of 8 mature + 4 saplings so players see growth in action from day 1. const SAMPLE_TREES: Array[Vector2i] = [ Vector2i(58, 30), Vector2i(60, 31), Vector2i(62, 30), Vector2i(61, 33), Vector2i(63, 34), Vector2i(59, 35), + Vector2i(57, 28), Vector2i(64, 32), # 2 more mature + Vector2i(56, 36), Vector2i(65, 29), # 2 more mature ] +# The first 4 in SAMPLE_TREES_SAPLING are planted as saplings (stage 0). +const SAMPLE_TREES_SAPLING_COUNT: int = 4 const SAMPLE_ROCKS: Array[Vector2i] = [ Vector2i(60, 60), Vector2i(62, 60), Vector2i(63, 62), Vector2i(58, 62), ] @@ -74,9 +82,28 @@ const SAMPLE_BIG_ROCKS: Array[Vector2i] = [ Vector2i(56, 64), ] +# Permanent stone outcrops (BigRockNode). Scattered far from the cabin at +# (44, 22)..(51, 28) so the player has to scout / plan transport routes. +# Each is a 2×2 footprint that never depletes; player paints `paint_quarry` +# to build a QuarryWorkbench on it. +const SAMPLE_BIG_ROCK_NODES: Array[Vector2i] = [ + Vector2i(12, 30), # west, near map edge + Vector2i(68, 12), # north-east corner area + Vector2i(70, 60), # south-east corner +] + # HaulingProvider re-flow cadence — every 5 sim seconds at 1× (100 ticks). const HAUL_SWEEP_INTERVAL_TICKS: int = 100 +# WildGrowth — spontaneous sapling spawning on eligible grass tiles. +# 1200 ticks = 1 in-game hour at 20 Hz (20 ticks/s × 60 s/min = 1200 ticks/min, +# but 1 in-game minute = 20 ticks at 1× so 1 hour = 1200 ticks at 1×). +const WILD_GROWTH_INTERVAL: int = 3000 # ~2.5 in-game hours between attempts +const WILD_GROWTH_SPAWN_PROBABILITY: float = 0.12 # 12% chance per attempt +const MAP_TREE_LIMIT: int = 80 +# Rejection-sample attempts before giving up for this tick. +const WILD_GROWTH_MAX_ATTEMPTS: int = 10 + # Phase 11 — global darkness tint. Day = white, night = deep cool blue. # Driven by Clock.darkness_factor() (0..1) each sim tick. const NIGHT_TINT: Color = Color(0.20, 0.22, 0.40, 1.0) @@ -212,7 +239,10 @@ func _ready() -> void: _spawn_sample_pawns() _spawn_sample_harvestables() - _spawn_sample_stockpiles() + # No pre-made stockpiles: the player must paint their own storage. Items + # from chop/mine/crafting sit where they're produced until a player-made + # stockpile or crate exists for hauling. (2026-05-15 — _spawn_sample_stockpiles + # removed; was leftover Phase 4 acceptance scaffolding south-west of the cabin.) _seed_phase5_demo_buildings() # Phase 13 — pre-stamp the cabin walls + floors on the TileMap data layers # so RoomDetector can see a completed enclosure at boot without waiting for @@ -406,7 +436,7 @@ func _spawn_sample_pawns() -> void: World.register_pawn(p) -# ── Phase 4: harvestables + stockpile zones ───────────────────────────────── +# ── Phase 4: harvestables (stockpile-zone seeding removed 2026-05-15) ─────── func _spawn_sample_harvestables() -> void: # Untyped vars — Godot's class-name cache for class_name'd classes is @@ -415,11 +445,15 @@ func _spawn_sample_harvestables() -> void: # Boot seed auto-designates so the production-chain demo runs end-to-end # without requiring a player to paint chop/mine first. Real player-painted # trees / rocks still gate on chop_designated / mine_designated (Rimworld parity). - for t_tile in SAMPLE_TREES: + for i in SAMPLE_TREES.size(): var tree = TREE_SCENE.instantiate() add_child(tree) - tree.setup(t_tile) - tree.chop_designated = true + # First SAMPLE_TREES_SAPLING_COUNT trees spawn as saplings (stage 0) + # so the player can observe growth from day 1. The rest are mature. + var stage: int = HarvestableTree.STAGE_SAPLING if i < SAMPLE_TREES_SAPLING_COUNT else HarvestableTree.STAGE_MATURE + tree.setup(SAMPLE_TREES[i], stage) + if stage == HarvestableTree.STAGE_MATURE: + tree.chop_designated = true for r_tile in SAMPLE_ROCKS: var rock = ROCK_SCENE.instantiate() add_child(rock) @@ -430,8 +464,13 @@ func _spawn_sample_harvestables() -> void: add_child(big) big.setup(br_origin) big.mine_designated = true - Audit.log("world", "spawned %d trees + %d rocks + %d big rocks" % [ - SAMPLE_TREES.size(), SAMPLE_ROCKS.size(), SAMPLE_BIG_ROCKS.size() + # Permanent stone outcrops (never deplete; Quarry workbench built on them). + for node_origin in SAMPLE_BIG_ROCK_NODES: + var node = BIG_ROCK_NODE_SCENE.instantiate() + add_child(node) + node.setup(node_origin) + Audit.log("world", "spawned %d trees + %d rocks + %d big rocks + %d stone outcrops" % [ + SAMPLE_TREES.size(), SAMPLE_ROCKS.size(), SAMPLE_BIG_ROCKS.size(), SAMPLE_BIG_ROCK_NODES.size() ]) @@ -443,8 +482,8 @@ func _seed_phase5_demo_buildings() -> void: # • Perimeter walls (skipping the door slot) # • Door at (47, 28) — middle of the south wall # • Wood floor across the 6×5 interior (rows 23..27) - # • One pre-built crate inside (north-east corner of the interior) - # • Two stockpile-target crates outside (Phase 4 hauling target) + # • One pre-built crate inside (north-east corner of the interior — the + # cabin's starting amenity; player paints additional storage later) # Bumped from 8×6 → 8×7 so the north interior row (23) is free for the # bed sprites to extend into (beds are 1×2, anchored at the foot, head # extends one tile up). Previously the headboards clipped into the wall. @@ -486,17 +525,13 @@ func _seed_phase5_demo_buildings() -> void: while interior_crate.is_buildable(): interior_crate.on_build_tick() - # Two external stockpile-target crates south-west (Phase 4 haul destination). - var crate_tiles: Array[Vector2i] = [Vector2i(17, 60), Vector2i(18, 60)] - for t in crate_tiles: - var c: Crate = CRATE_SCENE.instantiate() - add_child(c) - c.setup(t) - while c.is_buildable(): - c.on_build_tick() + # (2026-05-15) Two external SW crates removed alongside _spawn_sample_stockpiles: + # they were Phase 4 haul-destination scaffolding, no longer needed now that + # the player paints their own storage. The interior cabin crate above stays + # as a starting amenity. - Audit.log("world", "phase 5 demo: %d walls + 1 door + %d floors queued; %d crates pre-built" % [ - wall_count, floor_count, 1 + crate_tiles.size() + Audit.log("world", "phase 5 demo: %d walls + 1 door + %d floors queued; 1 interior crate pre-built" % [ + wall_count, floor_count ]) # Phase 6 demo — two pre-built workbenches inside the cabin with bills. @@ -631,31 +666,6 @@ func _seed_phase5_demo_buildings() -> void: Audit.log("world", "phase 11 demo: %d torches pre-built inside cabin" % torch_tiles.size()) -func _spawn_sample_stockpiles() -> void: - # Two zones for the Phase 4 acceptance demo: - # - Zone A (north): wood-only filter, NORMAL priority (just a wood drop) - # - Zone B (south): wildcard, HIGH priority (the "watch wood flow upward" target) - # When the sweep runs, wood items in Zone A get re-marked for haul and - # eventually migrate to Zone B. - var zone_a: StockpileZone = STOCKPILE_SCENE.instantiate() - add_child(zone_a) - zone_a.region = Rect2i(15, 55, 4, 4) - zone_a.label = "Wood (Normal)" - zone_a.priority = StorageDestination.Priority.NORMAL - zone_a.accepted_types = [Item.TYPE_WOOD] as Array[StringName] - zone_a.queue_redraw() - - var zone_b: StockpileZone = STOCKPILE_SCENE.instantiate() - add_child(zone_b) - zone_b.region = Rect2i(15, 62, 4, 4) - zone_b.label = "Anything (High)" - zone_b.priority = StorageDestination.Priority.HIGH - zone_b.accepted_types = [] as Array[StringName] # wildcard - zone_b.queue_redraw() - - Audit.log("world", "spawned 2 stockpiles: %s + %s" % [zone_a.label, zone_b.label]) - - # ── Phase 5: designation → build-site spawn bridge ────────────────────────── # Track build sites keyed by tile so we can find + queue_free them on cancel. @@ -772,6 +782,38 @@ func _on_designation_added(cell: Vector2i, tool: StringName) -> void: sz.accepted_types = [] as Array[StringName] # wildcard sz.queue_redraw() entity = sz + # Quarry — must be placed on a BigRockNode tile. Spawns a + # QuarryWorkbench ghost (auto-FOREVER bill on completion). + &"paint_quarry": + var node = World.big_rock_node_at_tile(cell) + if node == null: + Audit.log("world", "paint_quarry: %s not on a stone outcrop — rejected" % cell) + return + # Refuse if this node already has a quarry built/queued. + for ws in World.workbenches: + if "label_text" in ws and ws.label_text == "Quarry" and node.is_at(ws.tile): + Audit.log("world", "paint_quarry: outcrop at %s already has a quarry" % cell) + return + var quarry = QUARRY_WORKBENCH_SCENE.instantiate() + add_child(quarry) + quarry.setup(cell) + entity = quarry + # Tree planting — spawn a ghost sapling with pending_plant=true so + # ConstructionProvider can queue a build job (1 wood, 30 ticks of work). + # The ghost renders as a translucent sprout until the pawn completes it. + &"plant_tree": + # Reject if tile is already occupied by a tree. + for existing_t in World.trees: + if is_instance_valid(existing_t) and existing_t.tile == cell: + Audit.log("world", "plant_tree: tile %s already has a tree — skipped" % cell) + return + var pt = TREE_SCENE.instantiate() + add_child(pt) + pt.setup(cell, HarvestableTree.STAGE_SAPLING) + pt.pending_plant = true + # Register as a build site so ConstructionProvider can assign a pawn. + World.register_build_site(pt) + entity = pt _: Audit.log("world", "unknown designation tool: %s" % tool) return @@ -864,11 +906,88 @@ func _on_designation_cleared(cell: Vector2i) -> void: func _on_sim_tick_world_sweep(tick_n: int) -> void: _update_dark_overlay() + + # Tree growth — tick every registered tree; mature + pending-plant trees are + # no-ops inside on_sim_tick(), so iterating all is safe. + for tree in World.trees: + if is_instance_valid(tree): + tree.on_sim_tick() + + # WildGrowth — attempt to plant a new sapling once per WILD_GROWTH_INTERVAL. + if tick_n % WILD_GROWTH_INTERVAL == 0: + _try_wild_growth() + if tick_n % HAUL_SWEEP_INTERVAL_TICKS != 0: return hauling_provider.sweep_for_better_destinations() +## Attempt to spawn one wild sapling on a random eligible grass tile. +## Eligibility: walkable + grass terrain + no entity overlap + < 2 tree neighbours. +## Gives up after WILD_GROWTH_MAX_ATTEMPTS rejected tries to avoid lag spikes. +func _try_wild_growth() -> void: + if World.trees.size() >= MAP_TREE_LIMIT: + return + if randf() >= WILD_GROWTH_SPAWN_PROBABILITY: + return + var rng := RandomNumberGenerator.new() + rng.seed = Sim.tick + 9973 # stable within a tick, different each call cycle + for _attempt in WILD_GROWTH_MAX_ATTEMPTS: + var candidate := Vector2i( + rng.randi_range(0, MAP_SIZE_TILES.x - 1), + rng.randi_range(0, MAP_SIZE_TILES.y - 1) + ) + if not _wild_growth_tile_eligible(candidate): + continue + var tree = TREE_SCENE.instantiate() + add_child(tree) + tree.setup(candidate, HarvestableTree.STAGE_SAPLING) + Audit.log("world", "wild growth: sapling at %s (total %d)" % [candidate, World.trees.size()]) + return + + +## Returns true if `tile` is a valid WildGrowth spawn location. +## Checks: walkable, grass terrain (source 0, atlas (0,0)), no entity at tile, +## and fewer than 2 trees among the 4 cardinal neighbours. +func _wild_growth_tile_eligible(tile: Vector2i) -> bool: + # Bounds check (pathfinder bounds == map bounds). + if pathfinder == null: + return false + if not pathfinder.is_walkable(tile): + return false + # Grass terrain only — atlas (0,0) on source 0 = TILE_GRASS. + if terrain_layer == null: + return false + var src_id: int = terrain_layer.get_cell_source_id(tile) + var atlas: Vector2i = terrain_layer.get_cell_atlas_coords(tile) + if src_id != PLACEHOLDER_SOURCE_ID or atlas != TILE_GRASS: + return false + # No existing tree, rock, or item at this tile. + for t in World.trees: + if is_instance_valid(t) and t.tile == tile: + return false + for r in World.rocks: + if is_instance_valid(r): + if r.has_method("footprint_tiles"): + if tile in r.footprint_tiles(): + return false + elif r.tile == tile: + return false + for it in World.items: + if is_instance_valid(it) and it.tile == tile: + return false + # No clumping: reject if 2+ cardinal neighbours already have a tree. + var neighbour_trees: int = 0 + var offsets: Array[Vector2i] = [Vector2i(1, 0), Vector2i(-1, 0), Vector2i(0, 1), Vector2i(0, -1)] + for offset in offsets: + var nb: Vector2i = tile + offset + for t in World.trees: + if is_instance_valid(t) and t.tile == nb: + neighbour_trees += 1 + break + return neighbour_trees < 2 + + # Phase 11 — interpolate CanvasModulate between DAY_TINT and NIGHT_TINT based # on Clock.darkness_factor() (0 = full day, 1 = full night). # Called every sim tick; Color.lerp is a handful of float ops — negligible cost.