scad-gltf 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -73,7 +73,7 @@ func _import_scene(path: String, flags: int, options: Dictionary) -> Object:
73
73
  args.append(temp_glb_path)
74
74
 
75
75
  var output = []
76
- print("Importing %s via scad-convert... (This might take a few seconds on the first run)" % path.get_file())
76
+ print("Importing %s via scad-convert..." % path.get_file())
77
77
 
78
78
  var exit_code = -1
79
79
  if OS.get_name() == "Windows":
@@ -848,6 +848,8 @@ signal game_over_triggered
848
848
  @onready var aim_line: MeshInstance3D = $AimLine
849
849
  @onready var container_visual: Node3D = $ContainerVisual
850
850
 
851
+ # Dimensions of the 3D glass box container (box_w = 12.0, walls at +/- 6.0)
852
+ const CONTAINER_HALF_WIDTH: float = 6.0
851
853
  const BOX_HALF_WIDTH: float = 4.8
852
854
  const DROP_HEIGHT: float = 14.8
853
855
 
@@ -863,6 +865,10 @@ var is_game_over: bool = false
863
865
  var danger_timer: float = 0.0
864
866
  const DANGER_THRESHOLD: float = 2.8
865
867
 
868
+ # Touch / Mouse Drag Controls
869
+ var is_pointer_dragging: bool = false
870
+ var active_touch_index: int = -1
871
+
866
872
  var high_score_file = "user://fusion_highscore.save"
867
873
 
868
874
  func _ready():
@@ -880,6 +886,18 @@ func _ready():
880
886
  _spawn_preview_fruit()
881
887
  _update_aim_line()
882
888
 
889
+ func _notification(what):
890
+ if what == NOTIFICATION_APPLICATION_FOCUS_OUT:
891
+ is_pointer_dragging = false
892
+ active_touch_index = -1
893
+
894
+ func _get_dropper_max_x() -> float:
895
+ var fruit_r: float = 0.55
896
+ if active_preview_tier >= 1 and active_preview_tier <= FruitData.FRUIT_RADII.size():
897
+ fruit_r = FruitData.FRUIT_RADII[active_preview_tier - 1]
898
+ # Keep the preview fruit completely within the glass container walls with safety clearance
899
+ return max(0.5, CONTAINER_HALF_WIDTH - fruit_r - 0.08)
900
+
883
901
  func _setup_container_visuals():
884
902
  if not is_instance_valid(container_visual):
885
903
  return
@@ -925,11 +943,6 @@ func _build_procedural_glass_box():
925
943
  glass_mat.roughness = 0.05
926
944
  glass_mat.metallic = 0.05
927
945
 
928
- var frame_mat = StandardMaterial3D.new()
929
- frame_mat.albedo_color = Color(0.8, 0.85, 0.9, 1.0)
930
- frame_mat.metallic = 0.85
931
- frame_mat.roughness = 0.25
932
-
933
946
  # Back Glass
934
947
  var back_mesh = MeshInstance3D.new()
935
948
  var bm = BoxMesh.new()
@@ -1019,19 +1032,81 @@ func save_high_score():
1019
1032
  file.store_32(high_score)
1020
1033
  file.close()
1021
1034
 
1035
+ func _update_dropper_x(pos):
1036
+ if not is_instance_valid(dropper):
1037
+ return
1038
+
1039
+ var screen_pos: Vector2
1040
+ if pos is Vector2:
1041
+ screen_pos = pos
1042
+ else:
1043
+ screen_pos = Vector2(float(pos), get_viewport().get_visible_rect().size.y * 0.5)
1044
+
1045
+ var max_x = _get_dropper_max_x()
1046
+
1047
+ # Raycast onto the glass container's central plane (Z = 0) where the fruits reside.
1048
+ # This ensures picking maps directly along the physical glass container's width.
1049
+ var camera = get_viewport().get_camera_3d()
1050
+ if camera:
1051
+ var ray_from = camera.project_ray_origin(screen_pos)
1052
+ var ray_dir = camera.project_ray_normal(screen_pos)
1053
+ var drop_plane = Plane(Vector3(0, 0, 1), 0.0)
1054
+ var hit = drop_plane.intersects_ray(ray_from, ray_dir)
1055
+ if hit != null:
1056
+ dropper.position.x = clamp(hit.x, -max_x, max_x)
1057
+ _update_aim_line()
1058
+ return
1059
+
1060
+ var vp_size = get_viewport().get_visible_rect().size
1061
+ if vp_size.x > 0.0:
1062
+ var norm_x = (screen_pos.x / vp_size.x) * 2.0 - 1.0
1063
+ dropper.position.x = clamp(norm_x * max_x, -max_x, max_x)
1064
+ _update_aim_line()
1065
+
1022
1066
  func _unhandled_input(event: InputEvent):
