-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathmain.lua
More file actions
2340 lines (2287 loc) · 106 KB
/
Copy pathmain.lua
File metadata and controls
2340 lines (2287 loc) · 106 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
-- main.lua
-- Love2D Tower Defense Resource Game
-- See README.md for up-to-date documentation and instructions.
require('tiledmap')
local C = require('constants')
local function add_log(msg)
print(os.date('%H:%M:%S') .. ' ' .. msg)
end
-- Game settings
local TILE_SIZE = 16
local WIDTH, HEIGHT = 800, 600
local PLAYER_SIZE = 8
local PLAYER_COLOR = { 0.2, 0.8, 0.2 }
local BG_COLOR = { 0.12, 0.12, 0.12 }
local PLAYER_SPEED = 200
local ENEMY_SIZE = PLAYER_SIZE
local ENEMY_COLOR = { 0.78, 0.2, 0.2 }
local ENEMY_SPEED = C.ENEMY_SPEED
local ENEMY_HEALTH = C.ENEMY_HEALTH
local ENEMY_CHOP_TIME = 1.2 -- seconds to steal resource
local RESOURCE_TYPES = C.RESOURCE_TYPES
local RESOURCE_SIZE = TILE_SIZE
local RESOURCE_MAX_HP = C.RESOURCE_MAX_HP
local RESOURCE_RESPAWN_TIME = 5 -- seconds
local COLLECT_RADIUS = 24 -- smaller for 16x16 scale
local COLLECT_HIT_INTERVAL = 0.17 -- faster: 3 chops for default
local TOWER_COST = C.TOWER_COST
local TOWER_COIN_COST = C.TOWER_COST
local RESOURCE_DAMAGE = C.TOWER_DAMAGE
local TOWER_RANGE = C.TOWER_RANGE
local TOWER_FIRE_RATE = C.TOWER_FIRE_RATE
local BULLET_SPEED = 160 -- slower for scale
local CHECKOUT_ZONES = {}
local CUSTOMER_SIZE = PLAYER_SIZE
local CUSTOMER_COLOR = { 1, 0.9, 0.3 }
local CUSTOMER_SPEED = C.CUSTOMER_SPEED
local CUSTOMER_WAIT_TIME = 4 -- seconds
local CUSTOMER_SPAWN_INTERVAL = C.CUSTOMER_SPAWN_INTERVAL
local SELL_REWARD = 3 -- coins per resource
local UPGRADE_SHOPS = {}
local UPGRADE_COST = 20
local UPGRADE_COLOR = { 0.3, 0.7, 1 }
local has_upgrade = false
local ENEMY_ZONES = {}
local enemy_zone_triggered = false
local ENEMY_LINE_COUNT = C.ENEMY_WAVE_SIZE
local wave_number = 0
local wave_total = 0
local wave_alive = 0
local log_messages = {}
local LOG_MAX = 8
local debug_timer = 0
local next_raid_time = 0
local raid_countdown = 0
local raid_pending = false
local raid_message = nil
local RESOURCE_TO_CHECKOUT_TILE = {}
local logic = require('logic')
local camera = { x = 0, y = 0, scale = 2 } -- 2x zoom
local function dist(x1, y1, x2, y2)
return logic.dist(x1, y1, x2, y2)
end
function remove_nearest_flag_or_barack()
local px = player.x + player.size / 2
local py = player.y + player.size / 2
local best_dist, best_index, best_type = math.huge, nil, nil
for i, flag in ipairs(flags) do
local d = dist(px, py, flag.x, flag.y)
if d <= C.FLAG_COMMAND_RADIUS and d < best_dist then
best_dist, best_index, best_type = d, i, "flag"
end
end
for i, barack in ipairs(baracks) do
local d = dist(px, py, barack.x, barack.y)
if d <= C.FLAG_COMMAND_RADIUS and d < best_dist then
best_dist, best_index, best_type = d, i, "barack"
end
end
if best_index and best_type == "flag" then
table.remove(flags, best_index)
add_log("Flag removed!")
elseif best_index and best_type == "barack" then
table.remove(baracks, best_index)
add_log("Barack removed!")
else
add_log("No flag or barack in range to remove.")
end
end
function assign_resource_checkout_tiles()
-- Assign each resource type to a checkout tile (by order)
for i, rtype in ipairs(RESOURCE_TYPES) do
if CHECKOUT_ZONES[i] then
RESOURCE_TO_CHECKOUT_TILE[rtype.name] = CHECKOUT_ZONES[i]
end
end
end
function love.load()
love.window.setMode(WIDTH, HEIGHT)
TiledMap_Load('map.tmx', TILE_SIZE)
-- Set world size to map size
WORLD_WIDTH = TiledMap_GetMapW() * TILE_SIZE
WORLD_HEIGHT = TiledMap_GetMapH() * TILE_SIZE
-- Find all special tiles on the first layer
local layer_z = 1
-- Resource sources (cleaned up, data-driven)
resource_sources = {}
local resource_tile_ids = {
wood = 51,
stone = 104,
grass = 311,
metal = 69
}
for i, rtype in ipairs(RESOURCE_TYPES) do
local tile_id = resource_tile_ids[rtype.name]
if tile_id then
for _, pos in ipairs(TiledMap_ListAllOfTypeOnLayer(layer_z, tile_id)) do
table.insert(resource_sources, {
x = pos.x * TILE_SIZE,
y = pos.y * TILE_SIZE,
type = rtype,
max_hp = RESOURCE_MAX_HP[rtype.name],
hp = RESOURCE_MAX_HP[rtype.name],
respawn_timer = 0,
active = true
})
print("Resource created:", rtype.name, "hp:", RESOURCE_MAX_HP[rtype.name], "max_hp:",
RESOURCE_MAX_HP[rtype.name])
end
end
end
-- Enemy spawn zones (support multiple tiles)
ENEMY_ZONES = {}
local enemy_spawns = TiledMap_ListAllOfTypeOnLayer(layer_z, 981)
for _, s in ipairs(enemy_spawns) do
table.insert(ENEMY_ZONES, { x = s.x * TILE_SIZE, y = s.y * TILE_SIZE, w = TILE_SIZE, h = TILE_SIZE })
end
-- Customer shop zones (support multiple tiles)
CHECKOUT_ZONES = {}
local shop_tiles = TiledMap_ListAllOfTypeOnLayer(layer_z, 747)
for _, s in ipairs(shop_tiles) do
table.insert(CHECKOUT_ZONES, { x = s.x * TILE_SIZE, y = s.y * TILE_SIZE, w = TILE_SIZE, h = TILE_SIZE })
end
-- Upgrades zones (support multiple tiles)
UPGRADE_SHOPS = {}
local upgrade_tiles = TiledMap_ListAllOfTypeOnLayer(layer_z, 998)
for _, s in ipairs(upgrade_tiles) do
table.insert(UPGRADE_SHOPS, { x = s.x * TILE_SIZE, y = s.y * TILE_SIZE, w = TILE_SIZE, h = TILE_SIZE })
end
-- Place player at first empty tile (not a special tile)
local placed = false
for y = 0, TiledMap_GetMapH() - 1 do
for x = 0, TiledMap_GetMapW() - 1 do
local tid = TiledMap_GetMapTile(x, y, layer_z)
if tid ~= 51 and tid ~= 104 and tid ~= 311 and tid ~= 981 and tid ~= 747 and tid ~= 998 and tid ~= 0 and tid ~= 69 then
player = { x = x * TILE_SIZE, y = y * TILE_SIZE, size = PLAYER_SIZE, speed = PLAYER_SPEED }
placed = true
break
end
end
if placed then break end
end
-- For backward compatibility, set ENEMY_ZONE, CHECKOUT_ZONE, UPGRADE_SHOP to the first tile if any exist
ENEMY_ZONE = ENEMY_ZONES[1]
CHECKOUT_ZONE = CHECKOUT_ZONES[1]
UPGRADE_SHOP = UPGRADE_SHOPS[1]
enemies = {}
towers = {}
bullets = {}
blueprints = {}
-- New mechanics:
baracks = {} -- Completed Barack buildings
robots = {} -- Active robots
flags = {} -- Directional flags
barack_blueprints = {} -- Similar to blueprints, but for baracks
flag_blueprints = {} -- Optional: to support build-in-progress for flags
resource_items = {}
player_carry = {}
spawn_timer = 0
collect_timer = 0
collecting = nil
path = { { 0, HEIGHT / 2 }, { WIDTH, HEIGHT / 2 } }
customers = {}
customer_timer = 0
for _, t in ipairs(towers or {}) do t.ammo = {} end
-- Initialize raid timer
next_raid_time = love.timer.getTime() + math.random(30, 60) -- first raid in 30-60 seconds
raid_countdown = 0
raid_pending = false
raid_message = nil
assign_resource_checkout_tiles()
end
function love.update(dt)
-- Player movement (robust: always update player.x/y)
local dx, dy = 0, 0
if love.keyboard.isDown('w') or love.keyboard.isDown('up') then dy = dy - 1 end
if love.keyboard.isDown('s') or love.keyboard.isDown('down') then dy = dy + 1 end
if love.keyboard.isDown('a') or love.keyboard.isDown('left') then dx = dx - 1 end
if love.keyboard.isDown('d') or love.keyboard.isDown('right') then dx = dx + 1 end
if dx ~= 0 or dy ~= 0 then
local len = math.sqrt(dx * dx + dy * dy)
player.x = player.x + player.speed * dt * dx / (len > 0 and len or 1)
player.y = player.y + player.speed * dt * dy / (len > 0 and len or 1)
end
player.x = math.max(0, math.min(WORLD_WIDTH - player.size, player.x))
player.y = math.max(0, math.min(WORLD_HEIGHT - player.size, player.y))
-- Camera follows player, keep player centered in the world, account for zoom
camera.x = math.floor(player.x + player.size / 2 - WIDTH / (2 * camera.scale))
camera.y = math.floor(player.y + player.size / 2 - HEIGHT / (2 * camera.scale))
-- Clamp camera to map bounds
camera.x = math.max(0, math.min(camera.x, WORLD_WIDTH - WIDTH / camera.scale))
camera.y = math.max(0, math.min(camera.y, WORLD_HEIGHT - HEIGHT / camera.scale))
-- Resource collection
if collecting and (not collecting.active or dist(player.x, player.y, collecting.x, collecting.y) > COLLECT_RADIUS) then
collecting = nil
collect_timer = 0
end
if not collecting then
-- Only collect a resource if not near a blueprint
local near_blueprint = false
for _, bp in ipairs(blueprints) do
if not bp.complete and not bp.coin_paid and dist(player.x + player.size / 2, player.y + player.size / 2, bp.x, bp.y) < 40 then
near_blueprint = true
break
end
end
if not near_blueprint then
for _, src in ipairs(resource_sources) do
if src.active and dist(player.x, player.y, src.x, src.y) < COLLECT_RADIUS then
collecting = src
collect_timer = 0
break
end
end
end
end
-- Upgrade shop interaction
local near_upgrade = false
for _, shop in ipairs(UPGRADE_SHOPS) do
if dist(player.x + player.size / 2, player.y + player.size / 2, shop.x + shop.w / 2, shop.y + shop.h / 2) < 50 then
near_upgrade = true
break
end
end
if not has_upgrade and near_upgrade then
if love.keyboard.isDown('e') and #player_carry < C.MAX_CARRY then
has_upgrade = true
end
end
-- Faster chopping if upgraded
if collecting and collecting.active then
if has_upgrade then
collect_timer = collect_timer + dt * 1.5 -- 2 chops when upgraded
else
collect_timer = collect_timer + dt
end
if collect_timer >= COLLECT_HIT_INTERVAL then
for k, v in pairs(collecting) do print("collecting[" .. tostring(k) .. "] = " .. tostring(v)) end
print("type(collecting.hp):", type(collecting.hp))
collecting.hp = collecting.hp - 1
collect_timer = 0
if type(collecting.hp) ~= "number" then
error("Resource hp is not a number! It is: " .. tostring(collecting.hp))
end
if collecting.hp <= 0 then
-- Unified resource drop code for player and robots: always drop wood if depleted!
if collecting.type and collecting.type.name == "wood" then
table.insert(resource_items, {
x = collecting.x + 10, y = collecting.y + 10, type = collecting.type, picked = false
})
end
add_log('Resource node depleted: ' .. collecting.type.name)
collecting.active = false
collecting.respawn_timer = 0
collecting = nil
end
end
end
-- Pick up resource items
for _, item in ipairs(resource_items) do
if not item.picked and #player_carry < C.MAX_CARRY then
if dist(player.x + player.size / 2, player.y + player.size / 2, item.x + 2, item.y + 2) < 30 then
table.insert(player_carry, item.type.name)
item.picked = true
add_log('Picked up: ' .. item.type.name)
end
end
end
-- Remove picked items
for i = #resource_items, 1, -1 do
if resource_items[i].picked then table.remove(resource_items, i) end
end
-- Invest carried resources into blueprints (resource vector)
for _, bp in ipairs(blueprints) do
if not bp.complete then
if dist(player.x + player.size / 2, player.y + player.size / 2, bp.x, bp.y) < 40 then
for i = #player_carry, 1, -1 do
local r = player_carry[i]
if bp.required[r] and (bp.progress[r] or 0) < bp.required[r] then
bp.progress[r] = (bp.progress[r] or 0) + 1
table.remove(player_carry, i)
end
end
-- Check if all requirements are fulfilled
local all_done = true
for k, v in pairs(bp.required) do
if (bp.progress[k] or 0) < v then
all_done = false
break
end
end
if all_done then
bp.complete = true
end
end
end
end
-- Invest carried resources into barack blueprints
for i = #barack_blueprints, 1, -1 do
local bp = barack_blueprints[i]
if not bp.complete then
if dist(player.x + player.size / 2, player.y + player.size / 2, bp.x, bp.y) < 40 then
for j = #player_carry, 1, -1 do
local r = player_carry[j]
if bp.required[r] and (bp.progress[r] or 0) < bp.required[r] then
bp.progress[r] = (bp.progress[r] or 0) + 1
table.remove(player_carry, j)
end
end
-- Check if all requirements are fulfilled
local all_done = true
for k, v in pairs(bp.required) do
if (bp.progress[k] or 0) < v then
all_done = false
break
end
end
if all_done then
bp.complete = true
-- Move to main baracks list (completed building)
table.insert(baracks, {
x = bp.x,
y = bp.y,
ready = true,
pending_spawn = false,
resources = {},
robot_timer = 0
})
add_log("Barack built!")
table.remove(barack_blueprints, i)
end
end
end
end
-- Baracks spawn robots for free every 2 seconds
for _, barack in ipairs(baracks) do
barack.robot_timer = barack.robot_timer or 0
barack.robot_timer = barack.robot_timer + dt
if barack.robot_timer >= 2.0 then
barack.robot_timer = barack.robot_timer - 2.0
table.insert(robots, {
x = barack.x,
y = barack.y,
dir = "up",
state = "forward"
})
add_log("Robot spawned from Barack (autospawn every 2s)!")
end
end
-- Robot order/flag logic and movement
for i = #robots, 1, -1 do
local robot = robots[i]
local rx_center = math.floor(robot.x + 0.5)
local ry_center = math.floor(robot.y + 0.5)
local did_flag = false
-- Helper: gets priority for a flag object
local function flag_priority(flag)
if flag.flag_type == "dir" and flag.dir then
return (C.FLAG_PRIORITIES and C.FLAG_PRIORITIES["dir_" .. flag.dir]) or 1
end
return (C.FLAG_PRIORITIES and C.FLAG_PRIORITIES[flag.flag_type]) or 0
end
-- === FLAG LOGIC (PHYSICAL FLAG APPROACH & PRIORITY) ===
-- 1. On spawn, check for available flag and set as approach target if eligible, otherwise start with default up movement
if not robot.init_move then
local best_flag, best_priority = nil, -math.huge
for _, flag in ipairs(flags) do
local fx, fy = math.floor(flag.x + 0.5), math.floor(flag.y + 0.5)
local dist_flag = ((rx_center - fx) ^ 2 + (ry_center - fy) ^ 2) ^ 0.5
local pri = flag_priority(flag)
-- Prevent robot from revisiting last_flag unless they've truly left its radius
if dist_flag <= C.FLAG_COMMAND_RADIUS
and pri > best_priority
and (not robot.last_flag or flag ~= robot.last_flag)
then
best_flag, best_priority = flag, pri
end
end
if best_flag then
robot.state = "approaching_flag"
robot.target_flag = best_flag
robot.target_flag_pos = { x = best_flag.x, y = best_flag.y }
robot.target_flag_priority = best_priority
robot.init_move = true
else
robot.state = "moving_distance"
robot.dir = "up"
robot.distance_left = C.DIR_FLAG_DEFAULT_DISTANCE
robot.current_flag_priority = 0
robot.init_move = true
end
end
-- 2. Scan for (equal or higher) priority flag in range and, if not currently approaching one of equal/greater priority, set to approach
local best_flag, best_priority = robot.target_flag,
robot.target_flag_priority or (robot.current_flag_priority or 0)
for _, flag in ipairs(flags) do
local fx = math.floor(flag.x + 0.5)
local fy = math.floor(flag.y + 0.5)
local dist_flag = ((rx_center - fx) ^ 2 + (ry_center - fy) ^ 2) ^ 0.5
local pri = flag_priority(flag)
-- Prevent robot from picking its last_flag again, unless it has moved away
if dist_flag <= C.FLAG_COMMAND_RADIUS
and pri >= best_priority
and (not robot.last_flag or flag ~= robot.last_flag)
then
best_flag, best_priority = flag, pri
end
end
if best_flag and (not robot.target_flag or best_priority > (robot.target_flag_priority or -math.huge)) and (robot.state ~= "approaching_flag" or best_priority > (robot.target_flag_priority or -math.huge)) then
robot.state = "approaching_flag"
robot.target_flag = best_flag
robot.target_flag_pos = { x = best_flag.x, y = best_flag.y }
robot.target_flag_priority = best_priority
end
-- 3. If robot is in "approaching_flag" state, move toward flag (no command exec until arrival)
if robot.state == "approaching_flag" and robot.target_flag and robot.target_flag_pos then
local tx, ty = robot.target_flag_pos.x, robot.target_flag_pos.y
local dx, dy = tx - robot.x, ty - robot.y
local arrive_dist = 4
local d = (dx * dx + dy * dy) ^ 0.5
if d < arrive_dist then
-- Upload finished! Read and execute the flag
robot.current_flag_priority = robot.target_flag_priority
local flag = robot.target_flag
robot.last_flag = flag -- Prevent re-targeting this flag until moved away
robot.current_flag = flag -- Track currently executing flag for debug viz
robot.target_flag = nil
robot.target_flag_pos = nil
robot.target_flag_priority = nil
if flag.flag_type == "dir" then
robot.dir = flag.dir
robot.state = "moving_distance"
robot.distance_left = flag.distance or C.DIR_FLAG_DEFAULT_DISTANCE
robot._move_axis_hold = nil
elseif flag.flag_type == "splitter" then
-- Splitter logic: track how many robots have entered this flag; send left/right alternately
flag._splitter_count = (flag._splitter_count or 0) + 1
if flag._splitter_count % 2 == 1 then
robot.dir = "left"
else
robot.dir = "right"
end
robot.state = "moving_distance"
robot.distance_left = C.DIR_FLAG_DEFAULT_DISTANCE
robot._move_axis_hold = nil
elseif flag.flag_type == "chop_wood" then
robot.state = "chop_wood_seek"
robot.target_resource = nil
elseif flag.flag_type == "collect_wood" then
robot.state = "collect_wood"
robot.target_item = nil
robot.resource = nil
robot.target_shop = nil
elseif flag.flag_type == "build_something" then
if robot.resource ~= nil then
robot.state = "build_something"
robot.target_blueprint = nil
else
add_log("[Robot] Build flag: no resource, self-destruct.")
-- Drop resource if any
if robot.resource then
local rtype = nil
for _, t in ipairs(RESOURCE_TYPES) do
if t.name == robot.resource then rtype = t end
end
if rtype then
table.insert(resource_items, {
x = robot.x + 8,
y = robot.y + 8,
type = rtype,
picked = false
})
end
end
-- Drop ROBOT_DEATH_DROPS from constants.lua
if C.ROBOT_DEATH_DROPS then
for _, drop in ipairs(C.ROBOT_DEATH_DROPS) do
local drop_type = drop.type
local amount = drop.amount or 1
local rtype = nil
for _, t in ipairs(RESOURCE_TYPES) do
if t.name == drop_type then rtype = t end
end
if rtype then
for count = 1, amount do
table.insert(resource_items, {
x = robot.x + 8 + math.random(-4, 4),
y = robot.y + 8 + math.random(-4, 4),
type = rtype,
picked = false
})
end
end
end
end
table.remove(robots, i)
goto continue_robot_loop
end
end
else
robot.x = robot.x + dx / (d > 0 and d or 1) * (C.ROBOT_SPEED or 60) * dt
robot.y = robot.y + dy / (d > 0 and d or 1) * (C.ROBOT_SPEED or 60) * dt
-- DO NOT run any more logic or states if approaching a flag
goto continue_robot_loop
end
end
-- Only one robot action per frame: if just finished approaching_flag and switched state, skip remaining logic this frame to prevent double-execute:
if robot.state == "approaching_flag" or (robot.target_flag == nil and robot.target_flag_pos == nil and robot.target_flag_priority == nil and (rx_center ~= math.floor(robot.x + 0.5) or ry_center ~= math.floor(robot.y + 0.5))) then
goto continue_robot_loop
end
-- If robot moved away from the last_flag, clear last_flag to allow reactions again
if robot.last_flag then
local fx, fy = math.floor(robot.last_flag.x + 0.5), math.floor(robot.last_flag.y + 0.5)
local far_enough = ((robot.x - fx) ^ 2 + (robot.y - fy) ^ 2) ^ 0.5 > C.FLAG_COMMAND_RADIUS
if far_enough then
robot.last_flag = nil
end
end
-- 3. Movement and distance-based logic (move in direction for set distance)
if robot.state == "moving_distance" then
if not robot._move_axis_hold then
if robot.dir == "left" or robot.dir == "right" then
robot._move_axis_hold = { y = robot.y }
elseif robot.dir == "up" or robot.dir == "down" then
robot._move_axis_hold = { x = robot.x }
else
robot._move_axis_hold = {}
end
end
local d_step = (C.ROBOT_SPEED or 60) * dt
if robot.distance_left ~= nil and robot.distance_left > 0 then
local actual_step = math.min(robot.distance_left, d_step)
if robot.dir == "up" then
robot.y = robot.y - actual_step
if robot._move_axis_hold and robot._move_axis_hold.x then
robot.x = robot._move_axis_hold.x
end
elseif robot.dir == "down" then
robot.y = robot.y + actual_step
if robot._move_axis_hold and robot._move_axis_hold.x then
robot.x = robot._move_axis_hold.x
end
elseif robot.dir == "left" then
robot.x = robot.x - actual_step
if robot._move_axis_hold and robot._move_axis_hold.y then
robot.y = robot._move_axis_hold.y
end
elseif robot.dir == "right" then
robot.x = robot.x + actual_step
if robot._move_axis_hold and robot._move_axis_hold.y then
robot.y = robot._move_axis_hold.y
end
end
robot.distance_left = robot.distance_left - actual_step
if robot.distance_left <= 0 then
-- Snap exactly to axis (avoid overshoot)
if robot.dir == "up" then
robot.y = math.floor(robot.y + 0.5)
if robot._move_axis_hold and robot._move_axis_hold.x then
robot.x = robot._move_axis_hold.x
end
elseif robot.dir == "down" then
robot.y = math.floor(robot.y + 0.5)
if robot._move_axis_hold and robot._move_axis_hold.x then
robot.x = robot._move_axis_hold.x
end
elseif robot.dir == "left" then
robot.x = math.floor(robot.x + 0.5)
if robot._move_axis_hold and robot._move_axis_hold.y then
robot.y = robot._move_axis_hold.y
end
elseif robot.dir == "right" then
robot.x = math.floor(robot.x + 0.5)
if robot._move_axis_hold and robot._move_axis_hold.y then
robot.y = robot._move_axis_hold.y
end
end
robot._move_axis_hold = nil
-- After finishing movement, look for highest-priority flag PHYSICALLY within FLAG_COMMAND_RADIUS of CURRENT POSITION
local best_flag, best_priority = nil, -math.huge
for _, flag in ipairs(flags) do
local fx = math.floor(flag.x + 0.5)
local fy = math.floor(flag.y + 0.5)
local dist_flag = ((robot.x - fx) ^ 2 + (robot.y - fy) ^ 2) ^ 0.5
local pri = flag_priority(flag)
if dist_flag <= C.FLAG_COMMAND_RADIUS and pri >= best_priority then
best_flag = flag
best_priority = pri
end
end
if not best_flag then
add_log("[Robot] Self-destruct after moving distance (no next flag found)")
-- Drop resource if any
if robot.resource then
local rtype = nil
for _, t in ipairs(RESOURCE_TYPES) do
if t.name == robot.resource then rtype = t end
end
if rtype then
table.insert(resource_items, {
x = robot.x + 8,
y = robot.y + 8,
type = rtype,
picked = false
})
end
end
-- Drop ROBOT_DEATH_DROPS from constants.lua
if C.ROBOT_DEATH_DROPS then
for _, drop in ipairs(C.ROBOT_DEATH_DROPS) do
local drop_type = drop.type
local amount = drop.amount or 1
local rtype = nil
for _, t in ipairs(RESOURCE_TYPES) do
if t.name == drop_type then rtype = t end
end
if rtype then
for count = 1, amount do
table.insert(resource_items, {
x = robot.x + 8 + math.random(-4, 4),
y = robot.y + 8 + math.random(-4, 4),
type = rtype,
picked = false
})
end
end
end
end
table.remove(robots, i)
goto continue_robot_loop
else
-- BEGIN: approach new flag physically
robot.state = "approaching_flag"
robot.target_flag = best_flag
robot.target_flag_pos = { x = best_flag.x, y = best_flag.y }
robot.target_flag_priority = best_priority
robot.current_flag = nil -- Clear debug line until next approach/execute
goto continue_robot_loop
-- END approach
end
end
end
elseif robot.state == "chop_wood_seek" then
-- After visiting the flag: seek nearest wood node and go there
local function pick_free_wood_node()
local best_dist, best_src = math.huge, nil
for _, src in ipairs(resource_sources) do
if src.type and src.type.name == "wood" and src.active and src.hp and src.hp > 0 then
local is_claimed = false
for _, rbot in ipairs(robots) do
if rbot ~= robot and rbot.target_resource == src and (rbot.state == "chop_wood_seek" or rbot.state == "chop_wood_chop") then
is_claimed = true
break
end
end
if not is_claimed then
local d = dist(robot.x, robot.y, src.x, src.y)
if d < best_dist then
best_dist = d
best_src = src
end
end
end
end
return best_src
end
if not robot.target_resource or not robot.target_resource.active or not robot.target_resource.hp or robot.target_resource.hp <= 0 then
robot.target_resource = pick_free_wood_node()
if not robot.target_resource then
add_log("[Robot] No wood node available! Self-destruct.")
if robot.resource then
local rtype = nil
for _, t in ipairs(RESOURCE_TYPES) do
if t.name == robot.resource then rtype = t end
end
if rtype then
table.insert(resource_items, {
x = robot.x + 8,
y = robot.y + 8,
type = rtype,
picked = false
})
end
end
if C.ROBOT_DEATH_DROPS then
for _, drop in ipairs(C.ROBOT_DEATH_DROPS) do
local drop_type = drop.type
local amount = drop.amount or 1
local rtype = nil
for _, t in ipairs(RESOURCE_TYPES) do
if t.name == drop_type then rtype = t end
end
if rtype then
for count = 1, amount do
table.insert(resource_items, {
x = robot.x + 8 + math.random(-4, 4),
y = robot.y + 8 + math.random(-4, 4),
type = rtype,
picked = false
})
end
end
end
end
table.remove(robots, i)
goto continue_robot_loop
end
end
if robot.target_resource then
local tx = robot.target_resource.x + TILE_SIZE / 2
local ty = robot.target_resource.y + TILE_SIZE / 2
local dx, dy = tx - robot.x, ty - robot.y
local d = (dx * dx + dy * dy) ^ 0.5
if d < (C.ROBOT_SPEED or 60) * dt then
robot.x, robot.y = tx, ty
-- Arrived: switch to chop state
robot.state = "chop_wood_chop"
else
robot.x = robot.x + dx / (d > 0 and d or 1) * (C.ROBOT_SPEED or 60) * dt
robot.y = robot.y + dy / (d > 0 and d or 1) * (C.ROBOT_SPEED or 60) * dt
end
end
elseif robot.state == "chop_wood_chop" then
-- Chop the node once, then self-destruct
if robot.target_resource and robot.target_resource.active and robot.target_resource.hp and robot.target_resource.hp > 0 then
robot.target_resource.hp = robot.target_resource.hp - 1
-- Drop resource item if depleted now (unified logic)
if robot.target_resource.hp <= 0 and robot.target_resource.type and robot.target_resource.type.name == "wood" then
table.insert(resource_items, {
x = robot.target_resource.x + 10,
y = robot.target_resource.y + 10,
type = robot.target_resource
.type,
picked = false
})
end
if robot.target_resource.hp <= 0 then
robot.target_resource.active = false
robot.target_resource.respawn_timer = 0
end
add_log("[Robot] Chop Wood: hit wood node once.")
if robot.resource then
local rtype = nil
for _, t in ipairs(RESOURCE_TYPES) do
if t.name == robot.resource then rtype = t end
end
if rtype then
table.insert(resource_items, {
x = robot.x + 8,
y = robot.y + 8,
type = rtype,
picked = false
})
end
end
if C.ROBOT_DEATH_DROPS then
for _, drop in ipairs(C.ROBOT_DEATH_DROPS) do
local drop_type = drop.type
local amount = drop.amount or 1
local rtype = nil
for _, t in ipairs(RESOURCE_TYPES) do
if t.name == drop_type then rtype = t end
end
if rtype then
for count = 1, amount do
table.insert(resource_items, {
x = robot.x + 8 + math.random(-4, 4),
y = robot.y + 8 + math.random(-4, 4),
type = rtype,
picked = false
})
end
end
end
end
table.remove(robots, i)
goto continue_robot_loop
else
-- If for any reason node is invalid now, self-destruct
add_log("[Robot] Chop Wood: node was missing, abort.")
if robot.resource then
local rtype = nil
for _, t in ipairs(RESOURCE_TYPES) do
if t.name == robot.resource then rtype = t end
end
if rtype then
table.insert(resource_items, {
x = robot.x + 8,
y = robot.y + 8,
type = rtype,
picked = false
})
end
end
if C.ROBOT_DEATH_DROPS then
for _, drop in ipairs(C.ROBOT_DEATH_DROPS) do
local drop_type = drop.type
local amount = drop.amount or 1
local rtype = nil
for _, t in ipairs(RESOURCE_TYPES) do
if t.name == drop_type then rtype = t end
end
if rtype then
for count = 1, amount do
table.insert(resource_items, {
x = robot.x + 8 + math.random(-4, 4),
y = robot.y + 8 + math.random(-4, 4),
type = rtype,
picked = false
})
end
end
end
end
table.remove(robots, i)
goto continue_robot_loop
end
elseif robot.state == "collect_wood" then
-- Find nearest free wood item, pick up, visibly carry, deliver to shop, self-destruct after
-- Step 1: Find and move to nearest ground wood (not picked)
if not robot.resource then
-- Seek a free wood item on the ground
if not robot.target_item or robot.target_item.picked or robot.target_item.type.name ~= "wood" then
local best_dist, best_item = math.huge, nil
local found_any = false
for _, item in ipairs(resource_items) do
if item.type and item.type.name == "wood" and not item.picked then
found_any = true
local d = dist(robot.x, robot.y, item.x + 2, item.y + 2)
if d < best_dist then
best_dist = d
best_item = item
end
end
end
robot.target_item = best_item
if not found_any then
add_log("[Robot] No ground wood to collect! Self-destruct.")
if robot.resource then
local rtype = nil
for _, t in ipairs(RESOURCE_TYPES) do
if t.name == robot.resource then rtype = t end
end
if rtype then
table.insert(resource_items, {
x = robot.x + 8,
y = robot.y + 8,
type = rtype,
picked = false
})
end
end
if C.ROBOT_DEATH_DROPS then
for _, drop in ipairs(C.ROBOT_DEATH_DROPS) do
local drop_type = drop.type
local amount = drop.amount or 1
local rtype = nil
for _, t in ipairs(RESOURCE_TYPES) do
if t.name == drop_type then rtype = t end
end
if rtype then
for count = 1, amount do
table.insert(resource_items, {
x = robot.x + 8 + math.random(-4, 4),
y = robot.y + 8 + math.random(-4, 4),
type = rtype,
picked = false
})
end
end
end
end
table.remove(robots, i)
goto continue_robot_loop
end
end
if robot.target_item then
local tx = robot.target_item.x + 2
local ty = robot.target_item.y + 2
local dx = tx - robot.x
local dy = ty - robot.y
local d = (dx * dx + dy * dy) ^ 0.5
if d < (C.ROBOT_SPEED or 60) * dt then
robot.x = tx
robot.y = ty
-- Pick it up
if not robot.target_item.picked then
robot.target_item.picked = true
robot.resource = "wood"
add_log("[Robot] Picked up ground wood!")
end
robot.target_item = nil
else
robot.x = robot.x + dx / (d > 0 and d or 1) * (C.ROBOT_SPEED or 60) * dt
robot.y = robot.y + dy / (d > 0 and d or 1) * (C.ROBOT_SPEED or 60) * dt
end
end
else
-- Step 2: Carry it (show visually) to the nearest shop
if not robot.target_shop then
local best_dist, shop = math.huge, nil
for _, zone in ipairs(CHECKOUT_ZONES) do
local zx = zone.x + zone.w / 2
local zy = zone.y + zone.h / 2
local d = dist(robot.x, robot.y, zx, zy)
if d < best_dist then
best_dist = d
shop = zone
end
end
if shop then
robot.target_shop = { x = shop.x + shop.w / 2, y = shop.y + shop.h / 2 }
end
end
if robot.target_shop then
local tx = robot.target_shop.x
local ty = robot.target_shop.y
local dx, dy = tx - robot.x, ty - robot.y
local d = (dx * dx + dy * dy) ^ 0.5
if d < (C.ROBOT_SPEED or 60) * dt then
robot.x = tx
robot.y = ty
-- Deliver and self-destruct
add_log("[Robot] Delivered carried wood to shop.")
-- Drop carried resource if any
if robot.resource then
local rtype = nil
for _, t in ipairs(RESOURCE_TYPES) do
if t.name == robot.resource then rtype = t end
end
if rtype then
table.insert(resource_items, {
x = robot.x + 8,
y = robot.y + 8,
type = rtype,
picked = false
})
end
end
-- Drop ROBOT_DEATH_DROPS from constants.lua
if C.ROBOT_DEATH_DROPS then
for _, drop in ipairs(C.ROBOT_DEATH_DROPS) do
local drop_type = drop.type
local amount = drop.amount or 1
local rtype = nil
for _, t in ipairs(RESOURCE_TYPES) do
if t.name == drop_type then rtype = t end
end
if rtype then
for count = 1, amount do