-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgame_logic_tests.lua
More file actions
1621 lines (1333 loc) · 62 KB
/
game_logic_tests.lua
File metadata and controls
1621 lines (1333 loc) · 62 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
-- game_logic_tests.lua
-- User story validation tests for HemiMUD game engine
-- Run via: cargo test --test test_lua_game_logic
local Test = {}
Test.passed = 0
Test.failed = 0
Test.results = {}
function Test.assert(condition, msg)
if condition then
Test.passed = Test.passed + 1
table.insert(Test.results, { pass = true, msg = msg })
else
Test.failed = Test.failed + 1
table.insert(Test.results, { pass = false, msg = msg })
error("ASSERTION FAILED: " .. msg)
end
end
function Test.assert_eq(a, b, msg)
Test.assert(a == b, msg .. " (expected " .. tostring(b) .. ", got " .. tostring(a) .. ")")
end
function Test.assert_gt(a, b, msg)
Test.assert(a > b, msg .. " (expected > " .. tostring(b) .. ", got " .. tostring(a) .. ")")
end
function Test.assert_error(fn, expected_error, msg)
local ok, err = pcall(fn)
Test.assert(not ok, msg .. " (expected error)")
if expected_error then
Test.assert(string.find(err, expected_error), msg .. " (expected error containing '" .. expected_error .. "')")
end
end
-- ═══════════════════════════════════════════════════════════════════════════
-- SETUP: Create test universe with wizard and player accounts
-- ═══════════════════════════════════════════════════════════════════════════
local function setup_test_world()
-- Create wizard account
local wizard = game.create_object("player", nil, {
name = "TestWizard",
metadata = {
access_level = "wizard",
health = 100,
armor_class = 10
}
})
-- Create regular player account
local player = game.create_object("player", nil, {
name = "TestPlayer",
metadata = {
access_level = "player",
health = 50,
armor_class = 10,
carry_weight = 0,
max_carry = 100
}
})
-- Create builder account with assigned region
local builder = game.create_object("player", nil, {
name = "TestBuilder",
metadata = {
access_level = "builder",
assigned_regions = {}
}
})
return { wizard = wizard, player = player, builder = builder }
end
-- ═══════════════════════════════════════════════════════════════════════════
-- USER STORY 1: REGION CREATION
-- Wizard creates a new region with code and metadata
-- ═══════════════════════════════════════════════════════════════════════════
function test_region_creation_basic()
local actors = setup_test_world()
game.set_actor(actors.wizard.id)
local region = game.create_object("region", nil, {
name = "Haunted Forest",
description = "A dark and mysterious forest filled with ancient trees.",
metadata = {
environment = "forest",
ambient_light = "dim",
ambient_sounds = { "wind", "owls", "rustling" },
danger_level = 3
}
})
Test.assert(region ~= nil, "Region should be created")
Test.assert_eq(region.name, "Haunted Forest", "Region name should match")
Test.assert_eq(region.class, "region", "Region class should be 'region'")
Test.assert_eq(region.metadata.danger_level, 3, "Metadata should be preserved")
end
function test_region_with_attached_code()
local actors = setup_test_world()
game.set_actor(actors.wizard.id)
-- Define code that runs when entities enter the region
local region_code = [[
function on_enter(self, entity)
if entity.class == "player" then
game.send(entity.id, "A chill runs down your spine as you enter " .. self.name .. ".")
end
end
function on_leave(self, entity)
if entity.class == "player" then
game.send(entity.id, "You feel relieved leaving the darkness behind.")
end
end
]]
-- Store code and get hash
local code_hash = game.store_code(region_code)
Test.assert(code_hash ~= nil, "Code should be stored")
Test.assert(#code_hash == 64, "Code hash should be 64 chars (SHA-256)")
-- Create region with code attached
local region = game.create_object("region", nil, {
name = "Haunted Forest",
code_hash = code_hash,
metadata = {
environment = "forest"
}
})
Test.assert_eq(region.code_hash, code_hash, "Region should have code attached")
-- Verify code can be retrieved
local retrieved_code = game.get_code(code_hash)
Test.assert(string.find(retrieved_code, "on_enter"), "Code should contain on_enter handler")
end
function test_region_code_executes_on_enter()
local actors = setup_test_world()
game.set_actor(actors.wizard.id)
-- Create region with on_enter handler
local region_code = [[
function on_enter(self, entity)
entity.metadata.visited_haunted = true
game.update_object(entity.id, { metadata = entity.metadata })
return "Welcome to " .. self.name
end
]]
local code_hash = game.store_code(region_code)
local region = game.create_object("region", nil, {
name = "Haunted Forest",
code_hash = code_hash
})
-- Create room in region
local room = game.create_object("room", region.id, {
name = "Forest Entrance",
metadata = { exits = {} }
})
-- Move player into room (should trigger region's on_enter)
game.move_object(actors.player.id, room.id)
-- Verify handler executed
local updated_player = game.get_object(actors.player.id)
Test.assert(updated_player.metadata.visited_haunted == true, "on_enter handler should have set flag")
end
function test_region_permission_check_wizard_required()
local actors = setup_test_world()
-- Player cannot create region
game.set_actor(actors.player.id)
Test.assert_error(function()
game.create_object("region", nil, { name = "Forbidden Region" })
end, "permission", "Player should not be able to create region")
-- Builder cannot create region
game.set_actor(actors.builder.id)
Test.assert_error(function()
game.create_object("region", nil, { name = "Forbidden Region" })
end, "permission", "Builder should not be able to create region")
-- Wizard can create region
game.set_actor(actors.wizard.id)
local region = game.create_object("region", nil, { name = "Allowed Region" })
Test.assert(region ~= nil, "Wizard should be able to create region")
end
-- ═══════════════════════════════════════════════════════════════════════════
-- USER STORY 2: FIXED OBJECTS
-- Wizard creates room with immovable objects (chair, table)
-- ═══════════════════════════════════════════════════════════════════════════
function test_room_creation_in_region()
local actors = setup_test_world()
game.set_actor(actors.wizard.id)
-- Create region
local region = game.create_object("region", nil, {
name = "Town Square",
metadata = { environment = "urban" }
})
-- Assign builder to this region
actors.builder.metadata.assigned_regions[region.id] = true
game.update_object(actors.builder.id, { metadata = actors.builder.metadata })
-- Builder can now create room in assigned region
game.set_actor(actors.builder.id)
local room = game.create_object("room", region.id, {
name = "Tavern Common Room",
description = "A warm and inviting common room with a roaring fireplace.",
metadata = {
exits = { north = nil, south = nil },
lighting = "warm"
}
})
Test.assert(room ~= nil, "Builder should create room in assigned region")
Test.assert_eq(room.parent_id, region.id, "Room should be child of region")
end
function test_fixed_object_creation()
local actors = setup_test_world()
game.set_actor(actors.wizard.id)
-- Create region and room
local region = game.create_object("region", nil, { name = "Town" })
local room = game.create_object("room", region.id, { name = "Tavern" })
-- Create fixed furniture
local table = game.create_object("item", room.id, {
name = "oak table",
description = "A heavy oak table, scarred by years of use.",
metadata = {
fixed = true,
weight = 200,
material = "wood",
interactions = { "examine", "sit_at", "place_item" }
}
})
local chair = game.create_object("item", room.id, {
name = "wooden chair",
description = "A sturdy wooden chair.",
metadata = {
fixed = true,
weight = 15,
material = "wood",
interactions = { "examine", "sit" }
}
})
Test.assert(table.metadata.fixed == true, "Table should be fixed")
Test.assert(chair.metadata.fixed == true, "Chair should be fixed")
Test.assert_eq(table.parent_id, room.id, "Table should be in room")
end
function test_player_cannot_take_fixed_object()
local actors = setup_test_world()
game.set_actor(actors.wizard.id)
-- Setup room with fixed chair
local region = game.create_object("region", nil, { name = "Town" })
local room = game.create_object("room", region.id, { name = "Tavern" })
local chair = game.create_object("item", room.id, {
name = "wooden chair",
metadata = { fixed = true, weight = 15 }
})
-- Move player to room
game.move_object(actors.player.id, room.id)
-- Player attempts to take chair
game.set_actor(actors.player.id)
local result = Commands.take(actors.player, chair)
Test.assert(result.success == false, "Take should fail for fixed object")
Test.assert(string.find(result.message, "fixed"), "Error message should mention 'fixed'")
-- Verify chair is still in room
local updated_chair = game.get_object(chair.id)
Test.assert_eq(updated_chair.parent_id, room.id, "Chair should still be in room")
end
function test_player_cannot_move_fixed_object()
local actors = setup_test_world()
game.set_actor(actors.wizard.id)
-- Setup
local region = game.create_object("region", nil, { name = "Town" })
local room1 = game.create_object("room", region.id, { name = "Room 1" })
local room2 = game.create_object("room", region.id, { name = "Room 2" })
local table = game.create_object("item", room1.id, {
name = "heavy table",
metadata = { fixed = true }
})
-- Player tries to move table
game.set_actor(actors.player.id)
Test.assert_error(function()
game.move_object(table.id, room2.id)
end, "fixed", "Player should not move fixed object")
-- Verify table is still in room1
local updated_table = game.get_object(table.id)
Test.assert_eq(updated_table.parent_id, room1.id, "Table should still be in room1")
end
function test_wizard_can_move_fixed_object()
local actors = setup_test_world()
game.set_actor(actors.wizard.id)
-- Setup
local region = game.create_object("region", nil, { name = "Town" })
local room1 = game.create_object("room", region.id, { name = "Room 1" })
local room2 = game.create_object("room", region.id, { name = "Room 2" })
local table = game.create_object("item", room1.id, {
name = "heavy table",
metadata = { fixed = true }
})
-- Wizard can move fixed object
game.move_object(table.id, room2.id)
local updated_table = game.get_object(table.id)
Test.assert_eq(updated_table.parent_id, room2.id, "Wizard should move fixed object")
end
function test_fixed_object_can_be_interacted_with()
local actors = setup_test_world()
game.set_actor(actors.wizard.id)
-- Create chair with sit handler
local chair_code = [[
function on_use(self, actor, action)
if action == "sit" then
actor.metadata.sitting_on = self.id
game.update_object(actor.id, { metadata = actor.metadata })
game.broadcast(actor.parent_id, actor.name .. " sits on the " .. self.name .. ".")
return { success = true, message = "You sit down on the chair." }
end
return { success = false, message = "You can't do that." }
end
]]
local code_hash = game.store_code(chair_code)
local region = game.create_object("region", nil, { name = "Town" })
local room = game.create_object("room", region.id, { name = "Tavern" })
local chair = game.create_object("item", room.id, {
name = "wooden chair",
code_hash = code_hash,
metadata = {
fixed = true,
interactions = { "examine", "sit" }
}
})
-- Move player to room
game.move_object(actors.player.id, room.id)
-- Player sits on chair (interaction should work even though fixed)
game.set_actor(actors.player.id)
local result = game.use_object(chair.id, actors.player.id, "sit")
Test.assert(result.success == true, "Player should be able to sit on fixed chair")
local updated_player = game.get_object(actors.player.id)
Test.assert_eq(updated_player.metadata.sitting_on, chair.id, "Player should be sitting on chair")
end
-- ═══════════════════════════════════════════════════════════════════════════
-- USER STORY 3: CUSTOM WEAPON WITH ELEMENTAL DAMAGE + SPAWNER
-- Fire sword +1 with daily spawn from magic chest
-- ═══════════════════════════════════════════════════════════════════════════
function test_define_custom_weapon_class()
game.define_class("fire_sword", {
parent = "weapon",
properties = {
damage_dice = { type = "string", default = "1d8" },
damage_bonus = { type = "number", default = 1 },
damage_type = { type = "string", default = "physical" },
elemental_damage_dice = { type = "string", default = "1d6" },
elemental_damage_type = { type = "string", default = "fire" }
},
handlers = {
on_hit = function(self, attacker, defender, base_result)
-- Elemental damage handled by Combat.attack_extended
return base_result
end
}
})
local class_info = game.get_class("fire_sword")
Test.assert(class_info ~= nil, "fire_sword class should be defined")
Test.assert_eq(class_info.parent, "weapon", "Parent should be weapon")
Test.assert_eq(class_info.properties.damage_bonus.default, 1, "Default +1 damage")
end
function test_create_fire_sword_instance()
local actors = setup_test_world()
game.set_actor(actors.wizard.id)
local region = game.create_object("region", nil, { name = "Dungeon" })
local room = game.create_object("room", region.id, { name = "Treasure Room" })
local sword = game.create_object("fire_sword", room.id, {
name = "Flamebrand +1",
description = "A sword wreathed in magical flames. The blade glows with inner fire.",
metadata = {
damage_dice = "1d8",
damage_bonus = 1,
damage_type = "physical",
elemental_damage_dice = "1d6",
elemental_damage_type = "fire",
value = 500
}
})
Test.assert(sword ~= nil, "Fire sword should be created")
Test.assert_eq(sword.class, "fire_sword", "Class should be fire_sword")
Test.assert_eq(sword.metadata.elemental_damage_type, "fire", "Elemental type should be fire")
end
function test_fire_sword_damage_against_normal_enemy()
local actors = setup_test_world()
game.set_actor(actors.wizard.id)
-- Setup combat scenario
local region = game.create_object("region", nil, { name = "Arena" })
local room = game.create_object("room", region.id, { name = "Combat Pit" })
local sword = game.create_object("fire_sword", actors.player.id, {
name = "Flamebrand +1",
metadata = {
damage_dice = "1d8",
damage_bonus = 1,
elemental_damage_dice = "1d6",
elemental_damage_type = "fire"
}
})
local goblin = game.create_object("bot", room.id, {
name = "Goblin",
metadata = {
health = 30,
armor_class = 12,
-- No immunities, resistances, or vulnerabilities
}
})
-- Move player to room
game.move_object(actors.player.id, room.id)
-- Force a hit with known seed
game.set_rng_seed(12345) -- Seed that produces hit
local initial_health = goblin.metadata.health
local result = Combat.attack_extended(actors.player, goblin, sword)
if result.hit then
local updated_goblin = game.get_object(goblin.id)
Test.assert_gt(initial_health, updated_goblin.metadata.health, "Goblin should take damage")
Test.assert_gt(result.physical_damage, 0, "Physical damage should be dealt")
Test.assert_gt(result.elemental_damage_rolled, 0, "Elemental damage should be rolled")
Test.assert_eq(result.elemental_damage_applied, result.elemental_damage_rolled, "Full fire damage to normal enemy")
end
end
function test_fire_sword_against_fire_immune_enemy()
local actors = setup_test_world()
game.set_actor(actors.wizard.id)
local region = game.create_object("region", nil, { name = "Arena" })
local room = game.create_object("room", region.id, { name = "Fire Pit" })
local sword = game.create_object("fire_sword", actors.player.id, {
name = "Flamebrand +1",
metadata = {
damage_dice = "1d8",
damage_bonus = 1,
elemental_damage_dice = "1d6",
elemental_damage_type = "fire"
}
})
-- Fire elemental is immune to fire
local fire_elemental = game.create_object("bot", room.id, {
name = "Fire Elemental",
metadata = {
health = 50,
armor_class = 13,
immunities = { fire = true } -- IMMUNE TO FIRE
}
})
game.move_object(actors.player.id, room.id)
game.set_rng_seed(12345)
local initial_health = fire_elemental.metadata.health
local result = Combat.attack_extended(actors.player, fire_elemental, sword)
if result.hit then
Test.assert_gt(result.physical_damage, 0, "Physical damage should be dealt")
Test.assert_gt(result.elemental_damage_rolled, 0, "Fire damage was rolled")
Test.assert_eq(result.elemental_damage_applied, 0, "Fire damage should be ZERO (immune)")
local updated = game.get_object(fire_elemental.id)
local damage_taken = initial_health - updated.metadata.health
Test.assert_eq(damage_taken, result.physical_damage, "Only physical damage should be taken")
Test.assert(string.find(result.message, "immune"), "Message should mention immunity")
end
end
function test_fire_sword_against_fire_resistant_enemy()
local actors = setup_test_world()
game.set_actor(actors.wizard.id)
local region = game.create_object("region", nil, { name = "Arena" })
local room = game.create_object("room", region.id, { name = "Cave" })
local sword = game.create_object("fire_sword", actors.player.id, {
name = "Flamebrand +1",
metadata = {
damage_dice = "1d8",
damage_bonus = 1,
elemental_damage_dice = "2d6", -- Larger dice to test halving
elemental_damage_type = "fire"
}
})
-- Red dragon resists fire
local dragon = game.create_object("bot", room.id, {
name = "Young Red Dragon",
metadata = {
health = 100,
armor_class = 18,
resistances = { fire = true } -- RESISTANT TO FIRE (half damage)
}
})
game.move_object(actors.player.id, room.id)
game.set_rng_seed(12345)
local result = Combat.attack_extended(actors.player, dragon, sword)
if result.hit then
local expected_reduced = math.floor(result.elemental_damage_rolled / 2)
Test.assert_eq(result.elemental_damage_applied, expected_reduced, "Fire damage should be halved")
Test.assert(string.find(result.message, "resist"), "Message should mention resistance")
end
end
function test_spawner_chest_creates_sword()
local actors = setup_test_world()
game.set_actor(actors.wizard.id)
-- Define spawner chest class
local chest_code = [[
function on_use(self, actor, action)
if action ~= "open" then
return { success = false, message = "You can only open this chest." }
end
local meta = self.metadata
local now = game.time()
local cooldown = 86400000 -- 24 hours in ms
-- Check cooldown
if meta.last_spawn and (now - meta.last_spawn) < cooldown then
local remaining = math.ceil((cooldown - (now - meta.last_spawn)) / 3600000)
return {
success = false,
message = "The chest is empty. It will replenish in " .. remaining .. " hours."
}
end
-- Spawn the sword
local sword = game.create_object("fire_sword", self.parent_id, {
name = "Flamebrand +1",
description = "A freshly materialized sword, flames dancing along its blade.",
metadata = {
damage_dice = "1d8",
damage_bonus = 1,
elemental_damage_dice = "1d6",
elemental_damage_type = "fire"
}
})
-- Update last spawn time
meta.last_spawn = now
game.update_object(self.id, { metadata = meta })
game.broadcast(self.parent_id, "The chest glows with magical energy and a flaming sword appears!")
return {
success = true,
message = "A Flamebrand +1 materializes from the chest!",
spawned = sword.id
}
end
]]
local code_hash = game.store_code(chest_code)
local region = game.create_object("region", nil, { name = "Dungeon" })
local room = game.create_object("room", region.id, { name = "Treasure Chamber" })
local chest = game.create_object("item", room.id, {
name = "Magic Chest",
description = "An ornate chest covered in fire runes.",
code_hash = code_hash,
metadata = {
fixed = true,
spawns_class = "fire_sword",
spawn_cooldown_ms = 86400000, -- 24 hours
last_spawn = nil
}
})
game.move_object(actors.player.id, room.id)
-- Player opens chest
game.set_actor(actors.player.id)
local result = game.use_object(chest.id, actors.player.id, "open")
Test.assert(result.success == true, "First open should succeed")
Test.assert(result.spawned ~= nil, "Should return spawned item id")
-- Verify sword exists in room
local items = game.get_children(room.id, { class = "fire_sword" })
Test.assert_eq(#items, 1, "One fire sword should be in room")
Test.assert_eq(items[1].name, "Flamebrand +1", "Sword name should match")
end
function test_spawner_chest_respects_cooldown()
local actors = setup_test_world()
game.set_actor(actors.wizard.id)
-- Reuse chest code from above (simplified for test)
local chest_code = [[
function on_use(self, actor, action)
if action ~= "open" then return { success = false } end
local meta = self.metadata
local now = game.time()
local cooldown = 86400000
if meta.last_spawn and (now - meta.last_spawn) < cooldown then
return { success = false, message = "empty" }
end
local sword = game.create_object("fire_sword", self.parent_id, {
name = "Flamebrand +1",
metadata = { damage_dice = "1d8", damage_bonus = 1, elemental_damage_dice = "1d6", elemental_damage_type = "fire" }
})
meta.last_spawn = now
game.update_object(self.id, { metadata = meta })
return { success = true, spawned = sword.id }
end
]]
local code_hash = game.store_code(chest_code)
local region = game.create_object("region", nil, { name = "Dungeon" })
local room = game.create_object("room", region.id, { name = "Vault" })
local chest = game.create_object("item", room.id, {
name = "Magic Chest",
code_hash = code_hash,
metadata = { fixed = true, last_spawn = nil }
})
game.move_object(actors.player.id, room.id)
game.set_actor(actors.player.id)
-- Set time to known value
game.set_time(1000000000000) -- Some timestamp
-- First open: should work
local result1 = game.use_object(chest.id, actors.player.id, "open")
Test.assert(result1.success == true, "First open should succeed")
-- Immediate second open: should fail (cooldown)
local result2 = game.use_object(chest.id, actors.player.id, "open")
Test.assert(result2.success == false, "Second immediate open should fail")
Test.assert(string.find(result2.message, "empty"), "Should say chest is empty")
end
function test_spawner_only_one_sword_per_day()
local actors = setup_test_world()
game.set_actor(actors.wizard.id)
local chest_code = [[
function on_use(self, actor, action)
if action ~= "open" then return { success = false } end
local meta = self.metadata
local now = game.time()
local cooldown = 86400000
if meta.last_spawn and (now - meta.last_spawn) < cooldown then
return { success = false, message = "empty" }
end
local sword = game.create_object("fire_sword", self.parent_id, {
name = "Flamebrand +1",
metadata = { damage_dice = "1d8", damage_bonus = 1, elemental_damage_dice = "1d6", elemental_damage_type = "fire" }
})
meta.last_spawn = now
game.update_object(self.id, { metadata = meta })
return { success = true, spawned = sword.id }
end
]]
local code_hash = game.store_code(chest_code)
local region = game.create_object("region", nil, { name = "Dungeon" })
local room = game.create_object("room", region.id, { name = "Vault" })
local chest = game.create_object("item", room.id, {
name = "Magic Chest",
code_hash = code_hash,
metadata = { fixed = true, last_spawn = nil }
})
game.move_object(actors.player.id, room.id)
game.set_actor(actors.player.id)
local base_time = 1000000000000
game.set_time(base_time)
-- First open
game.use_object(chest.id, actors.player.id, "open")
-- Try 10 more times immediately
for i = 1, 10 do
local result = game.use_object(chest.id, actors.player.id, "open")
Test.assert(result.success == false, "Repeated open #" .. i .. " should fail")
end
-- Count swords in room - should be exactly 1
local swords = game.get_children(room.id, { class = "fire_sword" })
Test.assert_eq(#swords, 1, "Only one sword should exist")
-- Advance time by 25 hours
game.set_time(base_time + (25 * 3600 * 1000))
-- Now should work again
local result = game.use_object(chest.id, actors.player.id, "open")
Test.assert(result.success == true, "Open after cooldown should succeed")
-- Now 2 swords
swords = game.get_children(room.id, { class = "fire_sword" })
Test.assert_eq(#swords, 2, "Second sword should exist after cooldown")
end
-- ═══════════════════════════════════════════════════════════════════════════
-- ADDITIONAL COMBAT SYSTEM TESTS
-- ═══════════════════════════════════════════════════════════════════════════
function test_combat_multiple_damage_types()
local actors = setup_test_world()
game.set_actor(actors.wizard.id)
local region = game.create_object("region", nil, { name = "Arena" })
local room = game.create_object("room", region.id, { name = "Test Arena" })
-- Create entity with multiple immunities and resistances
local golem = game.create_object("bot", room.id, {
name = "Iron Golem",
metadata = {
health = 100,
armor_class = 20,
immunities = { poison = true, psychic = true },
resistances = { physical = true },
vulnerabilities = { lightning = true }
}
})
-- Test poison immunity
local result1 = Combat.deal_damage(golem.id, 20, "poison")
Test.assert_eq(result1.applied, 0, "Poison damage should be 0 (immune)")
-- Test physical resistance
local result2 = Combat.deal_damage(golem.id, 20, "physical")
Test.assert_eq(result2.applied, 10, "Physical damage should be halved (10)")
-- Test lightning vulnerability
local result3 = Combat.deal_damage(golem.id, 20, "lightning")
Test.assert_eq(result3.applied, 40, "Lightning damage should be doubled (40)")
-- Test fire (no modifier)
local result4 = Combat.deal_damage(golem.id, 20, "fire")
Test.assert_eq(result4.applied, 20, "Fire damage should be normal (20)")
end
function test_combat_death_handling()
local actors = setup_test_world()
game.set_actor(actors.wizard.id)
local region = game.create_object("region", nil, { name = "Arena" })
local room = game.create_object("room", region.id, { name = "Death Test" })
local minion = game.create_object("npc", room.id, {
name = "Weak Minion",
metadata = { health = 5, armor_class = 8 }
})
-- Deal lethal damage
Combat.deal_damage(minion.id, 10, "physical")
local updated = game.get_object(minion.id)
Test.assert_eq(updated.metadata.health, 0, "Health should be 0")
end
-- ═══════════════════════════════════════════════════════════════════════════
-- COMBAT LOOP TESTS (PvM and PvP)
-- ═══════════════════════════════════════════════════════════════════════════
function test_combat_initiation_pvm()
local actors = setup_test_world()
game.set_actor(actors.wizard.id)
local region = game.create_object("region", nil, { name = "Dungeon" })
local room = game.create_object("room", region.id, { name = "Monster Den" })
local goblin = game.create_object("npc", room.id, {
name = "Goblin",
metadata = {
health = 20,
max_health = 20,
armor_class = 12,
in_combat = false,
attacking = nil,
attackers = {}
}
})
-- Move player to room
game.move_object(actors.player.id, room.id)
game.set_actor(actors.player.id)
-- Initiate combat
local result = Combat.initiate(actors.player, goblin)
Test.assert(result.success, "Combat initiation should succeed")
-- Check player combat state
local updated_player = game.get_object(actors.player.id)
Test.assert(updated_player.metadata.in_combat, "Player should be in combat")
Test.assert_eq(updated_player.metadata.attacking, goblin.id, "Player should be attacking goblin")
-- Check goblin combat state
local updated_goblin = game.get_object(goblin.id)
Test.assert(updated_goblin.metadata.in_combat, "Goblin should be in combat")
Test.assert(updated_goblin.metadata.attackers[actors.player.id], "Player should be in goblin's attackers")
Test.assert_eq(updated_goblin.metadata.attacking, actors.player.id, "Goblin should auto-retaliate")
end
function test_combat_pvp_disabled()
local actors = setup_test_world()
game.set_actor(actors.wizard.id)
local region = game.create_object("region", nil, { name = "Town" })
local room = game.create_object("room", region.id, { name = "Town Square" })
-- Create another player
local player2 = game.create_object("player", room.id, {
name = "OtherPlayer",
metadata = {
access_level = "player",
health = 50,
in_combat = false,
attackers = {}
}
})
-- Set universe to PvP disabled
local universe = game.get_universe()
universe.metadata.pvp_mode = Combat.PVP_MODES.DISABLED
game.update_universe(universe)
-- Move first player to room
game.move_object(actors.player.id, room.id)
game.set_actor(actors.player.id)
-- Try to attack other player
local result = Combat.initiate(actors.player, player2)
Test.assert(not result.success, "PvP should be blocked when disabled")
Test.assert(not actors.player.metadata.in_combat, "Player should not be in combat")
end
function test_combat_pvp_arena_only()
local actors = setup_test_world()
game.set_actor(actors.wizard.id)
local region = game.create_object("region", nil, { name = "Town" })
local town_square = game.create_object("room", region.id, {
name = "Town Square",
metadata = { is_arena = false }
})
local arena = game.create_object("room", region.id, {
name = "Arena",
metadata = { is_arena = true }
})
local player2 = game.create_object("player", nil, {
name = "OtherPlayer",
metadata = { access_level = "player", health = 50, in_combat = false, attackers = {} }
})
-- Set universe to arena-only PvP
local universe = game.get_universe()
universe.metadata.pvp_mode = Combat.PVP_MODES.ARENA_ONLY
game.update_universe(universe)
-- Test in town square (non-arena)
game.move_object(actors.player.id, town_square.id)
game.move_object(player2.id, town_square.id)
game.set_actor(actors.player.id)
local result1 = Combat.initiate(actors.player, player2)
Test.assert(not result1.success, "PvP should be blocked outside arena")
-- Test in arena
game.move_object(actors.player.id, arena.id)
game.move_object(player2.id, arena.id)
local result2 = Combat.initiate(actors.player, player2)
Test.assert(result2.success, "PvP should be allowed in arena")
end
function test_combat_pvp_flagged()
local actors = setup_test_world()
game.set_actor(actors.wizard.id)
local region = game.create_object("region", nil, { name = "Wilderness" })
local room = game.create_object("room", region.id, { name = "Forest" })
local player2 = game.create_object("player", room.id, {
name = "OtherPlayer",
metadata = { access_level = "player", health = 50, in_combat = false, attackers = {}, pvp_flagged = false }
})
-- Set universe to flagged PvP
local universe = game.get_universe()
universe.metadata.pvp_mode = Combat.PVP_MODES.FLAGGED
game.update_universe(universe)
game.move_object(actors.player.id, room.id)
actors.player.metadata.pvp_flagged = false
game.set_actor(actors.player.id)
-- Neither flagged - should fail
local result1 = Combat.initiate(actors.player, player2)
Test.assert(not result1.success, "PvP blocked when neither flagged")
-- Only attacker flagged - should fail
actors.player.metadata.pvp_flagged = true
game.update_object(actors.player.id, { metadata = actors.player.metadata })
local result2 = Combat.initiate(actors.player, player2)
Test.assert(not result2.success, "PvP blocked when only attacker flagged")
-- Both flagged - should succeed
player2.metadata.pvp_flagged = true
game.update_object(player2.id, { metadata = player2.metadata })
local result3 = Combat.initiate(actors.player, player2)
Test.assert(result3.success, "PvP allowed when both flagged")
end
function test_combat_heartbeat_round()
local actors = setup_test_world()
game.set_actor(actors.wizard.id)
local region = game.create_object("region", nil, { name = "Arena" })
local room = game.create_object("room", region.id, { name = "Combat Pit" })
local goblin = game.create_object("npc", room.id, {
name = "Goblin",
metadata = {
health = 100, -- High HP to survive multiple rounds
max_health = 100,
armor_class = 10,
in_combat = false,
attacking = nil,
attackers = {}
}
})
-- Give player a weapon
local sword = game.create_object("weapon", actors.player.id, {
name = "Iron Sword",
metadata = { damage_dice = "1d8", damage_bonus = 0 }
})
actors.player.metadata.wielded = sword.id
actors.player.metadata.health = 100
actors.player.metadata.max_health = 100
game.update_object(actors.player.id, { metadata = actors.player.metadata })
game.move_object(actors.player.id, room.id)
game.set_actor(actors.player.id)