1023
1067
  if is_game_over or not is_instance_valid(dropper):
1024
1068
  return
1025
1069
 
1070
+ # Touch input (mobile / touchscreens)
1071
+ if event is InputEventScreenTouch:
1072
+ if event.pressed:
1073
+ if not is_pointer_dragging:
1074
+ is_pointer_dragging = true
1075
+ active_touch_index = event.index
1076
+ _update_dropper_x(event.position)
1077
+ else:
1078
+ if is_pointer_dragging and event.index == active_touch_index:
1079
+ is_pointer_dragging = false
1080
+ active_touch_index = -1
1081
+ _update_dropper_x(event.position)
1082
+ try_drop_fruit()
1083
+ return
1084
+
1085
+ if event is InputEventScreenDrag:
1086
+ if is_pointer_dragging and event.index == active_touch_index:
1087
+ _update_dropper_x(event.position)
1088
+ return
1089
+
1090
+ # Mouse input (desktop & web)
1091
+ if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
1092
+ if event.pressed:
1093
+ if not is_pointer_dragging:
1094
+ is_pointer_dragging = true
1095
+ _update_dropper_x(event.position)
1096
+ else:
1097
+ if is_pointer_dragging and active_touch_index == -1:
1098
+ is_pointer_dragging = false
1099
+ _update_dropper_x(event.position)
1100
+ try_drop_fruit()
1101
+ return
1102
+
1026
1103
  if event is InputEventMouseMotion:
1027
- var vp_size = get_viewport().get_visible_rect().size
1028
- if vp_size.x > 0.0:
1029
- var norm_x = (event.position.x / vp_size.x) * 2.0 - 1.0
1030
- var target_x = clamp(norm_x * (BOX_HALF_WIDTH + 0.5), -BOX_HALF_WIDTH, BOX_HALF_WIDTH)
1031
- dropper.position.x = target_x
1032
- _update_aim_line()
1104
+ if active_touch_index == -1:
1105
+ _update_dropper_x(event.position)
1106
+ return
1033
1107
 
1034
- if event.is_action_pressed("ui_accept") or (event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.is_pressed()):
1108
+ # Keyboard drop
1109
+ if event.is_action_pressed("ui_accept") or event.is_action_pressed("ui_down"):
1035
1110
  try_drop_fruit()
1036
1111
 
1037
1112
  func _physics_process(delta: float):
@@ -1041,9 +1116,10 @@ func _physics_process(delta: float):
1041
1116
  if is_game_over:
1042
1117
  return
1043
1118
 
1119
+ var max_x = _get_dropper_max_x()
1044
1120
  var move_dir = Input.get_axis("ui_left", "ui_right")
1045
1121
  if move_dir != 0.0:
1046
- dropper.position.x = clamp(dropper.position.x + move_dir * 14.0 * delta, -BOX_HALF_WIDTH, BOX_HALF_WIDTH)
1122
+ dropper.position.x = clamp(dropper.position.x + move_dir * 14.0 * delta, -max_x, max_x)
1047
1123
  _update_aim_line()
1048
1124
 
1049
1125
  var fruits_in_danger = 0
@@ -1073,6 +1149,10 @@ func _spawn_preview_fruit():
1073
1149
  preview_fruit_node.queue_free()
1074
1150
  preview_fruit_node = null
1075
1151
 
1152
+ var max_x = _get_dropper_max_x()
1153
+ dropper.position.x = clamp(dropper.position.x, -max_x, max_x)
1154
+ _update_aim_line()
1155
+
1076
1156
  var model_path = FruitData.get_model_path(active_preview_tier)
1077
1157
  if ResourceLoader.exists(model_path):
1078
1158
  var scene = load(model_path)
@@ -1169,6 +1249,8 @@ func trigger_game_over():
1169
1249
  if is_game_over:
1170
1250
  return
1171
1251
  is_game_over = true
1252
+ is_pointer_dragging = false
1253
+ active_touch_index = -1
1172
1254
  AudioManager.play_game_over_sound()
1173
1255
  game_over_triggered.emit()
1174
1256
 
@@ -1180,6 +1262,8 @@ func restart_game():
1180
1262
  danger_timer = 0.0
1181
1263
  is_game_over = false
1182
1264
  can_drop = true
1265
+ is_pointer_dragging = false
1266
+ active_touch_index = -1
1183
1267
  score_updated.emit(current_score, high_score)
1184
1268
  active_preview_tier = randi_range(1, 3)
1185
1269
  next_tier = randi_range(1, 3)
@@ -1240,16 +1324,135 @@ func _on_restart_pressed():
1240
1324
  `,
1241
1325
  );
1242
1326
 
1327
+ // Procedural 2D Evolution Guide (No missing emojis on Web/Mobile)
1328
+ writeFile(
1329
+ "scripts/evolution_guide.gd",
1330
+ `class_name EvolutionGuide
1331
+ extends Control
1332
+
1333
+ func _ready():
1334
+ mouse_filter = Control.MOUSE_FILTER_IGNORE
1335
+ resized.connect(queue_redraw)
1336
+
1337
+ func _draw():
1338
+ var w = size.x
1339
+ var h = size.y
1340
+ if w <= 0 or h <= 0:
1341
+ return
1342
+
1343
+ # Sleek pill-shaped background
1344
+ var bg_style = StyleBoxFlat.new()
1345
+ bg_style.bg_color = Color(0.06, 0.09, 0.15, 0.75)
1346
+ var cr = int(min(16.0, h * 0.45))
1347
+ bg_style.corner_radius_top_left = cr
1348
+ bg_style.corner_radius_top_right = cr
1349
+ bg_style.corner_radius_bottom_left = cr
1350
+ bg_style.corner_radius_bottom_right = cr
1351
+ bg_style.border_width_left = 1
1352
+ bg_style.border_width_top = 1
1353
+ bg_style.border_width_right = 1
1354
+ bg_style.border_width_bottom = 1
1355
+ bg_style.border_color = Color(1.0, 1.0, 1.0, 0.14)
1356
+ draw_style_box(bg_style, Rect2(Vector2.ZERO, size))
1357
+
1358
+ var fruit_count = 9
1359
+ var slot_width = w / float(fruit_count)
1360
+ var y_center = h * 0.5
1361
+
1362
+ var radii = [7.5, 9.0, 10.5, 12.0, 13.5, 15.0, 16.5, 18.0, 19.5]
1363
+ var max_r_allowed = h * 0.38
1364
+ var scale_factor = 1.0
1365
+ if radii[8] > max_r_allowed:
1366
+ scale_factor = max_r_allowed / radii[8]
1367
+
1368
+ for i in range(fruit_count):
1369
+ var r = radii[i] * scale_factor
1370
+ var cx = slot_width * (i + 0.5)
1371
+ var pos = Vector2(cx, y_center)
1372
+ var col = FruitData.FRUIT_COLORS[i]
1373
+
1374
+ # Shadow
1375
+ draw_circle(pos + Vector2(0, 1.5), r + 0.8, Color(0, 0, 0, 0.35))
1376
+ # Main body
1377
+ draw_circle(pos, r, col)
1378
+ # Specular reflection
1379
+ draw_circle(pos + Vector2(-r * 0.35, -r * 0.35), r * 0.32, Color(1.0, 1.0, 1.0, 0.38))
1380
+
1381
+ # Distinct fruit icon visual elements
1382
+ match i + 1:
1383
+ 1: # Cherry: stem and leaf
1384
+ draw_line(pos + Vector2(0, -r), pos + Vector2(r * 0.4, -r - 4.5 * scale_factor), Color(0.3, 0.7, 0.15), 1.4, true)
1385
+ draw_circle(pos + Vector2(r * 0.45, -r - 4.5 * scale_factor), 1.8 * scale_factor, Color(0.3, 0.85, 0.2))
1386
+ 2: # Strawberry: green sepal and seeds
1387
+ draw_circle(pos + Vector2(-r * 0.35, -r + 1), 1.8 * scale_factor, Color(0.2, 0.8, 0.25))
1388
+ draw_circle(pos + Vector2(0, -r - 1), 2.0 * scale_factor, Color(0.2, 0.8, 0.25))
1389
+ draw_circle(pos + Vector2(r * 0.35, -r + 1), 1.8 * scale_factor, Color(0.2, 0.8, 0.25))
1390
+ draw_circle(pos + Vector2(-r * 0.2, -r * 0.1), 0.9 * scale_factor, Color(1.0, 0.9, 0.35))
1391
+ draw_circle(pos + Vector2(r * 0.25, -r * 0.15), 0.9 * scale_factor, Color(1.0, 0.9, 0.35))
1392
+ draw_circle(pos + Vector2(0, r * 0.35), 0.9 * scale_factor, Color(1.0, 0.9, 0.35))
1393
+ 3: # Grape: stem and cluster spots
1394
+ draw_line(pos + Vector2(0, -r), pos + Vector2(0, -r - 3.5 * scale_factor), Color(0.4, 0.7, 0.2), 1.4, true)
1395
+ draw_circle(pos + Vector2(-r * 0.25, -r * 0.15), r * 0.25, Color(0.4, 0.1, 0.55, 0.7))
1396
+ draw_circle(pos + Vector2(r * 0.25, -r * 0.1), r * 0.25, Color(0.4, 0.1, 0.55, 0.7))
1397
+ draw_circle(pos + Vector2(0, r * 0.3), r * 0.25, Color(0.4, 0.1, 0.55, 0.7))
1398
+ 4: # Tangerine: leaf
1399
+ draw_line(pos + Vector2(0, -r), pos + Vector2(r * 0.25, -r - 3.0 * scale_factor), Color(0.3, 0.65, 0.2), 1.4, true)
1400
+ draw_circle(pos + Vector2(r * 0.35, -r - 2.5 * scale_factor), 2.2 * scale_factor, Color(0.25, 0.8, 0.2))
1401
+ 5: # Apple: stem and leaf
1402
+ draw_circle(pos + Vector2(0, -r + 1), 2.0 * scale_factor, Color(0.6, 0.05, 0.08))
1403
+ draw_line(pos + Vector2(0, -r + 1), pos + Vector2(r * 0.2, -r - 4.0 * scale_factor), Color(0.45, 0.25, 0.1), 1.5, true)
1404
+ draw_circle(pos + Vector2(r * 0.35, -r - 3.0 * scale_factor), 2.3 * scale_factor, Color(0.25, 0.8, 0.2))
1405
+ 6: # Peach: peach cleft
1406
+ draw_line(pos + Vector2(0, -r * 0.7), pos + Vector2(0, r * 0.7), Color(0.85, 0.32, 0.36, 0.85), 1.6, true)
1407
+ draw_circle(pos + Vector2(r * 0.25, -r - 1.5 * scale_factor), 2.2 * scale_factor, Color(0.25, 0.8, 0.2))
1408
+ 7: # Melon: netting ribs
1409
+ draw_arc(pos, r * 0.65, -PI * 0.5, PI * 0.5, 10, Color(0.88, 0.98, 0.62, 0.75), 1.4, true)
1410
+ draw_arc(pos, r * 0.65, PI * 0.5, PI * 1.5, 10, Color(0.88, 0.98, 0.62, 0.75), 1.4, true)
1411
+ draw_line(pos + Vector2(0, -r), pos + Vector2(0, -r - 3.5 * scale_factor), Color(0.3, 0.6, 0.2), 1.8, true)
1412
+ 8: # Watermelon: dark green wavy stripes
1413
+ var stripe_col = Color(0.06, 0.28, 0.10, 0.9)
1414
+ draw_arc(pos + Vector2(-r * 0.35, 0), r * 0.8, -PI * 0.4, PI * 0.4, 10, stripe_col, 1.8, true)
1415
+ draw_line(pos + Vector2(0, -r * 0.88), pos + Vector2(0, r * 0.88), stripe_col, 1.8, true)
1416
+ draw_arc(pos + Vector2(r * 0.35, 0), r * 0.8, PI * 0.6, PI * 1.4, 10, stripe_col, 1.8, true)
1417
+ 9: # King Sun: glowing halo and crown
1418
+ draw_arc(pos, r + 2.5 * scale_factor, 0, TAU, 28, Color(1.0, 0.85, 0.2, 0.6), 1.4, true)
1419
+ var cb = pos + Vector2(0, -r + 2 * scale_factor)
1420
+ var crown_pts = PackedVector2Array([
1421
+ cb + Vector2(-6 * scale_factor, 0),
1422
+ cb + Vector2(-6 * scale_factor, -5 * scale_factor),
1423
+ cb + Vector2(-3 * scale_factor, -2 * scale_factor),
1424
+ cb + Vector2(0, -7 * scale_factor),
1425
+ cb + Vector2(3 * scale_factor, -2 * scale_factor),
1426
+ cb + Vector2(6 * scale_factor, -5 * scale_factor),
1427
+ cb + Vector2(6 * scale_factor, 0)
1428
+ ])
1429
+ draw_colored_polygon(crown_pts, Color(1.0, 0.92, 0.3))
1430
+ draw_polyline(crown_pts, Color(0.85, 0.65, 0.1), 1.0, true)
1431
+ draw_circle(cb + Vector2(0, -7 * scale_factor), 1.4 * scale_factor, Color(0.2, 0.9, 1.0))
1432
+
1433
+ # Evolution progression arrow
1434
+ if i < fruit_count - 1:
1435
+ var arrow_x = slot_width * (i + 1)
1436
+ var arrow_pts = PackedVector2Array([
1437
+ Vector2(arrow_x - 3.0, y_center - 4.0),
1438
+ Vector2(arrow_x + 2.0, y_center),
1439
+ Vector2(arrow_x - 3.0, y_center + 4.0)
1440
+ ])
1441
+ draw_polyline(arrow_pts, Color(1.0, 1.0, 1.0, 0.45), 1.5, true)
1442
+ `,
1443
+ );
1444
+
1243
1445
  // ==========================================
1244
1446
  // 4. MAIN SCENE FILE (.tscn)
1245
1447
  // ==========================================
1246
1448
 
1247
1449
  writeFile(
1248
1450
  "scenes/main.tscn",
1249
- `[gd_scene load_steps=15 format=3 uid="uid://c2suika0main"]
1451
+ `[gd_scene load_steps=16 format=3 uid="uid://c2suika0main"]
1250
1452
 
1251
1453
  [ext_resource type="Script" path="res://scripts/game_manager.gd" id="1_gm"]
1252
1454
  [ext_resource type="Script" path="res://scripts/ui_manager.gd" id="2_ui"]
1455
+ [ext_resource type="Script" path="res://scripts/evolution_guide.gd" id="3_evo"]
1253
1456
 
1254
1457
  [sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_sky"]
1255
1458
  sky_top_color = Color(0.2, 0.45, 0.75, 1)
@@ -1440,23 +1643,20 @@ theme_override_colors/font_color = Color(1, 1, 1, 0.9)
1440
1643
  text = "Strawberry"
1441
1644
  horizontal_alignment = 1
1442
1645
 
1443
- [node name="EvolutionGuide" type="HBoxContainer" parent="UI"]
1646
+ [node name="EvolutionGuide" type="Control" parent="UI"]
1444
1647
  layout_mode = 1
1445
1648
  anchors_preset = 12
1446
1649
  anchor_top = 1.0
1447
1650
  anchor_right = 1.0
1448
1651
  anchor_bottom = 1.0
1449
- offset_top = -55.0
1652
+ offset_left = 20.0
1653
+ offset_top = -65.0
1654
+ offset_right = -20.0
1450
1655
  offset_bottom = -15.0
1451
1656
  grow_horizontal = 2
1452
1657
  grow_vertical = 0
1453
- alignment = 1
1454
-
1455
- [node name="GuideLabel" type="Label" parent="UI/EvolutionGuide"]
1456
- layout_mode = 2
1457
- theme_override_font_sizes/font_size = 15
1458
- theme_override_colors/font_color = Color(1, 1, 1, 0.75)
1459
- text = "🍒 Cherry → 🍓 Strawberry → 🍇 Grape → 🍊 Orange → 🍎 Apple → 🍑 Peach → 🍈 Melon → 🍉 Watermelon → 👑 Sun"
1658
+ mouse_filter = 2
1659
+ script = ExtResource("3_evo")
1460
1660
 
1461
1661
  [node name="DangerIndicator" type="ColorRect" parent="UI"]
1462
1662
  visible = false
@@ -1551,12 +1751,12 @@ Drop different fruits into the transparent glass container. When two identical f
1551
1751
  ---
1552
1752
 
1553
1753
  ## Controls
1554
- - **Move Dropper / Aim**:
1555
- - Mouse Move / Touch Drag
1556
- - Left / Right Arrow keys
1557
- - A / D keys
1754
+ - **Aim Dropper**:
1755
+ - Touch & Drag along the glass container (Mobile / Web)
1756
+ - Mouse Move / Click & Drag along the glass container (Desktop)
1757
+ - Left / Right Arrow keys / A & D keys
1558
1758
  - **Drop Fruit**:
1559
- - Left Click
1759
+ - Release Touch / Release Left Click
1560
1760
  - Spacebar
1561
1761
  - Enter
1562
1762
  - Down Arrow
@@ -1826,6 +1826,13 @@ var proj_scene = preload("res://scenes/projectile.tscn")
1826
1826
  var lightning_scene = preload("res://scenes/lightning_strike.tscn")
1827
1827
  var invulnerable_timer: float = 0.0
1828
1828
 
1829
+ # --- INVISIBLE TOUCH CONTROLS ---
1830
+ var touch_index: int = -1
1831
+ var touch_start_pos: Vector2 = Vector2.ZERO
1832
+ var touch_input_vec: Vector2 = Vector2.ZERO
1833
+ const TOUCH_DEADZONE: float = 8.0
1834
+ const TOUCH_MAX_DIST: float = 65.0
1835
+
1829
1836
  func _ready():
1830
1837
  add_to_group("player")
1831
1838
  current_health = max_health
@@ -1840,6 +1847,32 @@ func _ready():
1840
1847
  orbit_shield.set_orb_count(orbit_count)
1841
1848
  _setup_player_animations(model)
1842
1849
 
1850
+ func _unhandled_input(event: InputEvent) -> void:
1851
+ if event is InputEventScreenTouch:
1852
+ if event.pressed:
1853
+ if touch_index == -1:
1854
+ touch_index = event.index
1855
+ touch_start_pos = event.position
1856
+ touch_input_vec = Vector2.ZERO
1857
+ elif event.index == touch_index or event.is_canceled():
1858
+ _reset_touch()
1859
+ elif event is InputEventScreenDrag:
1860
+ if event.index == touch_index:
1861
+ var diff = event.position - touch_start_pos
1862
+ var dist = diff.length()
1863
+ if dist > TOUCH_DEADZONE:
1864
+ var move_dist = min(dist - TOUCH_DEADZONE, TOUCH_MAX_DIST)
1865
+ touch_input_vec = diff.normalized() * (move_dist / TOUCH_MAX_DIST)
1866
+ # Follow the finger smoothly if dragged past max threshold
1867
+ if dist > TOUCH_MAX_DIST + TOUCH_DEADZONE:
1868
+ touch_start_pos = event.position - diff.normalized() * (TOUCH_MAX_DIST + TOUCH_DEADZONE)
1869
+ else:
1870
+ touch_input_vec = Vector2.ZERO
1871
+
1872
+ func _reset_touch():
1873
+ touch_index = -1
1874
+ touch_input_vec = Vector2.ZERO
1875
+
1843
1876
  func _setup_player_animations(node: Node):
1844
1877
  if node is AnimationPlayer:
1845
1878
  anim_player = node
@@ -1868,7 +1901,14 @@ func _physics_process(delta: float):
1868
1901
  input_vec.x = Input.get_axis("move_left", "move_right")
1869
1902
  input_vec.y = Input.get_axis("move_up", "move_down")
1870
1903
 
1871
- var move_dir = Vector3(input_vec.x, 0, input_vec.y).normalized()
1904
+ # Blend or apply touch screen input
1905
+ if touch_input_vec != Vector2.ZERO:
1906
+ input_vec += touch_input_vec
1907
+
1908
+ if input_vec.length() > 1.0:
1909
+ input_vec = input_vec.normalized()
1910
+
1911
+ var move_dir = Vector3(input_vec.x, 0, input_vec.y)
1872
1912
  velocity.x = move_dir.x * move_speed
1873
1913
  velocity.z = move_dir.z * move_speed
1874
1914
  move_and_slide()
@@ -1973,6 +2013,7 @@ func take_damage(amount: int):
1973
2013
 
1974
2014
  if current_health <= 0:
1975
2015
  current_health = 0
2016
+ _reset_touch()
1976
2017
  emit_signal("died")
1977
2018
 
1978
2019
  func heal(amount: int):
@@ -1986,6 +2027,7 @@ func add_xp(amount: int):
1986
2027
  level += 1
1987
2028
  required_xp = int(required_xp * 1.4)
1988
2029
  SoundManager.play_level_up()
2030
+ _reset_touch()
1989
2031
  emit_signal("leveled_up", level)
1990
2032
  emit_signal("xp_changed", current_xp, required_xp, level)
1991
2033
 
@@ -2157,6 +2199,7 @@ func _on_restart_pressed():
2157
2199
  );
2158
2200
 
2159
2201
  // scenes/ui.tscn
2202
+ // HUD controls set to mouse_filter = 2 (IGNORE) so touch events flow directly to player input
2160
2203
  writeFile(
2161
2204
  "scenes/ui.tscn",
2162
2205
  `
@@ -2183,6 +2226,7 @@ anchors_preset = 10
2183
2226
  anchor_right = 1.0
2184
2227
  offset_bottom = 24.0
2185
2228
  grow_horizontal = 2
2229
+ mouse_filter = 2
2186
2230
  value = 30.0
2187
2231
  show_percentage = false
2188
2232
 
@@ -2196,6 +2240,7 @@ offset_top = 28.0
2196
2240
  offset_right = 60.0
2197
2241
  offset_bottom = 54.0
2198
2242
  grow_horizontal = 2
2243
+ mouse_filter = 2
2199
2244
  theme_override_font_sizes/font_size = 22
2200
2245
  text = "LVL 1"
2201
2246
  horizontal_alignment = 1
@@ -2210,6 +2255,7 @@ offset_top = 58.0
2210
2255
  offset_right = 60.0
2211
2256
  offset_bottom = 90.0
2212
2257
  grow_horizontal = 2
2258
+ mouse_filter = 2
2213
2259
  theme_override_font_sizes/font_size = 28
2214
2260
  text = "00:00"
2215
2261
  horizontal_alignment = 1
@@ -2220,6 +2266,7 @@ offset_left = 24.0
2220
2266
  offset_top = 36.0
2221
2267
  offset_right = 180.0
2222
2268
  offset_bottom = 68.0
2269
+ mouse_filter = 2
2223
2270
  theme_override_font_sizes/font_size = 20
2224
2271
  text = "Kills: 0"
2225
2272
 
@@ -2236,6 +2283,7 @@ offset_right = 200.0
2236
2283
  offset_bottom = -28.0
2237
2284
  grow_horizontal = 2
2238
2285
  grow_vertical = 0
2286
+ mouse_filter = 2
2239
2287
  value = 100.0
2240
2288
  show_percentage = false
2241
2289
 
@@ -2246,6 +2294,7 @@ anchor_right = 1.0
2246
2294
  anchor_bottom = 1.0
2247
2295
  grow_horizontal = 2
2248
2296
  grow_vertical = 2
2297
+ mouse_filter = 2
2249
2298
  text = "HP: 100 / 100"
2250
2299
  horizontal_alignment = 1
2251
2300
  vertical_alignment = 1
@@ -2611,6 +2660,11 @@ SpatialGrid="*res://scripts/spatial_grid.gd"
2611
2660
 
2612
2661
  enabled=PackedStringArray("res://addons/scad_importer/plugin.cfg")
2613
2662
 
2663
+ [input_devices]
2664
+
2665
+ pointing/emulate_touch_from_mouse=true
2666
+ pointing/emulate_mouse_from_touch=true
2667
+
2614
2668
  [layer_names]
2615
2669
 
2616
2670
  3d_physics/layer_1="World"
@@ -2676,19 +2730,24 @@ writeFile(
2676
2730
 
2677
2731
  A 3D auto-shooter / horde survival game inspired by *Vampire Survivors*.
2678
2732
 
2733
+ ## Touch Screen Controls
2734
+ - Touch and drag anywhere on the display to smoothly guide the hero with an invisible, dynamic virtual joystick.
2735
+ - Does not clutter the screen with visible on-screen buttons or virtual pads.
2736
+ - Upgrades and menu buttons respond immediately to direct taps.
2737
+
2679
2738
  ## Physics Optimization
2680
2739
  - Replaced the CPU-heavy Physics Engine character body repulsion with an ultra-fast **Spatial Grid Hashing** setup.
2681
- - Enemies manually separate and flock using 2D grid neighbor lookups, keeping thousands of CharacterBody3D nodes running at high frame rates.
2740
+ - Enemies manually separate and flock using 2D grid neighbor lookups, keeping CharacterBody3D nodes running at high frame rates.
2682
2741
 
2683
2742
  ## Model & Asset Polish:
2684
- - **The Brute**: Chest spikes now project forward (+Y) out of the armor plate instead of pointing up. Added spiked pauldrons, curved horns, and a two-handed obsidian war maul with a molten glowing core.
2685
- - **The Bat (Swarmer)**: Redesigned with organic swept wings, an arched elbow spar, wing claws, skeletal ribs, and smooth flapping animations.
2743
+ - **The Brute**: Spikes project forward (+Y) out of chest armor. Features spiked pauldrons, curved horns, and a two-handed war maul with a molten glowing core.
2744
+ - **The Bat (Swarmer)**: Designed with organic swept wings, elbow spars, wing claws, skeletal ribs, and flapping animations.
2686
2745
  - **The Zombie (Walker)**: Detailed with sunken eye sockets, glowing red pupils, a decaying jaw with teeth, exposed ribs, and clawed reaching hands.
2687
- - **The Hero (Knight)**: Wields an angled runic broadsword with pommel, grip, crossguard, and fuller in the right hand and an emblazoned heater kite shield on the left forearm.
2746
+ - **The Hero (Knight)**: Wields an angled runic broadsword in the right hand and an emblazoned heater kite shield on the left forearm.
2688
2747
  `,
2689
2748
  );
2690
2749
 
2691
2750
  console.log("\n======================================================");
2692
- console.log('Project "Horde Survivor 3D" updated with Fast Grid Physics!');
2751
+ console.log('Project "Horde Survivor 3D" updated with Touch Controls!');
2693
2752
  console.log(`Directory: ${ROOT_DIR}`);
2694
2753
  console.log("======================================================\n");
@@ -75,7 +75,7 @@ func _import_scene(path: String, flags: int, options: Dictionary) -> Object:
75
75
  args.append(temp_glb_path)
76
76
 
77
77
  var output = []
78
- print("Importing %s via scad-convert... (This might take a few seconds on the first run)" % path.get_file())
78
+ print("Importing %s via scad-convert..." % path.get_file())
79
79
 
80
80
  var exit_code = -1
81
81
  if OS.get_name() == "Windows":
@@ -0,0 +1,25 @@
1
+ [preset.0]
2
+ name="Web"
3
+ platform="Web"
4
+ runnable=true
5
+ advanced_options=false
6
+ dedicated_server=false
7
+ custom_features=""
8
+ export_filter="all_resources"
9
+ include_filter=""
10
+ exclude_filter=""
11
+ export_path=""
12
+
13
+ [preset.0.options]
14
+ custom_template/debug=""
15
+ custom_template/release=""
16
+ variant/extensions_support=false
17
+ variant/thread_support=true
18
+ vram_compression/import_etc2_astc=false
19
+ html/export_icon=true
20
+ html/custom_html_shell=""
21
+ html/head_include=""
22
+ html/canvas_resize_policy=2
23
+ html/focus_canvas_on_start=true
24
+ html/experimental_virtual_keyboard=false
25
+ progressive_web_app/enabled=false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scad-gltf",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "WASM build of a custom OpenSCAD fork that compiles .scad scripts directly to glTF/GLB with PBR and animation support",
5
5
  "type": "module",
6
6
  "main": "./src/ext/openscad.js",
@@ -49,7 +49,9 @@
49
49
  "scripts": {
50
50
  "format": "prettier --write .",
51
51
  "mcp": "npx @modelcontextprotocol/inspector node ./bin/scad-mcp.js",
52
- "prepublishOnly": "npm install --prefix editor && npm run build --prefix editor"
52
+ "prepublishOnly": "npm install --prefix editor && npm run build --prefix editor",
53
+ "build:godot": "node scripts/build-godot-web.js",
54
+ "deploy:godot": "gh-pages -d .godot-dist --repo git@github.com:iliagrigorevdev/scad-godot.git -b gh-pages"
53
55
  },
54
56
  "dependencies": {
55
57
  "@modelcontextprotocol/sdk": "^1.30.0",
@@ -58,6 +60,8 @@
58
60
  "puppeteer": "^25.9.0"
59
61
  },
60
62
  "devDependencies": {
63
+ "coi-serviceworker": "^0.1.7",
64
+ "gh-pages": "^6.3.0",
61
65
  "prettier": "3.9.6"
62
66
  }
63
67
  }
package/src/prompt.js CHANGED
@@ -117,7 +117,7 @@ export function generatePrompt(description, options = {}) {
117
117
  prompt += `\n\nImportant Animation rules:
118
118
  - Wrapping: Use the 'armature(animations=...)' module at the root to wrap all animated components.
119
119
  - Hierarchies: Use the 'bone(name="BoneName", t=[x,y,z], r=[x,y,z])' module to define hierarchical animated parts.
120
- - Auto-Unioning: Any child meshes (e.g., cube, cylinder, imported objects) placed directly inside an 'armature()' or 'bone()' node are automatically unioned together by the engine. Child bones remain separate nodes in the hierarchy.
120
+ - Auto-Unioning: Any child meshes (e.g., cube, cylinder, imported objects) placed directly inside an 'armature()' or 'bone()' node are automatically unioned together by the engine and rigidly attached to that bone's transform node. Child bones remain separate nodes in the hierarchy.
121
121
  - Animation Data: The 'animations' property is an array of named animation sequences. Each sequence contains an array of tracks defining keyframes for each bone. Format:
122
122
  animations = [
123
123
  ["AnimationName", [