diff --git a/data/area/kandarin/feldip_hills/feldip_hills.npc-spawns.toml b/data/area/kandarin/feldip_hills/feldip_hills.npc-spawns.toml index c5b27b7ad9..6e4df04ab2 100644 --- a/data/area/kandarin/feldip_hills/feldip_hills.npc-spawns.toml +++ b/data/area/kandarin/feldip_hills/feldip_hills.npc-spawns.toml @@ -159,21 +159,21 @@ spawns = [ { id = "black_warlock_feldip_hills", x = 2558, y = 2914 }, { id = "black_warlock_feldip_hills", x = 2563, y = 2920 }, { id = "black_warlock_feldip_hills", x = 2566, y = 2886 }, - { id = "barb_tailed_kebbit_feldip_hills", x = 2565, y = 2903 }, - { id = "barb_tailed_kebbit_feldip_hills", x = 2567, y = 2903 }, - { id = "barb_tailed_kebbit_feldip_hills", x = 2572, y = 2929 }, - { id = "barb_tailed_kebbit_feldip_hills", x = 2572, y = 2931 }, - { id = "barb_tailed_kebbit_feldip_hills", x = 2573, y = 2896 }, - { id = "barb_tailed_kebbit_feldip_hills", x = 2573, y = 2898 }, - { id = "barb_tailed_kebbit_feldip_hills", x = 2574, y = 2909 }, - { id = "barb_tailed_kebbit_feldip_hills", x = 2574, y = 2911 }, - { id = "barb_tailed_kebbit_feldip_hills", x = 2575, y = 2916 }, - { id = "barb_tailed_kebbit_feldip_hills", x = 2575, y = 2926 }, - { id = "barb_tailed_kebbit_feldip_hills", x = 2577, y = 2926 }, - { id = "barb_tailed_kebbit_feldip_hills", x = 2581, y = 2884 }, - { id = "barb_tailed_kebbit_feldip_hills", x = 2581, y = 2886 }, - { id = "barb_tailed_kebbit_feldip_hills", x = 2584, y = 2912 }, - { id = "barb_tailed_kebbit_feldip_hills", x = 2584, y = 2914 }, + { id = "barb_tailed_kebbit", x = 2565, y = 2903 }, + { id = "barb_tailed_kebbit", x = 2567, y = 2903 }, + { id = "barb_tailed_kebbit", x = 2572, y = 2929 }, + { id = "barb_tailed_kebbit", x = 2572, y = 2931 }, + { id = "barb_tailed_kebbit", x = 2573, y = 2896 }, + { id = "barb_tailed_kebbit", x = 2573, y = 2898 }, + { id = "barb_tailed_kebbit", x = 2574, y = 2909 }, + { id = "barb_tailed_kebbit", x = 2574, y = 2911 }, + { id = "barb_tailed_kebbit", x = 2575, y = 2916 }, + { id = "barb_tailed_kebbit", x = 2575, y = 2926 }, + { id = "barb_tailed_kebbit", x = 2577, y = 2926 }, + { id = "barb_tailed_kebbit", x = 2581, y = 2884 }, + { id = "barb_tailed_kebbit", x = 2581, y = 2886 }, + { id = "barb_tailed_kebbit", x = 2584, y = 2912 }, + { id = "barb_tailed_kebbit", x = 2584, y = 2914 }, { id = "spined_larupia", x = 2544, y = 2910, members = true }, { id = "spined_larupia", x = 2550, y = 2904, members = true }, { id = "spined_larupia", x = 2556, y = 2895, members = true }, diff --git a/data/area/kandarin/feldip_hills/feldip_hills.npcs.toml b/data/area/kandarin/feldip_hills/feldip_hills.npcs.toml index 2cfdabb789..9e431d7379 100644 --- a/data/area/kandarin/feldip_hills/feldip_hills.npcs.toml +++ b/data/area/kandarin/feldip_hills/feldip_hills.npcs.toml @@ -55,7 +55,7 @@ respawn_delay = 10 collision = "sky" examine = "A black warlock. The air seems to distort wherever it passes." -[barb_tailed_kebbit_feldip_hills] +[barb_tailed_kebbit] id = 5088 examine = "It uses its tail to hunt and skewer fish." diff --git a/data/area/kandarin/piscatoris/falconry_area/piscatoris_falconry_area.npcs.toml b/data/area/kandarin/piscatoris/falconry_area/piscatoris_falconry_area.npcs.toml index 29b8f6c952..f993d253bb 100644 --- a/data/area/kandarin/piscatoris/falconry_area/piscatoris_falconry_area.npcs.toml +++ b/data/area/kandarin/piscatoris/falconry_area/piscatoris_falconry_area.npcs.toml @@ -5,3 +5,15 @@ examine = "He's wearing a single glove; how strange." [spotted_kebbit] id = 5098 examine = "It seems to be on a permanent sugar rush." + +[spotted_kebbit_caught] +id = 5094 +examine = "It looks like it has caught something." + +[dark_kebbit_caught] +id = 5095 +examine = "It looks like it has caught something." + +[dashing_kebbit_caught] +id = 5096 +examine = "It looks like it has caught something." diff --git a/data/area/kandarin/piscatoris/piscatoris.areas.toml b/data/area/kandarin/piscatoris/piscatoris.areas.toml index 362f8e315f..a44b71ac56 100644 --- a/data/area/kandarin/piscatoris/piscatoris.areas.toml +++ b/data/area/kandarin/piscatoris/piscatoris.areas.toml @@ -19,3 +19,7 @@ hint = "where eagles fly." [ppiscatoris_fishing_area] x = [2340, 2349] y = [3702, 3702] + +[piscatoris_falconry_area] +x = [2363, 2395] +y = [3579, 3612] diff --git a/data/entity/npc/hunt_modes.toml b/data/entity/npc/hunt_modes.toml index 2e5d655560..3bb61c8a4a 100644 --- a/data/entity/npc/hunt_modes.toml +++ b/data/entity/npc/hunt_modes.toml @@ -148,3 +148,24 @@ id = "swamp_lizard,orange_salamander,red_salamander,black_salamander" check_visual = "line_of_sight" check_not_busy = true find_keep_hunting = true + +[box_trap] +type = "npc" +id = "chinchompa,carnivorous_chinchompa,ferret,pawya,grenwall" +check_visual = "line_of_sight" +check_not_busy = true +find_keep_hunting = true + +[deadfall] +type = "npc" +id = "wild_kebbit,barb_tailed_kebbit,prickly_kebbit,sabre_toothed_kebbit" +check_visual = "line_of_sight" +check_not_busy = true +find_keep_hunting = true + +[magic_box] +type = "npc" +id = "imp" +check_visual = "line_of_sight" +check_not_busy = true +find_keep_hunting = true diff --git a/data/skill/hunter/hunter.anims.toml b/data/skill/hunter/hunter.anims.toml index 01d91822df..b5b401a281 100644 --- a/data/skill/hunter/hunter.anims.toml +++ b/data/skill/hunter/hunter.anims.toml @@ -35,11 +35,38 @@ id = 5172 [chinchompa_catch] id = 5184 +[chinchompa_fail] +id = 5185 + +[ferret_catch] +id = 5191 + +[ferret_fail] +id = 5192 + +[pawya_catch] +id = 8615 + +[pawya_fail] +id = 8611 + +[grenwall_catch] +id = 8602 + +[grenwall_fail] +id = 8602 + +[imp_catch] +id = 5218 + +[imp_fail] +id = 5285 + [kebbit_catch] -id = 5208 +id = 5275 [kebbit_fail] -id = 5208 +id = 5277 [net_catch] id = 827 @@ -74,3 +101,15 @@ id = 5227 [big_cat_death] id = 5234 ticks = 2 + +[noose_catch_polar] +id = 5256 + +[noose_catch_common] +id = 5259 + +[noose_fail] +id = 5255 + +[noose_catch_desert] +id = 5257 diff --git a/data/skill/hunter/hunter.ifaces.toml b/data/skill/hunter/hunter.ifaces.toml new file mode 100644 index 0000000000..c24636e261 --- /dev/null +++ b/data/skill/hunter/hunter.ifaces.toml @@ -0,0 +1,19 @@ +[imp_box] +id = 478 +type = "main_screen" + +[.title] +id = 12 + +[.text] +id = 13 + +[.inventory] +id = 14 +inventory = "inventory" +width = 7 +height = 4 +options = { Deposit = 0, Examine = 9 } + +[.close] +id = 15 diff --git a/data/skill/hunter/hunter.npcs.toml b/data/skill/hunter/hunter.npcs.toml index c4768ed732..cb34749f82 100644 --- a/data/skill/hunter/hunter.npcs.toml +++ b/data/skill/hunter/hunter.npcs.toml @@ -8,7 +8,7 @@ id = 5071 [hunting_box_trap_npc] id = 5077 -hunt_mode = "hunter_trap" +hunt_mode = "box_trap" hunt_range = 2 [hunting_box_trap_npc_off] @@ -16,7 +16,7 @@ id = 5078 [hunting_deadfall_trap_npc] id = 5090 -hunt_mode = "hunter_trap" +hunt_mode = "deadfall" hunt_range = 3 [hunting_deadfall_trap_npc_off] @@ -24,7 +24,7 @@ id = 5091 [hunting_imptrap_npc] id = 5101 -hunt_mode = "hunter_trap" +hunt_mode = "magic_box" hunt_range = 2 [hunting_imptrap_npc_off] @@ -48,7 +48,7 @@ id = 5119 [afr_hunting_deadfall_trap_npc] # As a first resort id = 7037 -hunt_mode = "hunter_trap" +hunt_mode = "deadfall" hunt_range = 3 [afr_hunting_deadfall_trap_npc_off] diff --git a/data/skill/hunter/hunter.objs.toml b/data/skill/hunter/hunter.objs.toml index 8440c17cdd..f88c79b5c6 100644 --- a/data/skill/hunter/hunter.objs.toml +++ b/data/skill/hunter/hunter.objs.toml @@ -169,12 +169,31 @@ examine = "There's something caught in it." id = 28930 examine = "There's something caught in it." + +[magic_box] +id = 19223 + +[magic_box_fail] +id = 19224 + +[magic_box_catching] +id = 19225 + +[magic_box_caught] +id = 19226 + # Box trap — triggered states -[box_trap_grey_chinchompa] +[box_trap_catching] +id = 19188 + +[box_trap_ferret] +id = 19190 + +[box_trap_chinchompa] id = 28557 examine = "It looks like there is something caught inside this trap." -[box_trap_red_chinchompa] +[box_trap_carnivorous_chinchompa] id = 28558 examine = "It looks like there is something caught inside this trap." @@ -184,11 +203,36 @@ examine = "It looks like there is something caught inside this trap." [box_trap_grenwall] id = 28906 - examine = "It looks like there is something caught inside this trap." -# Boulder trap — triggered state -[boulder_trap_barb_tailed_kebbit] + +# Boulder trap — falling boulder states (per creature, boulder mid-crush) +[boulder_trap_wild_kebbit_catching] +id = 19213 + +[boulder_trap_barb_tailed_kebbit_catching] +id = 19211 + +[boulder_trap_prickly_kebbit_catching] id = 19207 + +[boulder_trap_sabre_toothed_kebbit_catching] +id = 19209 + +# Boulder trap — caught states (boulder dropped on creature) +[boulder_trap_wild_kebbit] +id = 19215 +examine = "Someone could get their fingers caught in that." + +[boulder_trap_sabre_toothed_kebbit] +id = 19216 +examine = "Someone could get their fingers caught in that." + +[boulder_trap_barb_tailed_kebbit] +id = 19217 +examine = "Someone could get their fingers caught in that." + +[boulder_trap_prickly_kebbit] +id = 19218 examine = "Someone could get their fingers caught in that." [pitfall_sabre_toothed_kyatt_0] @@ -277,3 +321,284 @@ examine = "Something has fallen into this trap." [pitfall_inverse_sabre_toothed_kyatt] id = 19236 examine = "Something has fallen into this trap." + +# Kebbit tracking +[kebbit_tracks_plant_0] +id = 19356 + +[kebbit_tracks_plant_1] +id = 19357 + +[kebbit_tracks_plant_2] +id = 19358 + +[kebbit_tracks_plant_3] +id = 19359 + +[kebbit_tracks_plant_4] +id = 19360 + +[kebbit_tracks_plant_5] +id = 19361 + +[kebbit_tracks_plant_6] +id = 19362 + +[kebbit_tracks_plant_7] +id = 19363 + +[kebbit_tracks_plant_8] +id = 19364 + +[kebbit_tracks_plant_9] +id = 19365 + +[kebbit_tracks_plant_10] +id = 19372 + +[kebbit_tracks_plant_11] +id = 19373 + +[kebbit_tracks_plant_12] +id = 19374 + +[kebbit_tracks_plant_13] +id = 19375 + +[kebbit_tracks_plant_14] +id = 19376 + +[kebbit_tracks_plant_15] +id = 19377 + +[kebbit_tracks_plant_16] +id = 19378 + +[kebbit_tracks_plant_17] +id = 19379 + +[kebbit_tracks_plant_18] +id = 19380 + +[kebbit_tunnel_0] +id = 19419 + +[kebbit_tunnel_1] +id = 19420 + +[kebbit_tunnel_2] +id = 19421 + +[kebbit_tunnel_3] +id = 19423 + +[kebbit_tunnel_4] +id = 19424 + +[kebbit_tunnel_5] +id = 19426 + +[kebbit_hollow_log_0] +id = 36688 + +[kebbit_hollow_log_1] +id = 36689 + +[kebbit_hollow_log_2] +id = 36690 + +[kebbit_bush] +id = 19428 + +[kebbit_snow_drift] +id = 19435 + +[common_kebbit_burrow] +id = 19439 + +[common_kebbit_burrow_2] +id = 19440 + +[polar_kebbit_hole] +id = 19640 + +[polar_kebbit_hole_2] +id = 19641 + +# Desert devil tracking +[desert_cactus_0] +id = 19388 + +[desert_cactus_1] +id = 19389 + +[desert_cactus_2] +id = 19391 + +[desert_cactus_3] +id = 19392 + +[desert_cactus_4] +id = 19394 + +[desert_cactus_5] +id = 19395 + +[desert_cactus_6] +id = 19398 + +[desert_cactus_7] +id = 19399 + +[desert_cactus_8] +id = 19401 + +[desert_rockslide_0] +id = 19390 + +[desert_rockslide_1] +id = 19393 + +[desert_rockslide_2] +id = 19396 + +[desert_rockslide_3] +id = 19397 + +[desert_rockslide_4] +id = 19400 + +[desert_rockslide_5] +id = 19402 + +[disturbed_sand] +id = 19430 + +[desert_devil_burrow] +id = 19552 + +[desert_devil_burrow_2] +id = 19553 + +# Feldip weasel tracking +[feldip_plant_0] +id = 19403 + +[feldip_plant_1] +id = 19404 + +[feldip_plant_2] +id = 19405 + +[feldip_plant_3] +id = 19406 + +[feldip_plant_4] +id = 19407 + +[feldip_plant_5] +id = 19408 + +[feldip_plant_6] +id = 19409 + +[feldip_plant_7] +id = 19410 + +[feldip_plant_8] +id = 19411 + +[feldip_plant_9] +id = 19412 + +[feldip_plant_10] +id = 19413 + +[feldip_plant_11] +id = 19414 + +[feldip_plant_12] +id = 19415 + +[feldip_plant_13] +id = 19416 + +[feldip_plant_14] +id = 19417 + +[weasel_bush] +id = 19427 + +[feldip_weasel_burrow] +id = 19594 + +[feldip_weasel_burrow_2] +id = 19593 + +# Razor-backed kebbit tracking + north common kebbit burrow +[common_kebbit_burrow_3] +id = 19438 + +[razor_backed_kebbit_burrow] +id = 19492 + +[razor_backed_kebbit_burrow_2] +id = 19493 + +[razor_backed_kebbit_burrow_3] +id = 19494 + +[razor_kebbit_bush] +id = 19429 + +[kebbit_tracks_plant_19] +id = 19339 + +[kebbit_tracks_plant_20] +id = 19340 + +[kebbit_tracks_plant_21] +id = 19341 + +[kebbit_tracks_plant_22] +id = 19342 + +[kebbit_tracks_plant_23] +id = 19343 + +[kebbit_tracks_plant_24] +id = 19344 + +[kebbit_tracks_plant_25] +id = 19345 + +[kebbit_tracks_plant_26] +id = 19346 + +[kebbit_tracks_plant_27] +id = 19347 + +[kebbit_tracks_plant_28] +id = 19348 + +[kebbit_tracks_plant_29] +id = 19349 + +[kebbit_tracks_plant_30] +id = 19350 + +[kebbit_tracks_plant_31] +id = 19351 + +[kebbit_tracks_plant_32] +id = 19352 + +[kebbit_tracks_plant_33] +id = 19353 + +[kebbit_tracks_plant_34] +id = 19354 + +[kebbit_tracks_plant_35] +id = 19355 + diff --git a/data/skill/hunter/hunter.sounds.toml b/data/skill/hunter/hunter.sounds.toml index 2c426504f0..2051391794 100644 --- a/data/skill/hunter/hunter.sounds.toml +++ b/data/skill/hunter/hunter.sounds.toml @@ -45,3 +45,24 @@ id = 666 [big_cat_death] id = 667 + +[lay_box_trap] +id = 2636 + +[box_trap_catch] +id = 2627 + +[set_deadfall] +id = 2645 + +[deadfall_catch] +id = 2631 + +[falcon_swoop] +id = 2634 + +[falcon_return] +id = 2633 + +[hunting_noose] +id = 2637 diff --git a/data/skill/hunter/hunter.tables.toml b/data/skill/hunter/hunter.tables.toml index 3fa68fbe8b..9bddf53dcb 100644 --- a/data/skill/hunter/hunter.tables.toml +++ b/data/skill/hunter/hunter.tables.toml @@ -33,15 +33,16 @@ npc = "hunting_box_trap_npc" [.boulder_trap] level = 23 -#trap = "boulder_trap_setup" -#fail = "boulder_trap_fail" -#setup_anim = "lay_trap" -#take_down_anim = "lay_trap" -#requires = ["knife"] +requires = ["knife"] items = ["logs"] max = 1 npc = "hunting_deadfall_trap_npc" +[.magic_box] +level = 71 +items = ["magic_box"] +npc = "hunting_imptrap_npc" + [.swamp_net] level = 29 bait = "guam_tar" @@ -90,6 +91,9 @@ level = "int" # minimum hunter level to lure xp = "int" # xp * 0.1 (stored as tenths for int precision) loot = "list" # items awarded on catch chance = "range" +bait = "item" # bait required before the creature will approach the trap +catch_anim = "anim" # animation the creature plays on a successful catch +fail_anim = "anim" # animation the creature plays on a failed catch [.crimson_swift] trap = "bird_snare" @@ -139,30 +143,75 @@ xp = 1670 loot = ["wimpy_feather", "raw_bird_meat", "bones"] chance = [65, 350] -[.grey_chinchompa] +[.ferret] +trap = "box_trap" +caught_obj = "box_trap_ferret" +level = 27 +xp = 1152 +catch_anim = "ferret_catch" +fail_anim = "ferret_fail" +loot = ["ferret"] +chance = [41, 255] + +[.chinchompa] trap = "box_trap" -caught_obj = "box_trap_grey_chinchompa" +caught_obj = "box_trap_chinchompa" level = 53 xp = 1984 -#catch_anim = "chinchompa_catch" -loot = ["box_trap", "chinchompa"] +catch_anim = "chinchompa_catch" +fail_anim = "chinchompa_fail" +loot = ["chinchompa"] +chance = [145, 268] -[.red_chinchompa] +[.carnivorous_chinchompa] trap = "box_trap" -caught_obj = "box_trap_red_chinchompa" +caught_obj = "box_trap_carnivorous_chinchompa" level = 63 xp = 2650 -#catch_anim = "chinchompa_catch" -loot = ["box_trap", "red_chinchompa"] +catch_anim = "chinchompa_catch" +fail_anim = "chinchompa_fail" +loot = ["red_chinchompa"] +chance = [115, 228] + +[.wild_kebbit] +trap = "boulder_trap" +caught_obj = "boulder_trap_wild_kebbit" +level = 23 +xp = 1280 +catch_anim = "kebbit_catch" +fail_anim = "kebbit_fail" +loot = ["bones", "kebbit_claws"] +chance = [105, 354] [.barb_tailed_kebbit] trap = "boulder_trap" caught_obj = "boulder_trap_barb_tailed_kebbit" -level = 23 +level = 33 xp = 1680 -#catch_anim = "kebbit_catch" -#fail_anim = "kebbit_fail" +catch_anim = "kebbit_catch" +fail_anim = "kebbit_fail" loot = ["bones", "barb_tail_harpoon"] +chance = [110, 326] + +[.prickly_kebbit] +trap = "boulder_trap" +caught_obj = "boulder_trap_prickly_kebbit" +level = 37 +xp = 2040 +catch_anim = "kebbit_catch" +fail_anim = "kebbit_fail" +loot = ["bones", "kebbit_spike"] +chance = [115, 298] + +[.sabre_toothed_kebbit] +trap = "boulder_trap" +caught_obj = "boulder_trap_sabre_toothed_kebbit" +level = 51 +xp = 2000 +catch_anim = "kebbit_catch" +fail_anim = "kebbit_fail" +loot = ["bones", "kebbit_teeth"] +chance = [120, 268] [.swamp_lizard] trap = "swamp_net" @@ -197,17 +246,33 @@ trap = "box_trap" caught_obj = "box_trap_pawya" level = 66 xp = 4000 -#catch_anim = "chinchompa_catch" +catch_anim = "pawya_catch" +fail_anim = "pawya_fail" loot = ["bones", "raw_pawya_meat"] +chance = [100, 215] +bait = "papaya_fruit" [.grenwall] trap = "box_trap" caught_obj = "box_trap_grenwall" level = 77 xp = 11000 -#catch_anim = "chinchompa_catch" +catch_anim = "grenwall_catch" +fail_anim = "grenwall_fail" +chance = [15, 130] +bait = "raw_pawya_meat" loot = ["grenwall_spikes", "grenwall_spikes", "grenwall_spikes", "grenwall_spikes", "grenwall_spikes", "grenwall_spikes", "grenwall_spikes", "grenwall_spikes", "grenwall_spikes", "grenwall_spikes", "grenwall_spikes", "grenwall_spikes", "grenwall_spikes", "grenwall_spikes", "grenwall_spikes", "grenwall_spikes", "grenwall_spikes", "grenwall_spikes"] +[.imp] +trap = "magic_box" +caught_obj = "magic_box_caught" +level = 71 +xp = 4500 +catch_anim = "imp_catch" +fail_anim = "imp_fail" +loot = ["imp_in_a_box_2"] +chance = [15, 130] + [.rabbit] trap = "rabbit_snare" caught_obj = "rabbit_snare_caught" @@ -231,3 +296,35 @@ loot = ["big_bones", "tatty_graahk_fur"] level = 55 xp = 3000 loot = ["big_bones", "tatty_kyatt_fur"] + + +# --------------------------------------------------------------------------- +# Falconry — keyed by the kebbit npc id +# --------------------------------------------------------------------------- +[falconry] +level = "int" +xp = "int" # xp * 0.1 +fur = "item" +caught = "npc" +chance = "range" + +[.spotted_kebbit] +level = 43 +xp = 1040 +fur = "spotted_kebbit_fur" +caught = "spotted_kebbit_caught" +chance = [26, 310] + +[.dark_kebbit] +level = 57 +xp = 1320 +fur = "dark_kebbit_fur" +caught = "dark_kebbit_caught" +chance = [0, 253] + +[.dashing_kebbit] +level = 69 +xp = 1560 +fur = "dashing_kebbit_fur" +caught = "dashing_kebbit_caught" +chance = [0, 205] diff --git a/data/skill/hunter/hunter.varbits.toml b/data/skill/hunter/hunter.varbits.toml index 12ff313409..d8ab194c27 100644 --- a/data/skill/hunter/hunter.varbits.toml +++ b/data/skill/hunter/hunter.varbits.toml @@ -85,4 +85,307 @@ values = ["empty", "spiked", "collapsed", "caught", "inverse"] [debug_traps] id = 3075 -format = "boolean" \ No newline at end of file +format = "boolean" +[common_kebbit_trail_0] +id = 2974 +format = "int" + +[common_kebbit_trail_1] +id = 2975 +format = "int" + +[common_kebbit_trail_2] +id = 2976 +format = "int" + +[common_kebbit_trail_3] +id = 2977 +format = "int" + +[common_kebbit_trail_4] +id = 2978 +format = "int" + +[common_kebbit_trail_5] +id = 2979 +format = "int" + +[common_kebbit_trail_6] +id = 2980 +format = "int" + +[common_kebbit_trail_7] +id = 2981 +format = "int" + +[common_kebbit_trail_8] +id = 2982 +format = "int" + +[common_kebbit_trail_9] +id = 2983 +format = "int" + +[polar_kebbit_trail_0] +id = 3053 +format = "int" + +[polar_kebbit_trail_1] +id = 3054 +format = "int" + +[polar_kebbit_trail_2] +id = 3055 +format = "int" + +[polar_kebbit_trail_3] +id = 3056 +format = "int" + +[polar_kebbit_trail_4] +id = 3057 +format = "int" + +[polar_kebbit_trail_5] +id = 3058 +format = "int" + +[polar_kebbit_trail_6] +id = 3059 +format = "int" + +[polar_kebbit_trail_7] +id = 3060 +format = "int" + +[polar_kebbit_trail_8] +id = 3061 +format = "int" + + +[desert_devil_trail_0] +id = 3016 +format = "int" + +[desert_devil_trail_1] +id = 3017 +format = "int" + +[desert_devil_trail_2] +id = 3018 +format = "int" + +[desert_devil_trail_3] +id = 3019 +format = "int" + +[desert_devil_trail_4] +id = 3020 +format = "int" + +[desert_devil_trail_5] +id = 3021 +format = "int" + +[desert_devil_trail_6] +id = 3022 +format = "int" + +[desert_devil_trail_7] +id = 3023 +format = "int" + +[desert_devil_trail_8] +id = 3024 +format = "int" + +[desert_devil_trail_9] +id = 3025 +format = "int" + +[desert_devil_trail_10] +id = 3026 +format = "int" + +[desert_devil_trail_11] +id = 3027 +format = "int" + +[desert_devil_trail_12] +id = 3030 +format = "int" + +[desert_devil_trail_13] +id = 3031 +format = "int" + +[desert_devil_trail_14] +id = 3032 +format = "int" + + +[feldip_weasel_trail_0] +id = 3036 +format = "int" + +[feldip_weasel_trail_1] +id = 3037 +format = "int" + +[feldip_weasel_trail_2] +id = 3038 +format = "int" + +[feldip_weasel_trail_3] +id = 3039 +format = "int" + +[feldip_weasel_trail_4] +id = 3040 +format = "int" + +[feldip_weasel_trail_5] +id = 3041 +format = "int" + +[feldip_weasel_trail_6] +id = 3043 +format = "int" + +[feldip_weasel_trail_7] +id = 3044 +format = "int" + +[feldip_weasel_trail_8] +id = 3045 +format = "int" + +[feldip_weasel_trail_9] +id = 3046 +format = "int" + +[feldip_weasel_trail_10] +id = 3047 +format = "int" + +[feldip_weasel_trail_11] +id = 3048 +format = "int" + +[feldip_weasel_trail_12] +id = 3049 +format = "int" + +[feldip_weasel_trail_13] +id = 3050 +format = "int" + +[feldip_weasel_trail_14] +id = 3051 +format = "int" + + +[common_kebbit_trail_10] +id = 2985 +format = "int" + +[common_kebbit_trail_11] +id = 2986 +format = "int" + +[common_kebbit_trail_12] +id = 2987 +format = "int" + +[common_kebbit_trail_13] +id = 2988 +format = "int" + +[common_kebbit_trail_14] +id = 2989 +format = "int" + +[common_kebbit_trail_15] +id = 2990 +format = "int" + +[common_kebbit_trail_16] +id = 2991 +format = "int" + +[common_kebbit_trail_17] +id = 2992 +format = "int" + +[common_kebbit_trail_18] +id = 2993 +format = "int" + +[razor_backed_kebbit_trail_0] +id = 2995 +format = "int" + +[razor_backed_kebbit_trail_1] +id = 2996 +format = "int" + +[razor_backed_kebbit_trail_2] +id = 2997 +format = "int" + +[razor_backed_kebbit_trail_3] +id = 2998 +format = "int" + +[razor_backed_kebbit_trail_4] +id = 2999 +format = "int" + +[razor_backed_kebbit_trail_5] +id = 3000 +format = "int" + +[razor_backed_kebbit_trail_6] +id = 3001 +format = "int" + +[razor_backed_kebbit_trail_7] +id = 3002 +format = "int" + +[razor_backed_kebbit_trail_8] +id = 3003 +format = "int" + +[razor_backed_kebbit_trail_9] +id = 3004 +format = "int" + +[razor_backed_kebbit_trail_10] +id = 3007 +format = "int" + +[razor_backed_kebbit_trail_11] +id = 3008 +format = "int" + +[razor_backed_kebbit_trail_12] +id = 3009 +format = "int" + +[razor_backed_kebbit_trail_13] +id = 3010 +format = "int" + +[razor_backed_kebbit_trail_14] +id = 3011 +format = "int" + +[razor_backed_kebbit_trail_15] +id = 3012 +format = "int" + +[razor_backed_kebbit_trail_16] +id = 3013 +format = "int" + diff --git a/data/skill/hunter/tracking.tables.toml b/data/skill/hunter/tracking.tables.toml new file mode 100644 index 0000000000..1431c98be8 --- /dev/null +++ b/data/skill/hunter/tracking.tables.toml @@ -0,0 +1,645 @@ +[tracking] +level = "int" +level_default = 1 +xp = "int" # xp * 0.1 +loot = "list" +catch_anim = "anim" +limit = "int" # maximum linking segments in a generated trail +finals = "list" # tiles of the attackable objects a trail can end at + +[.polar_kebbit] +xp = 300 +loot = ["raw_beast_meat", "polar_kebbit_fur", "bones"] +catch_anim = "noose_catch_polar" +limit = 3 +finals = [ + { x = 2708, y = 3819, level = 1 }, + { x = 2712, y = 3831, level = 1 }, + { x = 2716, y = 3827, level = 1 }, + { x = 2718, y = 3820, level = 1 }, +] + +[.common_kebbit] +level = 3 +xp = 360 +loot = ["common_kebbit_fur", "bones", "raw_beast_meat"] +catch_anim = "noose_catch_common" +limit = 3 +finals = [ + { x = 2349, y = 3604 }, + { x = 2351, y = 3619 }, + { x = 2354, y = 3609 }, + { x = 2355, y = 3601 }, + { x = 2357, y = 3607 }, + { x = 2358, y = 3620 }, + { x = 2360, y = 3602 }, + { x = 2362, y = 3615 }, +] + +[.feldip_weasel] +level = 7 +xp = 480 +loot = ["feldip_weasel_fur", "bones", "raw_beast_meat"] +catch_anim = "noose_catch_desert" +limit = 3 +finals = [ + { x = 2525, y = 2882 }, + { x = 2531, y = 2890 }, + { x = 2533, y = 2885 }, + { x = 2540, y = 2886 }, + { x = 2542, y = 2881 }, + { x = 2553, y = 2888 }, +] + +[.razor_backed_kebbit] +level = 49 +xp = 3480 +loot = ["long_kebbit_spike", "raw_beast_meat", "bones"] +catch_anim = "noose_catch_desert" +limit = 3 +finals = [ + { x = 2322, y = 3570 }, + { x = 2323, y = 3563 }, + { x = 2327, y = 3573 }, + { x = 2332, y = 3568 }, + { x = 2332, y = 3578 }, + { x = 2336, y = 3571 }, + { x = 2337, y = 3565 }, + { x = 2343, y = 3568 }, +] + +[.desert_devil] +level = 13 +xp = 660 +loot = ["bones", "raw_beast_meat", "desert_devil_fur"] +catch_anim = "noose_catch_desert" +limit = 3 +finals = [ + { x = 3393, y = 3122 }, + { x = 3400, y = 3114 }, + { x = 3405, y = 3124 }, + { x = 3407, y = 3121 }, + { x = 3411, y = 3108 }, + { x = 3414, y = 3121 }, +] + + +# --------------------------------------------------------------------------- +# Trail segments — burrow marks a starting segment for that burrow object, +# trigger is the trail object inspected to advance (defaults to end) +# --------------------------------------------------------------------------- +[trails] +kebbit = "string" +varbit = "var" +tunnel = "boolean" +tunnel_default = false +inverted = "boolean" +inverted_default = false +start = "tile" +end = "tile" +trigger = "tile" +burrow = "obj" + +[.common_0] +kebbit = "common_kebbit" +varbit = "common_kebbit_trail_0" +start = { x = 2354, y = 3595 } +end = { x = 2360, y = 3602 } +burrow = "common_kebbit_burrow" + +[.common_1] +kebbit = "common_kebbit" +varbit = "common_kebbit_trail_1" +start = { x = 2354, y = 3595 } +end = { x = 2355, y = 3601 } +burrow = "common_kebbit_burrow" + +[.common_2] +kebbit = "common_kebbit" +varbit = "common_kebbit_trail_2" +start = { x = 2354, y = 3594 } +end = { x = 2349, y = 3604 } +burrow = "common_kebbit_burrow" + +[.common_6] +kebbit = "common_kebbit" +varbit = "common_kebbit_trail_6" +inverted = true +start = { x = 2361, y = 3611 } +end = { x = 2360, y = 3602 } +burrow = "common_kebbit_burrow_2" + +[.common_7] +kebbit = "common_kebbit" +varbit = "common_kebbit_trail_7" +inverted = true +start = { x = 2360, y = 3612 } +end = { x = 2357, y = 3607 } +burrow = "common_kebbit_burrow_2" + +[.common_8] +kebbit = "common_kebbit" +varbit = "common_kebbit_trail_8" +start = { x = 2357, y = 3607 } +end = { x = 2354, y = 3609 } +trigger = { x = 2355, y = 3608 } + +[.common_9] +kebbit = "common_kebbit" +varbit = "common_kebbit_trail_9" +start = { x = 2354, y = 3609 } +end = { x = 2349, y = 3604 } +trigger = { x = 2351, y = 3608 } + +[.common_3] +kebbit = "common_kebbit" +varbit = "common_kebbit_trail_3" +start = { x = 2360, y = 3602 } +end = { x = 2355, y = 3601 } +trigger = { x = 2358, y = 3599 } + +[.common_4] +kebbit = "common_kebbit" +varbit = "common_kebbit_trail_4" +start = { x = 2355, y = 3601 } +end = { x = 2349, y = 3604 } +trigger = { x = 2352, y = 3603 } + +[.common_5] +kebbit = "common_kebbit" +varbit = "common_kebbit_trail_5" +start = { x = 2360, y = 3602 } +end = { x = 2357, y = 3607 } +trigger = { x = 2358, y = 3603 } + +[.polar_8] +kebbit = "polar_kebbit" +varbit = "polar_kebbit_trail_8" +tunnel = true +start = { x = 2712, y = 3831, level = 1 } +end = { x = 2718, y = 3832, level = 1 } +burrow = "polar_kebbit_hole" + +[.polar_7] +kebbit = "polar_kebbit" +varbit = "polar_kebbit_trail_7" +inverted = true +start = { x = 2712, y = 3831, level = 1 } +end = { x = 2716, y = 3827, level = 1 } +trigger = { x = 2713, y = 3827, level = 1 } +burrow = "polar_kebbit_hole" + +[.polar_4] +kebbit = "polar_kebbit" +varbit = "polar_kebbit_trail_4" +start = { x = 2712, y = 3831, level = 1 } +end = { x = 2708, y = 3819, level = 1 } +trigger = { x = 2708, y = 3825, level = 1 } +burrow = "polar_kebbit_hole" + +[.polar_0] +kebbit = "polar_kebbit" +varbit = "polar_kebbit_trail_0" +inverted = true +start = { x = 2718, y = 3820, level = 1 } +end = { x = 2708, y = 3819, level = 1 } +trigger = { x = 2712, y = 3815, level = 1 } +burrow = "polar_kebbit_hole_2" + +[.polar_2] +kebbit = "polar_kebbit" +varbit = "polar_kebbit_trail_2" +tunnel = true +start = { x = 2718, y = 3820, level = 1 } +end = { x = 2715, y = 3820, level = 1 } +burrow = "polar_kebbit_hole_2" + +[.polar_3] +kebbit = "polar_kebbit" +varbit = "polar_kebbit_trail_3" +tunnel = true +start = { x = 2718, y = 3820, level = 1 } +end = { x = 2721, y = 3827, level = 1 } +burrow = "polar_kebbit_hole_2" + +[.polar_5] +kebbit = "polar_kebbit" +varbit = "polar_kebbit_trail_5" +inverted = true +start = { x = 2714, y = 3821, level = 1 } +end = { x = 2716, y = 3827, level = 1 } + +[.polar_6] +kebbit = "polar_kebbit" +varbit = "polar_kebbit_trail_6" +tunnel = true +inverted = true +start = { x = 2716, y = 3827, level = 1 } +end = { x = 2718, y = 3829, level = 1 } + +[.polar_1] +kebbit = "polar_kebbit" +varbit = "polar_kebbit_trail_1" +tunnel = true +start = { x = 2708, y = 3819, level = 1 } +end = { x = 2711, y = 3819, level = 1 } + +[.desert_7] +kebbit = "desert_devil" +varbit = "desert_devil_trail_7" +start = { x = 3396, y = 3106 } +end = { x = 3393, y = 3122 } +burrow = "desert_devil_burrow" + +[.desert_11] +kebbit = "desert_devil" +varbit = "desert_devil_trail_11" +start = { x = 3396, y = 3106 } +end = { x = 3400, y = 3114 } +burrow = "desert_devil_burrow" + +[.desert_14] +kebbit = "desert_devil" +varbit = "desert_devil_trail_14" +start = { x = 3396, y = 3106 } +end = { x = 3411, y = 3108 } +burrow = "desert_devil_burrow" + +[.desert_0] +kebbit = "desert_devil" +varbit = "desert_devil_trail_0" +start = { x = 3402, y = 3131 } +end = { x = 3393, y = 3122 } +burrow = "desert_devil_burrow_2" + +[.desert_1] +kebbit = "desert_devil" +varbit = "desert_devil_trail_1" +start = { x = 3402, y = 3131 } +end = { x = 3405, y = 3124 } +burrow = "desert_devil_burrow_2" + +[.desert_2] +kebbit = "desert_devil" +varbit = "desert_devil_trail_2" +start = { x = 3402, y = 3131 } +end = { x = 3414, y = 3121 } +burrow = "desert_devil_burrow_2" + +[.desert_3] +kebbit = "desert_devil" +varbit = "desert_devil_trail_3" +start = { x = 3396, y = 3121 } +end = { x = 3393, y = 3122 } + +[.desert_4] +kebbit = "desert_devil" +varbit = "desert_devil_trail_4" +start = { x = 3396, y = 3121 } +end = { x = 3405, y = 3122 } + +[.desert_5] +kebbit = "desert_devil" +varbit = "desert_devil_trail_5" +start = { x = 3405, y = 3122 } +end = { x = 3409, y = 3121 } + +[.desert_6] +kebbit = "desert_devil" +varbit = "desert_devil_trail_6" +start = { x = 3409, y = 3121 } +end = { x = 3414, y = 3121 } + +[.desert_8] +kebbit = "desert_devil" +varbit = "desert_devil_trail_8" +start = { x = 3396, y = 3121 } +end = { x = 3400, y = 3114 } + +[.desert_9] +kebbit = "desert_devil" +varbit = "desert_devil_trail_9" +start = { x = 3406, y = 3111 } +end = { x = 3407, y = 3121 } + +[.desert_10] +kebbit = "desert_devil" +varbit = "desert_devil_trail_10" +start = { x = 3411, y = 3108 } +end = { x = 3414, y = 3121 } + +[.desert_12] +kebbit = "desert_devil" +varbit = "desert_devil_trail_12" +start = { x = 3400, y = 3114 } +end = { x = 3406, y = 3111 } + +[.desert_13] +kebbit = "desert_devil" +varbit = "desert_devil_trail_13" +start = { x = 3406, y = 3111 } +end = { x = 3411, y = 3108 } + +[.feldip_0] +kebbit = "feldip_weasel" +varbit = "feldip_weasel_trail_0" +start = { x = 2525, y = 2889 } +end = { x = 2525, y = 2882 } +burrow = "feldip_weasel_burrow" + + +[.feldip_1] +kebbit = "feldip_weasel" +varbit = "feldip_weasel_trail_1" +start = { x = 2525, y = 2889 } +end = { x = 2525, y = 2882 } +burrow = "feldip_weasel_burrow" + + +[.feldip_2] +kebbit = "feldip_weasel" +varbit = "feldip_weasel_trail_2" +start = { x = 2525, y = 2889 } +end = { x = 2531, y = 2890 } +burrow = "feldip_weasel_burrow" + + +[.feldip_12] +kebbit = "feldip_weasel" +varbit = "feldip_weasel_trail_12" +start = { x = 2554, y = 2882 } +end = { x = 2542, y = 2881 } +burrow = "feldip_weasel_burrow_2" + + +[.feldip_13] +kebbit = "feldip_weasel" +varbit = "feldip_weasel_trail_13" +start = { x = 2554, y = 2882 } +end = { x = 2553, y = 2888 } +burrow = "feldip_weasel_burrow_2" + + +[.feldip_14] +kebbit = "feldip_weasel" +varbit = "feldip_weasel_trail_14" +start = { x = 2554, y = 2882 } +end = { x = 2553, y = 2888 } +burrow = "feldip_weasel_burrow_2" + + +[.feldip_3] +kebbit = "feldip_weasel" +varbit = "feldip_weasel_trail_3" +start = { x = 2531, y = 2890 } +end = { x = 2533, y = 2885 } + + +[.feldip_4] +kebbit = "feldip_weasel" +varbit = "feldip_weasel_trail_4" +start = { x = 2525, y = 2882 } +end = { x = 2533, y = 2882 } + + +[.feldip_5] +kebbit = "feldip_weasel" +varbit = "feldip_weasel_trail_5" +start = { x = 2533, y = 2882 } +end = { x = 2533, y = 2885 } + + +[.feldip_6] +kebbit = "feldip_weasel" +varbit = "feldip_weasel_trail_6" +start = { x = 2531, y = 2890 } +end = { x = 2539, y = 2890 } + + +[.feldip_7] +kebbit = "feldip_weasel" +varbit = "feldip_weasel_trail_7" +start = { x = 2533, y = 2885 } +end = { x = 2540, y = 2886 } + + +[.feldip_8] +kebbit = "feldip_weasel" +varbit = "feldip_weasel_trail_8" +start = { x = 2542, y = 2881 } +end = { x = 2533, y = 2882 } + + +[.feldip_9] +kebbit = "feldip_weasel" +varbit = "feldip_weasel_trail_9" +start = { x = 2540, y = 2886 } +end = { x = 2542, y = 2881 } + + +[.feldip_10] +kebbit = "feldip_weasel" +varbit = "feldip_weasel_trail_10" +start = { x = 2539, y = 2890 } +end = { x = 2540, y = 2886 } + + +[.feldip_11] +kebbit = "feldip_weasel" +varbit = "feldip_weasel_trail_11" +start = { x = 2543, y = 2890 } +end = { x = 2553, y = 2888 } + +[.common_10] +kebbit = "common_kebbit" +varbit = "common_kebbit_trail_10" +start = { x = 2357, y = 3624 } +end = { x = 2362, y = 3615 } +burrow = "common_kebbit_burrow_3" + + +[.common_11] +kebbit = "common_kebbit" +varbit = "common_kebbit_trail_11" +start = { x = 2357, y = 3624 } +end = { x = 2351, y = 3619 } +burrow = "common_kebbit_burrow_3" + + +[.common_12] +kebbit = "common_kebbit" +varbit = "common_kebbit_trail_12" +start = { x = 2357, y = 3624 } +end = { x = 2356, y = 3620 } +burrow = "common_kebbit_burrow_3" + + +[.common_17] +kebbit = "common_kebbit" +varbit = "common_kebbit_trail_17" +start = { x = 2360, y = 3611 } +end = { x = 2362, y = 3615 } +burrow = "common_kebbit_burrow_2" + + +[.common_13] +kebbit = "common_kebbit" +varbit = "common_kebbit_trail_13" +start = { x = 2344, y = 3612 } +end = { x = 2349, y = 3604 } + + +[.common_14] +kebbit = "common_kebbit" +varbit = "common_kebbit_trail_14" +start = { x = 2354, y = 3609 } +end = { x = 2344, y = 3612 } + + +[.common_15] +kebbit = "common_kebbit" +varbit = "common_kebbit_trail_15" +start = { x = 2344, y = 3612 } +end = { x = 2351, y = 3619 } + + +[.common_16] +kebbit = "common_kebbit" +varbit = "common_kebbit_trail_16" +start = { x = 2356, y = 3620 } +end = { x = 2351, y = 3619 } + + +[.common_18] +kebbit = "common_kebbit" +varbit = "common_kebbit_trail_18" +start = { x = 2362, y = 3615 } +end = { x = 2358, y = 3620 } + + +[.razor_0] +kebbit = "razor_backed_kebbit" +varbit = "razor_backed_kebbit_trail_0" +start = { x = 2331, y = 3562 } +end = { x = 2323, y = 3563 } +burrow = "razor_backed_kebbit_burrow" + + +[.razor_1] +kebbit = "razor_backed_kebbit" +varbit = "razor_backed_kebbit_trail_1" +start = { x = 2331, y = 3562 } +end = { x = 2332, y = 3568 } +burrow = "razor_backed_kebbit_burrow" + + +[.razor_2] +kebbit = "razor_backed_kebbit" +varbit = "razor_backed_kebbit_trail_2" +start = { x = 2331, y = 3562 } +end = { x = 2337, y = 3565 } +burrow = "razor_backed_kebbit_burrow" + + +[.razor_9] +kebbit = "razor_backed_kebbit" +varbit = "razor_backed_kebbit_trail_9" +start = { x = 2322, y = 3576 } +end = { x = 2322, y = 3570 } +burrow = "razor_backed_kebbit_burrow_2" + + +[.razor_12] +kebbit = "razor_backed_kebbit" +varbit = "razor_backed_kebbit_trail_12" +start = { x = 2322, y = 3576 } +end = { x = 2327, y = 3573 } +burrow = "razor_backed_kebbit_burrow_2" + + +[.razor_13] +kebbit = "razor_backed_kebbit" +varbit = "razor_backed_kebbit_trail_13" +start = { x = 2322, y = 3576 } +end = { x = 2332, y = 3578 } +burrow = "razor_backed_kebbit_burrow_2" + + +[.razor_8] +kebbit = "razor_backed_kebbit" +varbit = "razor_backed_kebbit_trail_8" +start = { x = 2341, y = 3577 } +end = { x = 2343, y = 3568 } +burrow = "razor_backed_kebbit_burrow_3" + + +[.razor_15] +kebbit = "razor_backed_kebbit" +varbit = "razor_backed_kebbit_trail_15" +start = { x = 2341, y = 3577 } +end = { x = 2336, y = 3571 } +burrow = "razor_backed_kebbit_burrow_3" + + +[.razor_16] +kebbit = "razor_backed_kebbit" +varbit = "razor_backed_kebbit_trail_16" +start = { x = 2341, y = 3577 } +end = { x = 2332, y = 3578 } +burrow = "razor_backed_kebbit_burrow_3" + + +[.razor_3] +kebbit = "razor_backed_kebbit" +varbit = "razor_backed_kebbit_trail_3" +start = { x = 2337, y = 3565 } +end = { x = 2343, y = 3568 } + + +[.razor_4] +kebbit = "razor_backed_kebbit" +varbit = "razor_backed_kebbit_trail_4" +start = { x = 2322, y = 3570 } +end = { x = 2323, y = 3563 } + + +[.razor_5] +kebbit = "razor_backed_kebbit" +varbit = "razor_backed_kebbit_trail_5" +start = { x = 2323, y = 3563 } +end = { x = 2332, y = 3568 } + + +[.razor_6] +kebbit = "razor_backed_kebbit" +varbit = "razor_backed_kebbit_trail_6" +start = { x = 2337, y = 3565 } +end = { x = 2336, y = 3571 } + + +[.razor_7] +kebbit = "razor_backed_kebbit" +varbit = "razor_backed_kebbit_trail_7" +start = { x = 2343, y = 3568 } +end = { x = 2336, y = 3571 } + + +[.razor_10] +kebbit = "razor_backed_kebbit" +varbit = "razor_backed_kebbit_trail_10" +start = { x = 2327, y = 3573 } +end = { x = 2322, y = 3570 } + + +[.razor_11] +kebbit = "razor_backed_kebbit" +varbit = "razor_backed_kebbit_trail_11" +start = { x = 2327, y = 3573 } +end = { x = 2332, y = 3568 } + + +[.razor_14] +kebbit = "razor_backed_kebbit" +varbit = "razor_backed_kebbit_trail_14" +start = { x = 2332, y = 3578 } +end = { x = 2336, y = 3571 } diff --git a/game/src/main/kotlin/content/entity/player/equip/Equipping.kt b/game/src/main/kotlin/content/entity/player/equip/Equipping.kt index cbe8ac596d..e06395bfb7 100644 --- a/game/src/main/kotlin/content/entity/player/equip/Equipping.kt +++ b/game/src/main/kotlin/content/entity/player/equip/Equipping.kt @@ -37,6 +37,10 @@ class Equipping : Script { message("You should leave your gas mask on while you're in West Ardougne.") return@itemOption } + if (item.id.startsWith("falconers_glove")) { + message("You should return the falcon to Matthias instead of just dropping it.") + return@itemOption + } if (item.id == "rubber_chicken" || item.id == "easter_carrot") { options.remove("Whack") } @@ -70,6 +74,14 @@ class Equipping : Script { player.message("That armour is too small for a human.") return } + if (item.id.startsWith("falconers_glove")) { + player.message("You need Matthias to provide you with a bird before you can wear that.") + return + } + if ((item.slot == EquipSlot.Weapon || item.slot == EquipSlot.Shield) && player.equipped(EquipSlot.Weapon).id.startsWith("falconers_glove")) { + player.message("You can't wield anything while you have a falcon on your arm.") + return + } if (item.id.startsWith("bedsheet") && player.tile !in Areas["port_phasmatys"]) { player.message("I'll only wear the bedsheet inside Port Phasmatys!") return diff --git a/game/src/main/kotlin/content/skill/hunter/BirdSnare.kt b/game/src/main/kotlin/content/skill/hunter/BirdSnare.kt index b8e1d212ac..7bc05fac98 100644 --- a/game/src/main/kotlin/content/skill/hunter/BirdSnare.kt +++ b/game/src/main/kotlin/content/skill/hunter/BirdSnare.kt @@ -8,7 +8,6 @@ import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.client.message import world.gregs.voidps.engine.client.ui.chat.plural import world.gregs.voidps.engine.data.config.RowDefinition -import world.gregs.voidps.engine.data.definition.Areas import world.gregs.voidps.engine.data.definition.Rows import world.gregs.voidps.engine.data.definition.Tables import world.gregs.voidps.engine.entity.character.areaSound @@ -24,7 +23,6 @@ import world.gregs.voidps.engine.entity.character.player.skill.level.Level import world.gregs.voidps.engine.entity.character.player.skill.level.Level.has import world.gregs.voidps.engine.entity.character.sound import world.gregs.voidps.engine.entity.item.floor.FloorItem -import world.gregs.voidps.engine.entity.item.floor.FloorItems import world.gregs.voidps.engine.entity.obj.* import world.gregs.voidps.engine.inv.add import world.gregs.voidps.engine.inv.inventory @@ -64,7 +62,7 @@ class BirdSnare : Script { itemOnObjectOperate("*", "bird_snare") { when { it.item.id == "unlit_torch" -> message("I should light the torch before using it to smoke the trap.") - it.item.id == "torch_lit" -> smoke(it.target) + it.item.id == "torch_lit" -> Traps.smoke(this, "bird_snare", it.target.tile) it.item.def.contains(Params.HEALS) -> message("There isn't really anywhere to put any bait on this trap.") else -> noInterest() } @@ -80,12 +78,15 @@ class BirdSnare : Script { if (!player.has(Skill.Hunter, creature.int("level"))) { return@huntNPC } - if (tile.distanceTo(target.tile) > 2) { + if (tile.distanceTo(target.tile) > 2 || target["caught", false]) { return@huntNPC } transform("${id}_off") - var chance = Traps.chance(this, creature) + val chance = Traps.chance(this, creature) val success = Level.success(player.levels.get(Skill.Hunter), chance) + if (success) { + target["caught"] = true + } target.walkToDelay(tile) target.walkOverDelay(tile) target.face(Direction.SOUTH) @@ -107,69 +108,12 @@ class BirdSnare : Script { } npcDespawn("hunting_ojibway_trap_npc") { - val player = owner ?: return@npcDespawn - val trap = GameObjects.getLayer(tile, ObjectLayer.GROUND) ?: return@npcDespawn - player.dec("trap_count") - GameObjects.remove(trap) - val drop = if (lifecycle == 0) { - player.message("The bird snare that you laid has fallen over.") - true - } else { - player["logged_out", false] - } - if (drop) { - player.drop(trap.tile, "bird_snare") - } - } - } - - private fun Player.smoke(target: GameObject) { - val id = Tables.npc("traps.${target.id}.npc") - val npc = NPCs.find(target.tile, id) - if (npc["owner", ""] != accountName) { - message("This isn't your trap.") - return - } - if (npc["smoked", false]) { - message("You've already smoked this trap.") // TODO proper message - return + Traps.despawn(this, "bird_snare", "The bird snare that you laid has fallen over.") } - anim("lay_trap_small") - areaSound("hunting_smoke2", tile = target.tile, radius = 5) - npc["smoked"] = true - message("You use the smoke from the torch to remove your scent from the trap.", type = ChatType.Filter) } private suspend fun Player.layTrap(floorItem: FloorItem?) { - val trap = Rows.getOrNull("traps.bird_snare") ?: return - val level = levels.get(Skill.Hunter) - if (!has(Skill.Hunter, trap.int("level"), message = true)) { - return - } - if (Areas.get(tile.zone).any { it.tags.contains("bank") } || GameObjects.getLayer(tile, ObjectLayer.GROUND) != null) { - message("You can't lay a trap here.", ChatType.Filter) - return - } - val max = Traps.max(level, trap.int("max")) - val trapCount = get("trap_count", 0) - if (trapCount >= max) { - message("You may setup only $max ${"trap".plural(max)} at a time at your Hunter level.") - return - } - arriveDelay() - message("You begin setting up ${if (max == 1) "the" else "a"} trap.", ChatType.Filter) - anim("lay_trap") - sound("set_noose") - delay(3) - if (floorItem != null) { - FloorItems.remove(floorItem) - } else { - inventory.remove("bird_snare") - } - inc("trap_count") - NPCs.add("hunting_ojibway_trap_npc", tile, ticks = 100, owner = this) - val obj = GameObjects.add("bird_snare", tile) - stepAway(obj) + Traps.lay(this, "bird_snare", "set_noose", floorItem) } private suspend fun Player.dismantleTrap(target: GameObject, creature: RowDefinition?) { @@ -188,6 +132,9 @@ class BirdSnare : Script { anim("take_trap") sound("trap_dismantle", delay = 25) delay(2) + if (GameObjects.getLayer(target.tile, ObjectLayer.GROUND)?.id != target.id) { + return + } collapse(npc, target, drop = false) inventory.add("bird_snare") message("You dismantle the trap.", ChatType.Filter) diff --git a/game/src/main/kotlin/content/skill/hunter/BoxTrap.kt b/game/src/main/kotlin/content/skill/hunter/BoxTrap.kt new file mode 100644 index 0000000000..db21c74f75 --- /dev/null +++ b/game/src/main/kotlin/content/skill/hunter/BoxTrap.kt @@ -0,0 +1,183 @@ +package content.skill.hunter + +import content.entity.effect.transform +import content.quest.questCompleted +import net.pearx.kasechange.toLowerSpaceCase +import world.gregs.voidps.cache.definition.Params +import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.client.message +import world.gregs.voidps.engine.data.config.RowDefinition +import world.gregs.voidps.engine.data.definition.Rows +import world.gregs.voidps.engine.data.definition.Tables +import world.gregs.voidps.engine.entity.character.areaSound +import world.gregs.voidps.engine.entity.character.npc.NPC +import world.gregs.voidps.engine.entity.character.npc.NPCs +import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.entity.character.player.Players +import world.gregs.voidps.engine.entity.character.player.chat.ChatType +import world.gregs.voidps.engine.entity.character.player.chat.noInterest +import world.gregs.voidps.engine.entity.character.player.skill.Skill +import world.gregs.voidps.engine.entity.character.player.skill.exp.exp +import world.gregs.voidps.engine.entity.character.player.skill.level.Level +import world.gregs.voidps.engine.entity.character.player.skill.level.Level.has +import world.gregs.voidps.engine.entity.character.sound +import world.gregs.voidps.engine.entity.item.Item +import world.gregs.voidps.engine.entity.item.floor.FloorItem +import world.gregs.voidps.engine.entity.obj.* +import world.gregs.voidps.engine.inv.add +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.engine.inv.remove +import world.gregs.voidps.engine.inv.transact.operation.AddItem.add + +class BoxTrap : Script { + init { + itemOption("Lay", "box_trap") { + layTrap(null) + } + + floorItemOperate("Lay") { (item) -> + if (item.id == "box_trap") { + layTrap(item) + } + } + + objectOperate("Dismantle", "box_trap,box_trap_fail") { (target) -> + dismantleTrap(target, null) + } + + objectOperate("Check", "box_trap_ferret,box_trap_chinchompa,box_trap_carnivorous_chinchompa,box_trap_pawya,box_trap_grenwall") { (target) -> + dismantleTrap(target, creature = Rows.get("creatures.${target.id.removePrefix("box_trap_")}")) + } + + objectOperate("Investigate", "box_trap") { (target) -> + val id = Tables.npc("traps.box_trap.npc") + val npc = NPCs.find(target.tile, id) + Traps.investigate(this, npc) + } + + itemOnObjectOperate("*", "box_trap") { + when { + it.item.id == "unlit_torch" -> message("I should light the torch before using it to smoke the trap.") + it.item.id == "torch_lit" -> Traps.smoke(this, "box_trap", it.target.tile) + it.item.id == "papaya_fruit" || it.item.id == "raw_pawya_meat" -> bait(it.item, it.target) + it.item.def.contains(Params.HEALS) -> message("I don't think I'd catch much using that as bait.") + else -> noInterest() + } + } + + huntNPC("box_trap") { target -> + if (transform.endsWith("_off")) { + return@huntNPC + } + val creature = Rows.getOrNull("creatures.${target.id}") ?: return@huntNPC + val account: String = get("owner") ?: return@huntNPC + val player = Players.findByAccount(account) ?: return@huntNPC + if (!player.has(Skill.Hunter, creature.int("level"))) { + return@huntNPC + } + if (target.id == "ferret" && !player.questCompleted("eagles_peak")) { + return@huntNPC + } + val required = creature.itemOrNull("bait") + if (required != null) { + val bait: String? = get("bait") + if (bait != required) { + return@huntNPC + } + } + if (tile.distanceTo(target.tile) > 2 || target["caught", false]) { + return@huntNPC + } + transform("${id}_off") + val chance = Traps.chance(this, creature) + val success = Level.success(player.levels.get(Skill.Hunter), chance) + if (success) { + target["caught"] = true + } + target.walkToDelay(tile) + target.walkOverDelay(tile) + despawn(100) + val trap = GameObjects.getLayer(tile, ObjectLayer.GROUND) ?: return@huntNPC + val catching = trap.replace("box_trap_catching") + target.anim(if (success) creature.anim("catch_anim") else creature.anim("fail_anim")) + target.delay(1) + if (!success) { + catching.replace("box_trap_fail") + return@huntNPC + } + target.levels.set(Skill.Constitution, 0) + clear("bait") + catching.replace(Tables.obj("creatures.${target.id}.caught_obj")) + player.message("Something has been caught in your trap!") + areaSound("box_trap_catch", tile) + } + + npcDespawn("hunting_box_trap_npc") { + Traps.despawn(this, "box_trap", "The box trap that you laid has fallen over.") + } + } + + private fun Player.bait(item: Item, trap: GameObject) { + val npc = NPCs.find(trap.tile, "hunting_box_trap_npc") + if (npc["owner", ""] != accountName) { + message("This isn't your trap.") + return + } + if (npc.contains("bait")) { + message("You've already baited this trap.") + return + } + if (!inventory.remove(item.id)) { + return + } + anim("lay_trap_small") + sound("drop_item", delay = 25) + npc["bait"] = item.id + message("You bait the trap with ${item.id.toLowerSpaceCase()}.") + } + + private suspend fun Player.layTrap(floorItem: FloorItem?) { + Traps.lay(this, "box_trap", "lay_box_trap", floorItem) + } + + private suspend fun Player.dismantleTrap(target: GameObject, creature: RowDefinition?) { + val npc = NPCs.findOrNull(target.tile, "hunting_box_trap_npc") ?: return + if (npc["owner", ""] != accountName) { + message("This is not your trap.") + return + } + val loot = creature?.itemList("loot") ?: emptyList() + val bait: String? = npc["bait"] + val items = mutableListOf("box_trap") + if (loot.isEmpty() && bait != null) { + items.add(bait) + } + anim("take_trap") + sound("trap_dismantle", delay = 25) + delay(2) + if (GameObjects.getLayer(target.tile, ObjectLayer.GROUND)?.id != target.id) { + return + } + val added = inventory.transaction { + for (item in items + loot) { + add(item) + } + } + if (!added) { + message("You don't have enough inventory space.") + return + } + collapse(npc, target) + message("You dismantle the trap.", ChatType.Filter) + if (creature != null) { + exp(Skill.Hunter, creature.int("xp") / 10.0) + message("You've caught a ${creature.rowId.toLowerSpaceCase()}!", ChatType.Filter) + } + } + + private fun Player.collapse(npc: NPC, target: GameObject) { + dec("trap_count") + NPCs.remove(npc) + GameObjects.remove(target) + } +} diff --git a/game/src/main/kotlin/content/skill/hunter/DeadfallTrap.kt b/game/src/main/kotlin/content/skill/hunter/DeadfallTrap.kt new file mode 100644 index 0000000000..0e39b93b23 --- /dev/null +++ b/game/src/main/kotlin/content/skill/hunter/DeadfallTrap.kt @@ -0,0 +1,208 @@ +package content.skill.hunter + +import content.entity.effect.transform +import content.entity.player.inv.item.drop +import net.pearx.kasechange.toLowerSpaceCase +import world.gregs.voidps.cache.definition.Params +import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.client.message +import world.gregs.voidps.engine.client.ui.chat.plural +import world.gregs.voidps.engine.data.config.RowDefinition +import world.gregs.voidps.engine.data.definition.Rows +import world.gregs.voidps.engine.data.definition.Tables +import world.gregs.voidps.engine.entity.character.areaSound +import world.gregs.voidps.engine.entity.character.npc.NPC +import world.gregs.voidps.engine.entity.character.npc.NPCs +import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.entity.character.player.Players +import world.gregs.voidps.engine.entity.character.player.chat.ChatType +import world.gregs.voidps.engine.entity.character.player.chat.noInterest +import world.gregs.voidps.engine.entity.character.player.skill.Skill +import world.gregs.voidps.engine.entity.character.player.skill.exp.exp +import world.gregs.voidps.engine.entity.character.player.skill.level.Level +import world.gregs.voidps.engine.entity.character.player.skill.level.Level.has +import world.gregs.voidps.engine.entity.character.sound +import world.gregs.voidps.engine.entity.item.floor.FloorItems +import world.gregs.voidps.engine.entity.obj.* +import world.gregs.voidps.engine.inv.add +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.engine.inv.remove + +class DeadfallTrap : Script { + init { + objectOperate("Set-trap", "boulder_trap") { (target) -> + setTrap(target) + } + + objectOperate("Dismantle", "boulder_trap_setup") { (target) -> + dismantleTrap(target, null) + } + + objectOperate("Check", "boulder_trap_wild_kebbit,boulder_trap_barb_tailed_kebbit,boulder_trap_prickly_kebbit,boulder_trap_sabre_toothed_kebbit") { (target) -> + dismantleTrap(target, creature = Rows.get("creatures.${target.id.removePrefix("boulder_trap_")}")) + } + + objectOperate("Investigate", "boulder_trap_setup") { (target) -> + val npc = NPCs.find(target.tile, "hunting_deadfall_trap_npc") + Traps.investigate(this, npc) + } + + itemOnObjectOperate("*", "boulder_trap_setup") { + when { + it.item.id == "unlit_torch" -> message("I should light the torch before using it to smoke the trap.") + it.item.id == "torch_lit" -> Traps.smoke(this, "boulder_trap", it.target.tile) + it.item.def.contains(Params.HEALS) -> message("I don't think I'd catch much using that as bait.") + else -> noInterest() + } + } + + huntNPC("deadfall") { target -> + if (transform.endsWith("_off")) { + return@huntNPC + } + val creature = Rows.getOrNull("creatures.${target.id}") ?: return@huntNPC + val account: String = get("owner") ?: return@huntNPC + val player = Players.findByAccount(account) ?: return@huntNPC + if (!player.has(Skill.Hunter, creature.int("level"))) { + return@huntNPC + } + if (tile.distanceTo(target.tile) > 3 || target["caught", false]) { + return@huntNPC + } + transform("${id}_off") + val chance = Traps.chance(this, creature) + val success = Level.success(player.levels.get(Skill.Hunter), chance) + if (success) { + target["caught"] = true + } + target.walkToDelay(tile) + despawn(100) + val trap = GameObjects.getLayer(tile, ObjectLayer.GROUND) ?: return@huntNPC + areaSound("deadfall_catch", tile) + if (!success) { + target.anim(creature.anim("fail_anim")) + val failing = trap.replace("boulder_trap_fail") + target.delay(2) + collapse(player, this, failing) + player.message("Your deadfall trap has collapsed.") + return@huntNPC + } + target.anim(creature.anim("catch_anim")) + val catching = trap.replace("${Tables.obj("creatures.${target.id}.caught_obj")}_catching") + target.delay(2) + target.levels.set(Skill.Constitution, 0) + catching.replace(Tables.obj("creatures.${target.id}.caught_obj")) + player.message("Something has been caught in your trap!") + } + + npcDespawn("hunting_deadfall_trap_npc") { + val trap = GameObjects.getLayer(tile, ObjectLayer.GROUND) ?: return@npcDespawn + if (trap.id == "boulder_trap") { + return@npcDespawn + } + GameObjects.remove(trap) + val player = owner + if (player == null) { + FloorItems.add(trap.tile, "logs", disappearTicks = 200) + return@npcDespawn + } + player.dec("trap_count") + player.dec("deadfall_count") + val drop = if (lifecycle == 0) { + player.message("The deadfall trap that you constructed has collapsed.") + true + } else { + player["logged_out", false] + } + if (drop) { + player.drop(trap.tile, "logs") + } + } + } + + private suspend fun Player.setTrap(target: GameObject) { + val trap = Rows.getOrNull("traps.boulder_trap") ?: return + val level = levels.get(Skill.Hunter) + if (!has(Skill.Hunter, trap.int("level"), message = true)) { + return + } + for (item in trap.itemList("requires")) { + if (!inventory.contains(item)) { + message("You need a ${item.toLowerSpaceCase()} in order to set a deadfall trap.") + return + } + } + if (!inventory.contains("logs")) { + message("You need some logs in order to set a deadfall trap.") + return + } + if (get("deadfall_count", 0) >= trap.int("max")) { + message("You can only set up one deadfall trap at a time.") + return + } + val max = Traps.max(level, 5) + val trapCount = get("trap_count", 0) + if (trapCount >= max) { + message("You may setup only $max ${"trap".plural(max)} at a time at your Hunter level.") + return + } + arriveDelay() + message("You begin setting up the trap.", ChatType.Filter) + anim("lay_trap") + sound("set_deadfall") + delay(3) + if (GameObjects.getLayer(target.tile, ObjectLayer.GROUND)?.id != "boulder_trap") { + message("You can't lay a trap here.", ChatType.Filter) + return + } + if (!inventory.remove("logs")) { + return + } + inc("trap_count") + inc("deadfall_count") + NPCs.add("hunting_deadfall_trap_npc", target.tile, ticks = 100, owner = this) + target.replace("boulder_trap_setup") + stepAway(target) + } + + private suspend fun Player.dismantleTrap(target: GameObject, creature: RowDefinition?) { + val npc = NPCs.findOrNull(target.tile, "hunting_deadfall_trap_npc") ?: return + if (npc["owner", ""] != accountName) { + message("This is not your trap.") + return + } + val loot = creature?.itemList("loot") ?: emptyList() + val items = if (loot.isEmpty()) listOf("logs") else emptyList() + val size = items.size + loot.size + if (inventory.spaces < size) { + val slots = size - inventory.spaces + message("You don't have enough inventory space. You need $slots more free ${"slot".plural(slots)}.") + return + } + anim("take_trap") + sound("take_branches", delay = 25) + delay(2) + if (GameObjects.getLayer(target.tile, ObjectLayer.GROUND)?.id != target.id) { + return + } + collapse(this, npc, target) + for (item in items) { + inventory.add(item) + } + message("You dismantle the trap.", ChatType.Filter) + if (creature != null) { + for (item in loot) { + inventory.add(item) + } + exp(Skill.Hunter, creature.int("xp") / 10.0) + message("You've caught a ${creature.rowId.toLowerSpaceCase()}!", ChatType.Filter) + } + } + + private fun collapse(player: Player, npc: NPC, target: GameObject) { + player.dec("trap_count") + player.dec("deadfall_count") + NPCs.remove(npc) + GameObjects.remove(target) + } +} diff --git a/game/src/main/kotlin/content/skill/hunter/Falconry.kt b/game/src/main/kotlin/content/skill/hunter/Falconry.kt new file mode 100644 index 0000000000..894a153d57 --- /dev/null +++ b/game/src/main/kotlin/content/skill/hunter/Falconry.kt @@ -0,0 +1,189 @@ +package content.skill.hunter + +import content.entity.player.dialogue.Happy +import content.entity.player.dialogue.Neutral +import content.entity.player.dialogue.Quiz +import content.entity.player.dialogue.Sad +import content.entity.player.dialogue.type.choice +import content.entity.player.dialogue.type.npc +import content.entity.player.dialogue.type.player +import content.entity.proj.shoot +import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.client.clearHint +import world.gregs.voidps.engine.client.hint +import world.gregs.voidps.engine.client.message +import world.gregs.voidps.engine.client.ui.chat.plural +import world.gregs.voidps.engine.data.definition.Rows +import world.gregs.voidps.engine.entity.character.npc.NPC +import world.gregs.voidps.engine.entity.character.npc.NPCs +import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.entity.character.player.equip.equipped +import world.gregs.voidps.engine.entity.character.player.skill.Skill +import world.gregs.voidps.engine.entity.character.player.skill.exp.exp +import world.gregs.voidps.engine.entity.character.player.skill.level.Level +import world.gregs.voidps.engine.entity.character.player.skill.level.Level.has +import world.gregs.voidps.engine.entity.character.sound +import world.gregs.voidps.engine.entity.item.Item +import world.gregs.voidps.engine.inv.add +import world.gregs.voidps.engine.inv.equipment +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.engine.inv.remove +import world.gregs.voidps.engine.inv.replace +import world.gregs.voidps.network.login.protocol.visual.update.player.EquipSlot + +class Falconry : Script { + init { + npcOperate("Talk-to", "matthias") { + if (hasFalcon()) { + npc("How are you finding Valor? Remember to bring him back once you're done.") + choice { + option("I'd like to hand him back, thanks.") { + equipment.transaction { + set(EquipSlot.Weapon.index, null) + } + npc("I hope you enjoyed the experience.") + } + option("I'll hang on to him a while longer.") + } + return@npcOperate + } + npc("Greetings. Can I help you at all? Perhaps you'd be interested in trying your hand at falconry?") + choice { + option("Could you tell me more about that?") { + npc("For 500 coins you can borrow one of my birds and try to catch some of the kebbits around here. I'll want the bird back when you're done, of course.") + choice { + option("Sounds good to me.") { + startFalconry() + } + option("Maybe some other time.") + } + } + option("No thanks, I'm fine.") + } + } + + npcOperate("Falconry", "matthias") { + startFalconry() + } + + npcApproach("Catch", "spotted_kebbit,dark_kebbit,dashing_kebbit") { (target) -> + approachRange(6) + catch(target) + } + + npcOperate("Retrieve", "spotted_kebbit_caught,dark_kebbit_caught,dashing_kebbit_caught") { (target) -> + retrieve(target) + } + + npcDespawn("*_kebbit_caught") { + val player = owner ?: return@npcDespawn + val hint = get("hint", -1) + if (hint != -1) { + player.clearHint(hint) + } + if (lifecycle != 0) { + return@npcDespawn + } + if (player.equipped(EquipSlot.Weapon).id == "falconers_glove") { + player.equipment.replace("falconers_glove", "falconers_glove_2") + player.message("Your falcon gives up on its catch and returns to your glove.") + } + } + + exited("piscatoris_falconry_area") { + if (hasFalcon()) { + equipment.transaction { + set(EquipSlot.Weapon.index, null) + } + message("You hand the falcon back to Matthias before leaving.") + } + } + } + + private fun Player.hasFalcon(): Boolean { + val weapon = equipped(EquipSlot.Weapon).id + return weapon == "falconers_glove" || weapon == "falconers_glove_2" + } + + private suspend fun Player.startFalconry() { + if (hasFalcon()) { + npc("You've already got one of my birds.") + return + } + if (!has(Skill.Hunter, 43, message = false)) { + npc("I'm afraid my birds are hard to handle. You'll need a Hunter level of at least 43 before I can let you use one.") + return + } + if (equipped(EquipSlot.Weapon).isNotEmpty() || equipped(EquipSlot.Shield).isNotEmpty()) { + npc("You'll need both hands free to handle the bird. Come back once you've put away what you're holding.") + return + } + if (!inventory.remove("coins", 500)) { + npc("I'm afraid it's 500 coins if you want to borrow a bird, and it looks like you don't have enough on you.") + return + } + equipment.transaction { + set(EquipSlot.Weapon.index, Item("falconers_glove_2")) + } + npc("Here you go then. Treat Valor well, and bring him back when you're done.") + } + + private suspend fun Player.catch(target: NPC) { + val row = Rows.getOrNull("falconry.${target.id}") ?: return + val weapon = equipped(EquipSlot.Weapon).id + if (weapon == "falconers_glove") { + message("Your falcon is already off chasing prey.") + return + } + if (weapon != "falconers_glove_2") { + message("You need a trained falcon to catch this kebbit.") + return + } + if (!has(Skill.Hunter, row.int("level"), message = true)) { + return + } + face(target) + sound("falcon_swoop") + shoot("gyr_falcon", target) + delay(2) + if (target.tile.distanceTo(tile) > 8) { + return + } + val success = Level.success(levels.get(Skill.Hunter), row.intRange("chance")) + if (!success) { + target.shoot("gyr_falcon", this) + sound("falcon_return", delay = 20) + message("The kebbit is too quick for your falcon.") + return + } + equipment.replace("falconers_glove_2", "falconers_glove") + target.levels.set(Skill.Constitution, 0) + val caught = NPCs.add(row.npc("caught"), target.tile, ticks = 100, owner = this) + caught["hint"] = hint(caught) + message("Your falcon successfully swoops down on the kebbit.") + } + + private suspend fun Player.retrieve(target: NPC) { + val id = target.id.removeSuffix("_caught") + val row = Rows.getOrNull("falconry.$id") ?: return + if (target["owner", ""] != accountName) { + message("This isn't your falcon.") + return + } + val loot = listOf(row.item("fur"), "bones") + if (inventory.spaces < loot.size) { + val slots = loot.size - inventory.spaces + message("You don't have enough inventory space. You need $slots more free ${"slot".plural(slots)}.") + return + } + anim("net_catch") + delay(1) + NPCs.remove(target) + equipment.replace("falconers_glove", "falconers_glove_2") + for (item in loot) { + inventory.add(item) + } + exp(Skill.Hunter, row.int("xp") / 10.0) + message("You retrieve your falcon and its catch.") + } +} diff --git a/game/src/main/kotlin/content/skill/hunter/KebbitTracking.kt b/game/src/main/kotlin/content/skill/hunter/KebbitTracking.kt new file mode 100644 index 0000000000..c3afb9a8df --- /dev/null +++ b/game/src/main/kotlin/content/skill/hunter/KebbitTracking.kt @@ -0,0 +1,240 @@ +package content.skill.hunter + +import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.client.message +import world.gregs.voidps.engine.data.definition.Rows +import world.gregs.voidps.engine.data.definition.Tables +import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.entity.character.player.equip.equipped +import world.gregs.voidps.engine.entity.character.player.skill.Skill +import world.gregs.voidps.engine.entity.character.player.skill.exp.exp +import world.gregs.voidps.engine.entity.character.player.skill.level.Level.has +import world.gregs.voidps.engine.entity.character.sound +import world.gregs.voidps.engine.entity.obj.GameObject +import world.gregs.voidps.engine.inv.add +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.engine.inv.transact.operation.AddItem.add +import world.gregs.voidps.network.login.protocol.visual.update.player.EquipSlot +import world.gregs.voidps.type.Tile +import world.gregs.voidps.type.random + +class KebbitTracking : Script { + + private data class Segment( + val varbit: String, + val tunnel: Boolean, + val inverted: Boolean, + val start: Tile, + val end: Tile, + val trigger: Tile, + val burrow: String?, + ) + + private val trackingTrails = mutableMapOf>() + private val trackingSteps = mutableMapOf() + private val segmentCache = mutableMapOf>() + private val poolCache = mutableMapOf>() + + init { + objectOperate("Inspect", "common_kebbit_burrow,common_kebbit_burrow_2,common_kebbit_burrow_3,polar_kebbit_hole,polar_kebbit_hole_2,desert_devil_burrow,desert_devil_burrow_2,feldip_weasel_burrow,feldip_weasel_burrow_2,razor_backed_kebbit_burrow,razor_backed_kebbit_burrow_2,razor_backed_kebbit_burrow_3") { (target) -> + inspectBurrow(target) + } + + objectOperate("Inspect", "kebbit_tracks_plant_*,kebbit_tunnel_*,kebbit_hollow_log_*,desert_cactus_*,desert_rockslide_*,feldip_plant_*") { (target) -> + inspectTrail(target) + } + + objectOperate("Search", "kebbit_bush,kebbit_snow_drift,disturbed_sand,weasel_bush,razor_kebbit_bush") { (target) -> + inspectTrail(target) + } + + objectOperate("Attack", "kebbit_bush,kebbit_snow_drift,disturbed_sand,weasel_bush,razor_kebbit_bush") { (target) -> + catch(target) + } + + playerDespawn { + clearTrail(this) + } + } + + private fun kebbit(id: String) = when { + id.startsWith("polar") -> "polar_kebbit" + id.startsWith("desert") -> "desert_devil" + id.startsWith("feldip") -> "feldip_weasel" + id.startsWith("razor") -> "razor_backed_kebbit" + else -> "common_kebbit" + } + + private fun segments(kebbit: String): List = segmentCache.getOrPut(kebbit) { load(kebbit) } + + private fun load(kebbit: String): List = Tables.get("trails").rows().filter { it.string("kebbit") == kebbit }.map { row -> + Segment( + varbit = row.string("varbit"), + tunnel = row.bool("tunnel"), + inverted = row.bool("inverted"), + start = row.tile("start"), + end = row.tile("end"), + trigger = row.tileOrNull("trigger") ?: row.tile("end"), + burrow = row.objOrNull("burrow"), + ) + } + + private fun tunnels(kebbit: String) = segments(kebbit).filter { it.tunnel }.flatMap { listOf(it.start, it.end) }.toSet() + + private fun linkingPool(kebbit: String): List = poolCache.getOrPut(kebbit) { + val base = segments(kebbit) + val tunnels = tunnels(kebbit) + // Only polar kebbit trails can route back through a burrow's starting segments + val pool = if (kebbit == "polar_kebbit") base else base.filter { it.burrow == null } + pool + pool.map { inverse(it, tunnels) } + } + + private fun inverse(segment: Segment, tunnels: Set) = Segment( + varbit = segment.varbit, + tunnel = segment.end in tunnels, + inverted = !segment.inverted, + start = segment.end, + end = segment.start, + trigger = segment.trigger, + burrow = null, + ) + + private fun generate(kebbit: String, burrow: String, limit: Int, finals: List): List? { + val trail = mutableListOf(segments(kebbit).filter { it.burrow == burrow }.random(random)) + val pool = linkingPool(kebbit) + var spotsLeft = random.nextInt(2, limit + 1) + var tries = 20 + while (spotsLeft > 0 || trail.last().end !in finals) { + if (tries-- <= 0) { + return trim(trail, finals) + } + val previous = trail.last() + val possible = if (previous.tunnel) { + pool.filter { it.tunnel && it.start != previous.end && it.start.level == previous.end.level && it.start.distanceTo(previous.end) <= 5 } + } else { + pool.filter { it.start == previous.end } + }.filter { next -> trail.none { it.varbit == next.varbit } } + if (possible.isEmpty()) { + return trim(trail, finals) + } + val next = possible.random(random) + trail.add(next) + if (!next.tunnel) { + spotsLeft-- + } + } + return trail + } + + private fun trim(trail: MutableList, finals: List): List? { + while (trail.size > 1 && trail.last().end !in finals) { + trail.removeAt(trail.lastIndex) + } + if (trail.last().end !in finals) { + return null + } + return trail + } + + private fun updateTrail(player: Player) { + val trail = trackingTrails[player.accountName] ?: return + val step = trackingSteps[player.accountName] ?: 0 + for (index in 0..step) { + val segment = trail[index] + player[segment.varbit] = if (segment.inverted) 5 else 4 + } + } + + private fun clearTrail(player: Player) { + val trail = trackingTrails.remove(player.accountName) + trackingSteps.remove(player.accountName) + if (trail != null) { + for (segment in trail) { + player[segment.varbit] = 0 + } + } + } + + private fun Player.inspectBurrow(target: GameObject) { + val kebbit = kebbit(target.id) + val row = Rows.get("tracking.$kebbit") + if (!has(Skill.Hunter, row.int("level"), message = true)) { + return + } + clearTrail(this) + val trail = generate(kebbit, target.id, row.int("limit"), row.tileList("finals")) + if (trail == null) { + message("You search but find nothing of interest.") + return + } + trackingTrails[accountName] = trail + trackingSteps[accountName] = 0 + updateTrail(this) + message("You discover some tracks nearby.") + } + + private fun Player.inspectTrail(target: GameObject) { + val trail = trackingTrails[accountName] + if (trail == null) { + message("You search but find nothing.") + return + } + val step = (trackingSteps[accountName] ?: 0).coerceAtMost(trail.lastIndex) + if (step == trail.lastIndex) { + if (trail[step].end == target.tile) { + message("It looks like something is moving around in there.") + } else { + message("You search but find nothing of interest.") + } + return + } + val current = trail[step + 1] + if (current.trigger == target.tile || current.end == target.tile) { + trackingSteps[accountName] = step + 1 + updateTrail(this) + message("You discover some tracks nearby.") + return + } + message("You search but find nothing of interest.") + } + + private suspend fun Player.catch(target: GameObject) { + val trail = trackingTrails[accountName] + if (trail == null) { + message("You search but find nothing.") + return + } + if (!inventory.contains("noose_wand") && equipped(EquipSlot.Weapon).id != "noose_wand") { + message("You need a noose wand to catch the kebbit.") + return + } + val step = (trackingSteps[accountName] ?: 0).coerceAtMost(trail.lastIndex) + val kebbit = kebbit(trail.first().varbit) + val row = Rows.get("tracking.$kebbit") + sound("hunting_noose") + if (step != trail.lastIndex || trail[step].end != target.tile) { + anim("noose_fail") + delay(2) + if (target.id == "disturbed_sand" && inventory.add("old_boot")) { + message("The trail was false. You find an old boot buried in the sand.") + } else { + message("You fail to find anything with your noose wand.") + } + return + } + anim(row.anim("catch_anim")) + delay(2) + val added = inventory.transaction { + for (item in row.itemList("loot")) { + add(item) + } + } + if (!added) { + message("You don't have enough inventory space.") + return + } + exp(Skill.Hunter, row.int("xp") / 10.0) + clearTrail(this) + message("You've caught a ${kebbit.replace('_', ' ')}!") + } +} diff --git a/game/src/main/kotlin/content/skill/hunter/MagicBox.kt b/game/src/main/kotlin/content/skill/hunter/MagicBox.kt new file mode 100644 index 0000000000..6734cec1df --- /dev/null +++ b/game/src/main/kotlin/content/skill/hunter/MagicBox.kt @@ -0,0 +1,274 @@ +package content.skill.hunter + +import content.entity.effect.transform +import content.entity.player.bank.BankDeposit +import content.entity.player.dialogue.Happy +import content.entity.player.dialogue.Neutral +import content.entity.player.dialogue.Quiz +import content.entity.player.dialogue.type.choice +import content.entity.player.dialogue.type.npc +import content.entity.player.dialogue.type.player +import net.pearx.kasechange.toLowerSpaceCase +import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.client.message +import world.gregs.voidps.engine.client.ui.chat.plural +import world.gregs.voidps.engine.client.ui.close +import world.gregs.voidps.engine.client.ui.open +import world.gregs.voidps.engine.data.config.RowDefinition +import world.gregs.voidps.engine.data.definition.Rows +import world.gregs.voidps.engine.entity.character.npc.NPC +import world.gregs.voidps.engine.entity.character.npc.NPCs +import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.entity.character.player.Players +import world.gregs.voidps.engine.entity.character.player.chat.ChatType +import world.gregs.voidps.engine.entity.character.player.skill.Skill +import world.gregs.voidps.engine.entity.character.player.skill.exp.exp +import world.gregs.voidps.engine.entity.character.player.skill.level.Level +import world.gregs.voidps.engine.entity.character.player.skill.level.Level.has +import world.gregs.voidps.engine.entity.character.sound +import world.gregs.voidps.engine.entity.item.Item +import world.gregs.voidps.engine.entity.item.floor.FloorItem +import world.gregs.voidps.engine.entity.obj.* +import world.gregs.voidps.engine.inv.add +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.engine.inv.remove +import world.gregs.voidps.engine.inv.replace + +class MagicBox : Script { + init { + itemOption("Activate", "magic_box") { + layTrap(null) + } + + floorItemOperate("Lay") { (item) -> + if (item.id == "magic_box") { + layTrap(item) + } + } + + objectOperate("Deactivate", "magic_box,magic_box_fail") { (target) -> + dismantleTrap(target, null) + } + + objectOperate("Retrieve", "magic_box_caught") { (target) -> + dismantleTrap(target, creature = Rows.get("creatures.imp")) + } + + objectOperate("Investigate", "magic_box") { (target) -> + val npc = NPCs.find(target.tile, "hunting_imptrap_npc") + if (npc["owner", ""] == accountName) { + message("This is your magic box, ready to catch an imp.") + } else { + message("This isn't your magic box.") + } + } + + huntNPC("magic_box") { target -> + if (transform.endsWith("_off")) { + return@huntNPC + } + val creature = Rows.getOrNull("creatures.${target.id}") ?: return@huntNPC + val account: String = get("owner") ?: return@huntNPC + val player = Players.findByAccount(account) ?: return@huntNPC + if (!player.has(Skill.Hunter, creature.int("level"))) { + return@huntNPC + } + if (tile.distanceTo(target.tile) > 2 || target["caught", false]) { + return@huntNPC + } + transform("${id}_off") + val chance = Traps.chance(this, creature) + val success = Level.success(player.levels.get(Skill.Hunter), chance) + if (success) { + target["caught"] = true + } + target.walkToDelay(tile) + target.walkOverDelay(tile) + despawn(100) + val trap = GameObjects.getLayer(tile, ObjectLayer.GROUND) ?: return@huntNPC + target.anim(if (success) creature.anim("catch_anim") else creature.anim("fail_anim")) + target.gfx("imp") + target.delay(1) + if (!success) { + trap.replace("magic_box_fail") + return@huntNPC + } + target.levels.set(Skill.Constitution, 0) + val catching = trap.replace("magic_box_catching") + delay(1) + catching.replace("magic_box_caught") + player.message("Something has been caught in your trap!") + } + + npcDespawn("hunting_imptrap_npc") { + Traps.despawn(this, "magic_box", "The magic box that you activated has stopped working.") + } + + itemOption("Talk-to", "imp_in_a_box_2,imp_in_a_box_1") { + impDialogue() + } + + itemOption("Bank", "imp_in_a_box_2,imp_in_a_box_1") { + open("imp_box") + } + + interfaceOpened("imp_box") { + interfaces.sendText("imp_box", "text", depositText()) + interfaceOptions.send("imp_box", "inventory") + interfaceOptions.unlockAll("imp_box", "inventory", 0 until 28) + } + + interfaceOption("Deposit", "imp_box:inventory") { (item, slot) -> + depositItem(item, slot) + } + + interfaceOption("Close", "imp_box:close") { + close("imp_box") + } + + itemOnItem("*", "imp_in_a_box_2,imp_in_a_box_1") { item, box -> + if (item.id.startsWith("imp_in_a_box") || item.id.startsWith("magic_box")) { + message("The imp refuses to take that to your bank.") + return@itemOnItem + } + val before = inventory.count(item.id) + BankDeposit.deposit(this, inventory, item, 1, check = false) + if (inventory.count(item.id) >= before) { + return@itemOnItem + } + if (box.id == "imp_in_a_box_2") { + inventory.replace("imp_in_a_box_2", "imp_in_a_box_1") + message("The imp takes the item to your bank.") + } else { + inventory.replace("imp_in_a_box_1", "magic_box") + message("The imp takes the item to your bank and escapes from the box.") + } + } + } + + private fun Player.depositText() = if (inventory.contains("imp_in_a_box_2")) { + "Select an item or stack of items to deposit.
You can deposit up to 2 items or stacks." + } else { + "Select an item or stack of items to deposit.
You can deposit 1 more item or stack." + } + + private fun Player.depositItem(item: Item, slot: Int) { + if (item.id.startsWith("imp_in_a_box") || item.id.startsWith("magic_box")) { + message("A magical force prevents you from banking this item.") + return + } + val before = inventory.count(item.id) + BankDeposit.deposit(this, inventory, item, item.amount, slot, check = false) + if (inventory.count(item.id) >= before) { + return + } + if (inventory.contains("imp_in_a_box_2")) { + inventory.replace("imp_in_a_box_2", "imp_in_a_box_1") + interfaces.sendText("imp_box", "text", depositText()) + } else if (inventory.contains("imp_in_a_box_1")) { + inventory.replace("imp_in_a_box_1", "magic_box") + close("imp_box") + message("The imp takes your items to the bank and escapes from the box.") + } else { + close("imp_box") + } + } + + private suspend fun Player.impDialogue() { + player("Hey imp, are you still there?") + npc("imp", "Of course I can hear ya, ya great big ape. You know, there's not even enuf space to swing Bob about in 'ere. How about a breather? You know, stretch me pins for a bit?") + impOptions() + } + + private suspend fun Player.impOptions() { + choice { + option("No, I'm going to keep you in there.") { + player("No, I'm going to keep you in there. I might keep you as a pet.") + npc("imp", "Pet!! Nah mate. I fink you'd find dat you'd be my pet!! We is not makin good pets.") + player("Really? Why not?") + npc("imp", "Coz...errr...") + npc("imp", "We bite! Yeah we is biting and...and...er...") + npc("imp", "We is fire risk! Yeah dat's it! We be burning down your housey and stealin' all ya shiny gems. Oh, and da beads!! Mmmm, beads.") + player("Fire risk? How does that work?") + npc("imp", "Is those wizzies. Dey don't like de imps so dey make us go BOOOM!!") + } + option("It's not that bad.") { + player("It's not that bad. You've got four big windows, charming company...er...") + npc("imp", "Yeah, we's love tiny, crampt space. It be magical. But I is a busy imp, innit? Dragons needin' ticklin', shiny relics needin' stealin', you know how it goes.") + npc("imp", "So, if you's know whas good for ya, you'd be lettin' me go, right?") + impOptions() + } + option("Don't I get three wishes?") { + player("Don't I get three wishes?") + npc("imp", "Nah, mate. Dunno what you're chirpin' about.") + player("Well, you're a magical creature aren't you? Surely I get some wishes for capturing you, or releasing you, or something?") + npc("imp", "I'm finking dat you be a bit confoosed. I is an imp, not some namby-pamby genie or some kinda fairy. Ye can tell by the horns.") + npc("imp", "Sayin' dat, I don't fancy being cooped up like one of me uncle's pigeons. Tell you what, is there anything you need deliverin' to the bank?") + npc("imp", "I may not be no cunjerer or sommink like dat, but I can get about nice an quick like. If you let me scarper, I'll take a couple of fings to the bank for ya. You game?") + bankOptions() + } + } + } + + private suspend fun Player.bankOptions() { + choice { + option("Okay, that sounds fair.") { + open("imp_box") + } + option("Surely it should be three items?") { + player("Surely it should be three items? Then it's one item per wish.") + npc("imp", "I've already told ya, I ain't no bloomin' fairy. Besides, you know wot dey say, three's a crowd innit? I don't fink I can hop about carryin' more dan 2 fings.") + bankOptions() + } + option("I've got nothing I need banking right now.") { + player("I've got nothing I need banking right now.") + npc("imp", "Great, just blinkin great, dat is. I'll just sit about countin' zombie sheep then. One...two...two and a bit...three and a bit more... I don't fink sheep 'ave dat many legs...") + } + } + } + + private suspend fun Player.layTrap(floorItem: FloorItem?) { + Traps.lay(this, "magic_box", "lay_box_trap", floorItem) + } + + private suspend fun Player.dismantleTrap(target: GameObject, creature: RowDefinition?) { + val npc = NPCs.findOrNull(target.tile, "hunting_imptrap_npc") ?: return + if (npc["owner", ""] != accountName) { + message("This is not your trap.") + return + } + val loot = creature?.itemList("loot") ?: emptyList() + val items = if (loot.isEmpty()) listOf("magic_box") else emptyList() + val size = items.size + loot.size + if (inventory.spaces < size) { + val slots = size - inventory.spaces + message("You don't have enough inventory space. You need $slots more free ${"slot".plural(slots)}.") + return + } + anim("take_trap") + sound("trap_dismantle", delay = 25) + delay(2) + if (GameObjects.getLayer(target.tile, ObjectLayer.GROUND)?.id != target.id) { + return + } + collapse(npc, target) + for (item in items) { + inventory.add(item) + } + if (creature != null) { + for (item in loot) { + inventory.add(item) + } + exp(Skill.Hunter, creature.int("xp") / 10.0) + message("You've caught an ${creature.rowId.toLowerSpaceCase()}!", ChatType.Filter) + } else { + message("You dismantle the trap.", ChatType.Filter) + } + } + + private fun Player.collapse(npc: NPC, target: GameObject) { + dec("trap_count") + NPCs.remove(npc) + GameObjects.remove(target) + } +} diff --git a/game/src/main/kotlin/content/skill/hunter/NetTrap.kt b/game/src/main/kotlin/content/skill/hunter/NetTrap.kt index 76cda5227a..81201c6ed0 100644 --- a/game/src/main/kotlin/content/skill/hunter/NetTrap.kt +++ b/game/src/main/kotlin/content/skill/hunter/NetTrap.kt @@ -13,7 +13,6 @@ import world.gregs.voidps.engine.data.definition.Areas import world.gregs.voidps.engine.data.definition.Rows import world.gregs.voidps.engine.data.definition.Tables import world.gregs.voidps.engine.entity.character.areaSound -import world.gregs.voidps.engine.entity.character.npc.NPC import world.gregs.voidps.engine.entity.character.npc.NPCs import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.Players @@ -25,6 +24,7 @@ import world.gregs.voidps.engine.entity.character.player.skill.level.Level import world.gregs.voidps.engine.entity.character.player.skill.level.Level.has import world.gregs.voidps.engine.entity.character.sound import world.gregs.voidps.engine.entity.item.Item +import world.gregs.voidps.engine.entity.item.floor.FloorItems import world.gregs.voidps.engine.entity.obj.* import world.gregs.voidps.engine.inv.add import world.gregs.voidps.engine.inv.inventory @@ -63,12 +63,12 @@ class NetTrap : Script { objectOperate("Investigate", "net") { (target) -> val npc = NPCs.find(target.tile, "hunting_sapling_trap_npc") - investigate(npc) + Traps.investigate(this, npc) } objectOperate("Investigate", "*_net_setup") { (target) -> val npc = NPCs.find(target.tile.add(target.direction()), "hunting_sapling_trap_npc") - investigate(npc) + Traps.investigate(this, npc) } itemOnObjectOperate("*", "net,*_net_setup") { (target, item) -> @@ -96,12 +96,15 @@ class NetTrap : Script { if (!player.has(Skill.Hunter, creature.int("level"))) { return@huntNPC } - if (tile.distanceTo(target.tile) > 2) { + if (tile.distanceTo(target.tile) > 2 || target["caught", false]) { return@huntNPC } transform("${id}_off") - var chance = Traps.chance(this, creature) + val chance = Traps.chance(this, creature) val success = Level.success(player.levels.get(Skill.Hunter), chance) + if (success) { + target["caught"] = true + } val trapId = creature.string("trap") target.walkToDelay(tile) target.delay(1) @@ -127,16 +130,24 @@ class NetTrap : Script { npcDespawn("hunting_sapling_trap_npc") { val trap = GameObjects.getLayer(tile.add(direction.inverse()), ObjectLayer.GROUND) ?: return@npcDespawn - val player = owner ?: return@npcDespawn - player.dec("trap_count") val net = GameObjects.findOrNull(trap.tile.add(trap.direction()), "net") net?.remove() GameObjects.remove(trap) + val bait: String? = get("bait") + val items = if (bait != null) listOf("rope", "small_fishing_net", bait) else listOf("rope", "small_fishing_net") + val player = owner + if (player == null) { + for (item in items) { + FloorItems.add(tile, item, disappearTicks = 200) + } + return@npcDespawn + } + player.dec("trap_count") if (lifecycle == 0 || trap.id.endsWith("_net_failed")) { player.message("The net trap that you set has collapsed.") } if (lifecycle == 0 || trap.id.endsWith("_net_failed") || player["logged_out", false]) { - for (item in listOf("rope", "small_fishing_net")) { + for (item in items) { player.drop(tile, item) } } @@ -162,20 +173,6 @@ class NetTrap : Script { message("You place a blob of tar on the net as bait.") } - private fun Player.investigate(npc: NPC) { - val bait: String? = npc["bait"] - if (bait != null) { - message("This trap has been baited with ${bait.toLowerSpaceCase()}.") - } else { - message("This trap has been set without any bait.") - } - if (npc["smoked", false]) { - message("The scent on this trap has been masked.") - } else { - message("Your scent lingers around this trap.") - } - } - private suspend fun Player.layTrap(trapId: String, obj: GameObject) { val trap = Rows.getOrNull("traps.$trapId") ?: return val level = levels.get(Skill.Hunter) diff --git a/game/src/main/kotlin/content/skill/hunter/Traps.kt b/game/src/main/kotlin/content/skill/hunter/Traps.kt index 0e669743a3..563399d752 100644 --- a/game/src/main/kotlin/content/skill/hunter/Traps.kt +++ b/game/src/main/kotlin/content/skill/hunter/Traps.kt @@ -1,18 +1,97 @@ package content.skill.hunter +import content.entity.player.inv.item.drop +import net.pearx.kasechange.toLowerSpaceCase import world.gregs.voidps.engine.client.message +import world.gregs.voidps.engine.client.ui.chat.plural import world.gregs.voidps.engine.data.config.RowDefinition +import world.gregs.voidps.engine.data.definition.Areas +import world.gregs.voidps.engine.data.definition.Rows import world.gregs.voidps.engine.data.definition.Tables import world.gregs.voidps.engine.entity.character.areaSound import world.gregs.voidps.engine.entity.character.npc.NPC import world.gregs.voidps.engine.entity.character.npc.NPCs import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.chat.ChatType +import world.gregs.voidps.engine.entity.character.player.skill.Skill +import world.gregs.voidps.engine.entity.character.player.skill.level.Level.has +import world.gregs.voidps.engine.entity.character.sound +import world.gregs.voidps.engine.entity.item.floor.FloorItem +import world.gregs.voidps.engine.entity.item.floor.FloorItems +import world.gregs.voidps.engine.entity.obj.GameObjects +import world.gregs.voidps.engine.entity.obj.ObjectLayer +import world.gregs.voidps.engine.entity.obj.stepAway +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.engine.inv.remove import world.gregs.voidps.type.Tile object Traps { fun max(level: Int, max: Int) = (1 + level / 20).coerceAtMost(max) + suspend fun lay(player: Player, trapId: String, sound: String, floorItem: FloorItem?) { + val trap = Rows.getOrNull("traps.$trapId") ?: return + val level = player.levels.get(Skill.Hunter) + if (!player.has(Skill.Hunter, trap.int("level"), message = true)) { + return + } + if (Areas.get(player.tile.zone).any { it.tags.contains("bank") } || GameObjects.getLayer(player.tile, ObjectLayer.GROUND) != null) { + player.message("You can't lay a trap here.", ChatType.Filter) + return + } + val max = max(level, trap.int("max")) + if (player.get("trap_count", 0) >= max) { + player.message("You may setup only $max ${"trap".plural(max)} at a time at your Hunter level.") + return + } + player.arriveDelay() + player.message("You begin setting up ${if (max == 1) "the" else "a"} trap.", ChatType.Filter) + player.anim("lay_trap") + player.sound(sound) + player.delay(3) + if (GameObjects.getLayer(player.tile, ObjectLayer.GROUND) != null) { + player.message("You can't lay a trap here.", ChatType.Filter) + return + } + if (floorItem != null) { + if (!FloorItems.remove(floorItem)) { + return + } + } else if (!player.inventory.remove(trapId)) { + return + } + player.inc("trap_count") + NPCs.add(Tables.npc("traps.$trapId.npc"), player.tile, ticks = 100, owner = player) + val obj = GameObjects.add(trapId, player.tile) + player.stepAway(obj) + } + + fun despawn(npc: NPC, trapItem: String, collapseMessage: String) { + val trap = GameObjects.getLayer(npc.tile, ObjectLayer.GROUND) ?: return + GameObjects.remove(trap) + val bait: String? = npc["bait"] + val player = npc.owner + if (player == null) { + FloorItems.add(trap.tile, trapItem, disappearTicks = 200) + if (bait != null) { + FloorItems.add(trap.tile, bait, disappearTicks = 200) + } + return + } + player.dec("trap_count") + val drop = if (npc.lifecycle == 0) { + player.message(collapseMessage) + true + } else { + player["logged_out", false] + } + if (drop) { + player.drop(trap.tile, trapItem) + if (bait != null) { + player.drop(trap.tile, bait) + } + } + } + fun chance(npc: NPC, creature: RowDefinition): IntRange { val chance = creature.intRange("chance") var add = 0 @@ -28,6 +107,20 @@ object Traps { return (chance.first + add)..(chance.last + add) } + fun investigate(player: Player, npc: NPC) { + val bait: String? = npc["bait"] + if (bait != null) { + player.message("This trap has been baited with ${bait.toLowerSpaceCase()}.") + } else { + player.message("This trap has been set without any bait.") + } + if (npc["smoked", false]) { + player.message("The scent on this trap has been masked.") + } else { + player.message("Your scent lingers around this trap.") + } + } + fun smoke(player: Player, trap: String, tile: Tile) { val id = Tables.npc("traps.$trap.npc") val npc = NPCs.find(tile, id) diff --git a/game/src/test/kotlin/content/skill/hunter/BoxTrapTest.kt b/game/src/test/kotlin/content/skill/hunter/BoxTrapTest.kt new file mode 100644 index 0000000000..df4b23200c --- /dev/null +++ b/game/src/test/kotlin/content/skill/hunter/BoxTrapTest.kt @@ -0,0 +1,260 @@ +package content.skill.hunter + +import FakeRandom +import WorldTest +import containsMessage +import itemOnObject +import itemOption +import objectOption +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertNotNull +import org.junit.jupiter.api.assertNull +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import world.gregs.voidps.engine.entity.character.move.tele +import world.gregs.voidps.engine.entity.character.player.skill.Skill +import world.gregs.voidps.engine.entity.item.floor.FloorItems +import world.gregs.voidps.engine.entity.obj.GameObjects +import world.gregs.voidps.engine.inv.add +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.type.setRandom +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertTrue + +class BoxTrapTest : WorldTest() { + @ParameterizedTest + @ValueSource(strings = ["chinchompa", "carnivorous_chinchompa"]) + fun `Catch a chinchompa`(id: String) { + val player = createPlayer() + val start = player.tile + player.inventory.add("box_trap") + player.levels.set(Skill.Hunter, 99) + + player.itemOption("Lay", "box_trap") + tick(3) + assertTrue(GameObjects.at(start).any { it.id == "box_trap" }) + createNPC(id, player.tile.addY(2)) + + tick(22) + + val trap = GameObjects.at(start).firstOrNull { it.id == "box_trap_$id" } + assertNotNull(trap) + + player.objectOption(trap, "Check") + tick(3) + assertEquals(1, player.inventory.count("box_trap")) + assertNotEquals(0.0, player.experience.get(Skill.Hunter)) + } + + @ParameterizedTest + @ValueSource(strings = ["pawya", "grenwall"]) + fun `Catch with bait`(id: String) { + val bait = if (id == "pawya") "papaya_fruit" else "raw_pawya_meat" + val player = createPlayer() + val start = player.tile + player.inventory.add("box_trap") + player.inventory.add(bait) + player.levels.set(Skill.Hunter, 99) + + player.itemOption("Lay", "box_trap") + tick(3) + val laid = GameObjects.at(start).firstOrNull { it.id == "box_trap" } + assertNotNull(laid) + player.itemOnObject(laid, player.inventory.indexOf(bait)) + tick(2) + assertEquals(0, player.inventory.count(bait)) + createNPC(id, player.tile.addY(2)) + + tick(22) + + val trap = GameObjects.at(start).firstOrNull { it.id == "box_trap_$id" } + assertNotNull(trap) + + player.objectOption(trap, "Check") + tick(3) + assertEquals(1, player.inventory.count("box_trap")) + assertNotEquals(0.0, player.experience.get(Skill.Hunter)) + if (id == "pawya") { + assertEquals(1, player.inventory.count("raw_pawya_meat")) + } else { + assertEquals(18, player.inventory.count("grenwall_spikes")) + } + } + + @Test + fun `Grenwall spikes stack when checking with few free slots`() { + val player = createPlayer() + val start = player.tile + player.inventory.add("box_trap") + player.inventory.add("raw_pawya_meat") + player.levels.set(Skill.Hunter, 99) + + player.itemOption("Lay", "box_trap") + tick(3) + val laid = GameObjects.at(start).firstOrNull { it.id == "box_trap" } + assertNotNull(laid) + player.itemOnObject(laid, player.inventory.indexOf("raw_pawya_meat")) + tick(2) + player.inventory.add("bones", 26) + assertEquals(2, player.inventory.spaces) + createNPC("grenwall", player.tile.addY(2)) + + tick(22) + + val trap = GameObjects.at(start).firstOrNull { it.id == "box_trap_grenwall" } + assertNotNull(trap) + player.objectOption(trap, "Check") + tick(3) + assertEquals(18, player.inventory.count("grenwall_spikes")) + assertEquals(1, player.inventory.count("box_trap")) + } + + @Test + fun `Bait is not returned when a caught trap collapses`() { + val player = createPlayer() + val start = player.tile + player.inventory.add("box_trap") + player.inventory.add("papaya_fruit") + player.levels.set(Skill.Hunter, 99) + + player.itemOption("Lay", "box_trap") + tick(3) + val laid = GameObjects.at(start).firstOrNull { it.id == "box_trap" } + assertNotNull(laid) + player.itemOnObject(laid, player.inventory.indexOf("papaya_fruit")) + tick(2) + createNPC("pawya", player.tile.addY(2)) + + tick(22) + assertNotNull(GameObjects.at(start).firstOrNull { it.id == "box_trap_pawya" }) + + tick(110) + assertEquals(1, FloorItems.at(start).count { it.id == "box_trap" }) + assertEquals(0, FloorItems.at(start).count { it.id == "papaya_fruit" }) + } + + @Test + fun `Grenwall ignores unbaited trap`() { + val player = createPlayer() + val start = player.tile + player.inventory.add("box_trap") + player.levels.set(Skill.Hunter, 99) + + player.itemOption("Lay", "box_trap") + tick(3) + assertTrue(GameObjects.at(start).any { it.id == "box_trap" }) + createNPC("grenwall", player.tile.addY(2)) + + tick(22) + + assertNull(GameObjects.at(start).firstOrNull { it.id == "box_trap_grenwall" }) + assertTrue(GameObjects.at(start).any { it.id == "box_trap" }) + } + + @Test + fun `Ferret requires Eagles' Peak`() { + val player = createPlayer() + val start = player.tile + player.inventory.add("box_trap") + player.levels.set(Skill.Hunter, 99) + + player.itemOption("Lay", "box_trap") + tick(3) + createNPC("ferret", player.tile.addY(2)) + + tick(22) + + assertNull(GameObjects.at(start).firstOrNull { it.id == "box_trap_ferret" }) + } + + @Test + fun `Catch a ferret after Eagles' Peak`() { + val player = createPlayer() + val start = player.tile + player["eagles_peak"] = "completed" + player.inventory.add("box_trap") + player.levels.set(Skill.Hunter, 99) + + player.itemOption("Lay", "box_trap") + tick(3) + createNPC("ferret", player.tile.addY(2)) + + tick(22) + + val trap = GameObjects.at(start).firstOrNull { it.id == "box_trap_ferret" } + assertNotNull(trap) + + player.objectOption(trap, "Check") + tick(3) + assertEquals(1, player.inventory.count("box_trap")) + assertEquals(1, player.inventory.count("ferret")) + assertNotEquals(0.0, player.experience.get(Skill.Hunter)) + } + + @Test + fun `Can't lay without hunter level`() { + val player = createPlayer() + val start = player.tile + player.inventory.add("box_trap") + + player.itemOption("Lay", "box_trap") + tick(3) + assertFalse(GameObjects.at(start).any { it.id == "box_trap" }) + assertEquals(1, player.inventory.count("box_trap")) + } + + @Test + fun `Fail to catch`() { + setRandom(object : FakeRandom() { + override fun nextInt(until: Int) = if (until == 4) 0 else until - 1 + }) + val player = createPlayer() + val start = player.tile + player.inventory.add("box_trap") + player.levels.set(Skill.Hunter, 60) + + player.itemOption("Lay", "box_trap") + tick(3) + assertTrue(GameObjects.at(start).any { it.id == "box_trap" }) + createNPC("chinchompa", player.tile.addY(2)) + + tick(22) + + val trap = GameObjects.at(start).firstOrNull { it.id == "box_trap_fail" } + assertNotNull(trap) + + player.objectOption(trap, "Dismantle") + tick(3) + assertEquals(1, player.inventory.count("box_trap")) + assertEquals(0.0, player.experience.get(Skill.Hunter)) + } + + @Test + fun `Can't place more than two traps at level 27`() { + val player = createPlayer() + player.levels.set(Skill.Hunter, 27) + var start = player.tile + player.inventory.add("box_trap", 3) + + player.itemOption("Lay", "box_trap") + tick(3) + assertTrue(GameObjects.at(start).any { it.id == "box_trap" }) + + player.tele(start.x + 5, start.y) + tick() + start = player.tile + player.itemOption("Lay", "box_trap") + tick(3) + assertTrue(GameObjects.at(start).any { it.id == "box_trap" }) + + player.tele(start.x + 5, start.y) + tick() + start = player.tile + player.itemOption("Lay", "box_trap") + tick(3) + assertFalse(GameObjects.at(start).any { it.id == "box_trap" }) + assertTrue(player.containsMessage("only 2 traps at a time")) + } +} diff --git a/game/src/test/kotlin/content/skill/hunter/DeadfallTest.kt b/game/src/test/kotlin/content/skill/hunter/DeadfallTest.kt new file mode 100644 index 0000000000..ff3c39f0f9 --- /dev/null +++ b/game/src/test/kotlin/content/skill/hunter/DeadfallTest.kt @@ -0,0 +1,145 @@ +package content.skill.hunter + +import FakeRandom +import WorldTest +import containsMessage +import objectOption +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertNotNull +import org.junit.jupiter.api.assertNull +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import world.gregs.voidps.engine.entity.character.player.skill.Skill +import world.gregs.voidps.engine.entity.obj.GameObjects +import world.gregs.voidps.engine.inv.add +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.type.Tile +import world.gregs.voidps.type.setRandom +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertTrue + +class DeadfallTest : WorldTest() { + @ParameterizedTest + @CsvSource( + "wild_kebbit, kebbit_claws", + "barb_tailed_kebbit, barb_tail_harpoon", + "prickly_kebbit, kebbit_spike", + "sabre_toothed_kebbit, kebbit_teeth", + ) + fun `Catch a kebbit`(id: String, loot: String) { + val player = createPlayer(Tile(2557, 2895)) + val boulder = createObject("boulder_trap", player.tile.addX(1)) + player.inventory.add("knife") + player.inventory.add("logs") + player.levels.set(Skill.Hunter, 99) + + player.objectOption(boulder, "Set-trap") + tick(10) + assertTrue(GameObjects.at(boulder.tile).any { it.id == "boulder_trap_setup" }) + assertEquals(0, player.inventory.count("logs")) + assertEquals(1, player.inventory.count("knife")) + createNPC(id, boulder.tile.addY(2)) + + tick(22) + + val trap = GameObjects.at(boulder.tile).firstOrNull { it.id == "boulder_trap_$id" } + assertNotNull(trap) + + player.objectOption(trap, "Check") + tick(10) + assertEquals(1, player.inventory.count("bones")) + assertEquals(1, player.inventory.count(loot)) + assertEquals(0, player.inventory.count("logs")) + assertNotEquals(0.0, player.experience.get(Skill.Hunter)) + assertFalse(GameObjects.at(boulder.tile).any { it.id.startsWith("boulder_trap_") }) + } + + @Test + fun `Can't set trap without a knife`() { + val player = createPlayer(Tile(2557, 2895)) + val boulder = createObject("boulder_trap", player.tile.addX(1)) + player.inventory.add("logs") + player.levels.set(Skill.Hunter, 99) + + player.objectOption(boulder, "Set-trap") + tick(10) + assertFalse(GameObjects.at(boulder.tile).any { it.id == "boulder_trap_setup" }) + assertTrue(player.containsMessage("You need a knife")) + } + + @Test + fun `Can't set trap without hunter level`() { + val player = createPlayer(Tile(2557, 2895)) + val boulder = createObject("boulder_trap", player.tile.addX(1)) + player.inventory.add("knife") + player.inventory.add("logs") + + player.objectOption(boulder, "Set-trap") + tick(10) + assertFalse(GameObjects.at(boulder.tile).any { it.id == "boulder_trap_setup" }) + } + + @Test + fun `Only one deadfall at a time`() { + val player = createPlayer(Tile(2557, 2895)) + val boulder = createObject("boulder_trap", player.tile.addX(1)) + val second = createObject("boulder_trap", player.tile.addX(-2)) + player.inventory.add("knife") + player.inventory.add("logs", 2) + player.levels.set(Skill.Hunter, 99) + + player.objectOption(boulder, "Set-trap") + tick(10) + assertTrue(GameObjects.at(boulder.tile).any { it.id == "boulder_trap_setup" }) + + player.objectOption(second, "Set-trap") + tick(10) + assertFalse(GameObjects.at(second.tile).any { it.id == "boulder_trap_setup" }) + assertTrue(player.containsMessage("only set up one deadfall")) + } + + @Test + fun `Fail to catch collapses the trap`() { + setRandom(object : FakeRandom() { + override fun nextInt(until: Int) = if (until == 4) 0 else until - 1 + }) + val player = createPlayer(Tile(2557, 2895)) + val boulder = createObject("boulder_trap", player.tile.addX(1)) + player.inventory.add("knife") + player.inventory.add("logs") + player.levels.set(Skill.Hunter, 50) + + player.objectOption(boulder, "Set-trap") + tick(10) + assertTrue(GameObjects.at(boulder.tile).any { it.id == "boulder_trap_setup" }) + createNPC("wild_kebbit", boulder.tile.addY(2)) + + tick(22) + + assertNull(GameObjects.at(boulder.tile).firstOrNull { it.id == "boulder_trap_wild_kebbit" }) + assertNull(GameObjects.at(boulder.tile).firstOrNull { it.id == "boulder_trap_setup" }) + assertEquals(0, player.inventory.count("logs")) + assertEquals(0.0, player.experience.get(Skill.Hunter)) + } + + @Test + fun `Dismantle returns the logs`() { + val player = createPlayer(Tile(2557, 2895)) + val boulder = createObject("boulder_trap", player.tile.addX(1)) + player.inventory.add("knife") + player.inventory.add("logs") + player.levels.set(Skill.Hunter, 99) + + player.objectOption(boulder, "Set-trap") + tick(10) + val trap = GameObjects.at(boulder.tile).firstOrNull { it.id == "boulder_trap_setup" } + assertNotNull(trap) + + player.objectOption(trap, "Dismantle") + tick(10) + assertEquals(1, player.inventory.count("logs")) + assertFalse(GameObjects.at(boulder.tile).any { it.id.startsWith("boulder_trap_") }) + } +} diff --git a/game/src/test/kotlin/content/skill/hunter/FalconryTest.kt b/game/src/test/kotlin/content/skill/hunter/FalconryTest.kt new file mode 100644 index 0000000000..6a78e01ed0 --- /dev/null +++ b/game/src/test/kotlin/content/skill/hunter/FalconryTest.kt @@ -0,0 +1,119 @@ +package content.skill.hunter + +import FakeRandom +import WorldTest +import npcOption +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertNotNull +import org.junit.jupiter.api.assertNull +import world.gregs.voidps.engine.entity.character.move.tele +import world.gregs.voidps.engine.entity.character.npc.NPCs +import world.gregs.voidps.engine.entity.character.player.equip.equipped +import world.gregs.voidps.engine.entity.character.player.skill.Skill +import world.gregs.voidps.engine.inv.add +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.network.login.protocol.visual.update.player.EquipSlot +import world.gregs.voidps.type.Tile +import world.gregs.voidps.type.setRandom +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class FalconryTest : WorldTest() { + @Test + fun `Borrow a falcon from Matthias`() { + val player = createPlayer(Tile(2376, 3605)) + val matthias = createNPC("matthias", Tile(2376, 3606)) + player.inventory.add("coins", 500) + player.levels.set(Skill.Hunter, 43) + + player.npcOption(matthias, "Falconry") + tick(5) + assertEquals("falconers_glove_2", player.equipped(EquipSlot.Weapon).id) + assertEquals(0, player.inventory.count("coins")) + } + + @Test + fun `Can't borrow a falcon without the level`() { + val player = createPlayer(Tile(2376, 3605)) + val matthias = createNPC("matthias", Tile(2376, 3606)) + player.inventory.add("coins", 500) + + player.npcOption(matthias, "Falconry") + tick(5) + assertTrue(player.equipped(EquipSlot.Weapon).isEmpty()) + assertEquals(500, player.inventory.count("coins")) + } + + @Test + fun `Can't borrow a falcon without coins`() { + val player = createPlayer(Tile(2376, 3605)) + val matthias = createNPC("matthias", Tile(2376, 3606)) + player.levels.set(Skill.Hunter, 43) + + player.npcOption(matthias, "Falconry") + tick(5) + assertTrue(player.equipped(EquipSlot.Weapon).isEmpty()) + } + + @Test + fun `Catch and retrieve a kebbit`() { + val player = createPlayer(Tile(2376, 3605)) + val matthias = createNPC("matthias", Tile(2376, 3606)) + player.inventory.add("coins", 500) + player.levels.set(Skill.Hunter, 99) + player.npcOption(matthias, "Falconry") + tick(5) + assertEquals("falconers_glove_2", player.equipped(EquipSlot.Weapon).id) + + val kebbit = createNPC("spotted_kebbit", Tile(2378, 3600)) + player.npcOption(kebbit, "Catch") + tick(10) + + assertEquals("falconers_glove", player.equipped(EquipSlot.Weapon).id) + val caught = NPCs.at(kebbit.tile).firstOrNull { it.id == "spotted_kebbit_caught" } + assertNotNull(caught) + + player.npcOption(caught, "Retrieve") + tick(10) + assertEquals("falconers_glove_2", player.equipped(EquipSlot.Weapon).id) + assertEquals(1, player.inventory.count("spotted_kebbit_fur")) + assertEquals(1, player.inventory.count("bones")) + assertEquals(104.0, player.experience.get(Skill.Hunter)) + } + + @Test + fun `Fail to catch a kebbit`() { + setRandom(object : FakeRandom() { + override fun nextInt(until: Int) = if (until == 4) 0 else until - 1 + }) + val player = createPlayer(Tile(2376, 3605)) + val matthias = createNPC("matthias", Tile(2376, 3606)) + player.inventory.add("coins", 500) + player.levels.set(Skill.Hunter, 50) + player.npcOption(matthias, "Falconry") + tick(5) + + val kebbit = createNPC("spotted_kebbit", Tile(2378, 3600)) + player.npcOption(kebbit, "Catch") + tick(10) + + assertEquals("falconers_glove_2", player.equipped(EquipSlot.Weapon).id) + assertNull(NPCs.at(kebbit.tile).firstOrNull { it.id == "spotted_kebbit_caught" }) + assertEquals(0.0, player.experience.get(Skill.Hunter)) + } + + @Test + fun `Leaving the area returns the falcon`() { + val player = createPlayer(Tile(2376, 3605)) + val matthias = createNPC("matthias", Tile(2376, 3606)) + player.inventory.add("coins", 500) + player.levels.set(Skill.Hunter, 43) + player.npcOption(matthias, "Falconry") + tick(5) + assertEquals("falconers_glove_2", player.equipped(EquipSlot.Weapon).id) + + player.tele(2340, 3605) + tick(2) + assertTrue(player.equipped(EquipSlot.Weapon).isEmpty()) + } +} diff --git a/game/src/test/kotlin/content/skill/hunter/KebbitTrackingTest.kt b/game/src/test/kotlin/content/skill/hunter/KebbitTrackingTest.kt new file mode 100644 index 0000000000..0a6027bb2d --- /dev/null +++ b/game/src/test/kotlin/content/skill/hunter/KebbitTrackingTest.kt @@ -0,0 +1,390 @@ +package content.skill.hunter + +import WorldTest +import containsMessage +import objectOption +import org.junit.jupiter.api.Test +import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.entity.character.player.skill.Skill +import world.gregs.voidps.engine.entity.obj.GameObject +import world.gregs.voidps.engine.inv.add +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.engine.inv.remove +import world.gregs.voidps.type.Tile +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class KebbitTrackingTest : WorldTest() { + + // With FakeRandom the common kebbit trail is always burrow -> trail_0 -> trail_3 -> trail_4, + // triggered at (2358,3599) then (2352,3603), ending at (2349,3604) + private fun startTrail(player: Player): GameObject { + val burrow = createObject("common_kebbit_burrow", Tile(2354, 3595)) + player.objectOption(burrow, "Inspect") + tick(5) + return burrow + } + + @Test + fun `Track and catch a common kebbit`() { + val player = createPlayer(Tile(2353, 3595)) + player.inventory.add("noose_wand") + player.levels.set(Skill.Hunter, 3) + startTrail(player) + assertEquals(4, player["common_kebbit_trail_0", 0]) + + val first = createObject("kebbit_tracks_plant_0", Tile(2358, 3599)) + player.objectOption(first, "Inspect") + tick(10) + assertEquals(4, player["common_kebbit_trail_3", 0]) + + val second = createObject("kebbit_tracks_plant_1", Tile(2352, 3603)) + player.objectOption(second, "Inspect") + tick(10) + assertEquals(4, player["common_kebbit_trail_4", 0]) + + val bush = createObject("kebbit_bush", Tile(2349, 3604)) + player.objectOption(bush, "Search") + tick(10) + assertTrue(player.containsMessage("something is moving around")) + + player.objectOption(bush, "Attack") + tick(5) + assertEquals(1, player.inventory.count("common_kebbit_fur")) + assertEquals(1, player.inventory.count("bones")) + assertEquals(1, player.inventory.count("raw_beast_meat")) + assertEquals(36.0, player.experience.get(Skill.Hunter)) + assertEquals(0, player["common_kebbit_trail_0", 0]) + assertEquals(0, player["common_kebbit_trail_3", 0]) + } + + @Test + fun `Wrong trail object reveals nothing`() { + val player = createPlayer(Tile(2353, 3595)) + player.levels.set(Skill.Hunter, 3) + startTrail(player) + + val wrong = createObject("kebbit_tracks_plant_2", Tile(2352, 3603)) + player.objectOption(wrong, "Inspect") + tick(15) + assertEquals(0, player["common_kebbit_trail_3", 0]) + assertEquals(0, player["common_kebbit_trail_4", 0]) + assertTrue(player.containsMessage("You search but find nothing of interest")) + } + + @Test + fun `Can't catch without a noose wand`() { + val player = createPlayer(Tile(2353, 3595)) + player.levels.set(Skill.Hunter, 3) + startTrail(player) + + val bush = createObject("kebbit_bush", Tile(2349, 3604)) + player.objectOption(bush, "Attack") + tick(15) + assertTrue(player.containsMessage("You need a noose wand")) + assertEquals(0.0, player.experience.get(Skill.Hunter)) + } + + @Test + fun `Attacking the wrong spot fails`() { + val player = createPlayer(Tile(2353, 3595)) + player.inventory.add("noose_wand") + player.levels.set(Skill.Hunter, 3) + startTrail(player) + + val bush = createObject("kebbit_bush", Tile(2349, 3604)) + player.objectOption(bush, "Attack") + tick(15) + assertEquals(0, player.inventory.count("common_kebbit_fur")) + assertEquals(0.0, player.experience.get(Skill.Hunter)) + assertEquals(4, player["common_kebbit_trail_0", 0]) + } + + // With FakeRandom the desert trail is always burrow -> trail_7 -> inv trail_3 -> trail_4 -> trail_5 -> trail_6, + // advanced at sand (3393,3122), rockslide (3405,3122), cactus (3409,3121), ending at sand (3414,3121) + @Test + fun `Track and catch a desert devil`() { + val player = createPlayer(Tile(3395, 3106)) + player.inventory.add("noose_wand") + player.inventory.add("waterskin_4", 3) + player.levels.set(Skill.Hunter, 13) + val burrow = createObject("desert_devil_burrow", Tile(3396, 3106)) + player.objectOption(burrow, "Inspect") + tick(5) + assertEquals(4, player["desert_devil_trail_7", 0]) + + val sand = createObject("disturbed_sand", Tile(3393, 3122)) + player.objectOption(sand, "Search") + tick(20) + assertEquals(5, player["desert_devil_trail_3", 0]) + + val rockslide = createObject("desert_rockslide_1", Tile(3405, 3122)) + player.objectOption(rockslide, "Inspect") + tick(20) + assertEquals(4, player["desert_devil_trail_4", 0]) + + val cactus = createObject("desert_cactus_4", Tile(3409, 3121)) + player.objectOption(cactus, "Inspect") + tick(15) + assertEquals(4, player["desert_devil_trail_5", 0]) + + val last = createObject("disturbed_sand", Tile(3414, 3121)) + player.objectOption(last, "Search") + tick(15) + assertEquals(4, player["desert_devil_trail_6", 0]) + player.objectOption(last, "Search") + tick(5) + assertTrue(player.containsMessage("something is moving around")) + + player.objectOption(last, "Attack") + tick(5) + assertEquals(1, player.inventory.count("desert_devil_fur")) + assertEquals(1, player.inventory.count("bones")) + assertEquals(1, player.inventory.count("raw_beast_meat")) + assertEquals(66.0, player.experience.get(Skill.Hunter)) + assertEquals(0, player["desert_devil_trail_7", 0]) + } + + // The common trail's final segment has a side trigger at (2352,3603) — re-inspecting it once + // the trail is fully revealed must not advance the step past the final segment + @Test + fun `Re-inspecting the final segment's trigger doesn't break the trail`() { + val player = createPlayer(Tile(2353, 3595)) + player.inventory.add("noose_wand") + player.levels.set(Skill.Hunter, 3) + startTrail(player) + + val first = createObject("kebbit_tracks_plant_0", Tile(2358, 3599)) + player.objectOption(first, "Inspect") + tick(10) + val second = createObject("kebbit_tracks_plant_1", Tile(2352, 3603)) + player.objectOption(second, "Inspect") + tick(10) + assertEquals(4, player["common_kebbit_trail_4", 0]) + + player.objectOption(second, "Inspect") + tick(10) + assertTrue(player.containsMessage("You search but find nothing of interest")) + + val bush = createObject("kebbit_bush", Tile(2349, 3604)) + player.objectOption(bush, "Search") + tick(10) + assertTrue(player.containsMessage("something is moving around")) + + player.objectOption(bush, "Attack") + tick(5) + assertEquals(1, player.inventory.count("common_kebbit_fur")) + assertEquals(36.0, player.experience.get(Skill.Hunter)) + } + + // The second desert segment runs inverted (sand at 3393,3122 back to the cactus at 3396,3121), + // so following the revealed tracks to the cactus must advance the trail too + @Test + fun `Inspecting the object at the end of an inverted segment advances the trail`() { + val player = createPlayer(Tile(3395, 3106)) + player.inventory.add("waterskin_4", 3) + player.levels.set(Skill.Hunter, 13) + val burrow = createObject("desert_devil_burrow", Tile(3396, 3106)) + player.objectOption(burrow, "Inspect") + tick(5) + assertEquals(4, player["desert_devil_trail_7", 0]) + + val cactus = createObject("desert_cactus_2", Tile(3396, 3121)) + player.objectOption(cactus, "Inspect") + tick(20) + assertEquals(5, player["desert_devil_trail_3", 0]) + assertTrue(player.containsMessage("You discover some tracks nearby")) + } + + @Test + fun `False trail digs up an old boot`() { + val player = createPlayer(Tile(3395, 3106)) + player.inventory.add("noose_wand") + player.inventory.add("waterskin_4", 3) + player.levels.set(Skill.Hunter, 13) + val burrow = createObject("desert_devil_burrow", Tile(3396, 3106)) + player.objectOption(burrow, "Inspect") + tick(5) + + val wrong = createObject("disturbed_sand", Tile(3400, 3114)) + player.objectOption(wrong, "Attack") + tick(15) + assertEquals(1, player.inventory.count("old_boot")) + assertEquals(0.0, player.experience.get(Skill.Hunter)) + assertEquals(4, player["desert_devil_trail_7", 0]) + } + + @Test + fun `Can't track a desert devil below level 13`() { + val player = createPlayer(Tile(3395, 3106)) + player.inventory.add("waterskin_4", 3) + val burrow = createObject("desert_devil_burrow", Tile(3396, 3106)) + player.objectOption(burrow, "Inspect") + tick(5) + assertEquals(0, player["desert_devil_trail_7", 0]) + } + + // With FakeRandom the feldip trail is always burrow -> trail_0 -> trail_4 -> trail_5, + // advanced at the plant (2533,2882), ending at the bush (2533,2885) + @Test + fun `Track and catch a feldip weasel`() { + val player = createPlayer(Tile(2524, 2889)) + player.inventory.add("noose_wand") + player.levels.set(Skill.Hunter, 7) + val burrow = createObject("feldip_weasel_burrow", Tile(2525, 2889)) + player.objectOption(burrow, "Inspect") + tick(5) + assertEquals(4, player["feldip_weasel_trail_0", 0]) + + val plant = createObject("feldip_plant_5", Tile(2533, 2882)) + player.objectOption(plant, "Inspect") + tick(15) + assertEquals(4, player["feldip_weasel_trail_4", 0]) + + val bush = createObject("weasel_bush", Tile(2533, 2885)) + player.objectOption(bush, "Search") + tick(10) + assertEquals(4, player["feldip_weasel_trail_5", 0]) + player.objectOption(bush, "Search") + tick(5) + assertTrue(player.containsMessage("something is moving around")) + + player.objectOption(bush, "Attack") + tick(5) + assertEquals(1, player.inventory.count("feldip_weasel_fur")) + assertEquals(1, player.inventory.count("bones")) + assertEquals(1, player.inventory.count("raw_beast_meat")) + assertEquals(48.0, player.experience.get(Skill.Hunter)) + assertEquals(0, player["feldip_weasel_trail_0", 0]) + } + + @Test + fun `Can't track a feldip weasel below level 7`() { + val player = createPlayer(Tile(2524, 2889)) + val burrow = createObject("feldip_weasel_burrow", Tile(2525, 2889)) + player.objectOption(burrow, "Inspect") + tick(5) + assertEquals(0, player["feldip_weasel_trail_0", 0]) + } + + // With FakeRandom the razor trail is always burrow -> trail_0 -> trail_5 -> inverse trail_11, + // advanced at bushes (2323,3563) then (2332,3568), ending at the bush (2327,3573) + @Test + fun `Track and catch a razor-backed kebbit`() { + val player = createPlayer(Tile(2330, 3562)) + player.inventory.add("noose_wand") + player.levels.set(Skill.Hunter, 49) + val burrow = createObject("razor_backed_kebbit_burrow", Tile(2331, 3562)) + player.objectOption(burrow, "Inspect") + tick(5) + assertEquals(4, player["razor_backed_kebbit_trail_0", 0]) + + val first = createObject("razor_kebbit_bush", Tile(2332, 3568)) + player.objectOption(first, "Search") + tick(15) + assertEquals(4, player["razor_backed_kebbit_trail_5", 0]) + + val last = createObject("razor_kebbit_bush", Tile(2327, 3573)) + player.objectOption(last, "Search") + tick(15) + assertEquals(5, player["razor_backed_kebbit_trail_11", 0]) + player.objectOption(last, "Search") + tick(5) + assertTrue(player.containsMessage("something is moving around")) + + player.objectOption(last, "Attack") + tick(5) + assertEquals(1, player.inventory.count("long_kebbit_spike")) + assertEquals(1, player.inventory.count("raw_beast_meat")) + assertEquals(1, player.inventory.count("bones")) + assertEquals(348.0, player.experience.get(Skill.Hunter)) + assertEquals(0, player["razor_backed_kebbit_trail_0", 0]) + } + + // With FakeRandom the north burrow trail is burrow -> trail_10 -> trail_18, trimmed short + // because nothing links onward from the bush at (2358,3620) + @Test + fun `Track a common kebbit from the north burrow`() { + val player = createPlayer(Tile(2356, 3624)) + player.inventory.add("noose_wand") + player.levels.set(Skill.Hunter, 3) + val burrow = createObject("common_kebbit_burrow_3", Tile(2357, 3624)) + player.objectOption(burrow, "Inspect") + tick(5) + assertEquals(4, player["common_kebbit_trail_10", 0]) + + val last = createObject("kebbit_bush", Tile(2358, 3620)) + player.objectOption(last, "Search") + tick(15) + assertEquals(4, player["common_kebbit_trail_18", 0]) + player.objectOption(last, "Search") + tick(5) + assertTrue(player.containsMessage("something is moving around")) + + player.objectOption(last, "Attack") + tick(5) + assertEquals(1, player.inventory.count("common_kebbit_fur")) + assertEquals(36.0, player.experience.get(Skill.Hunter)) + } + + @Test + fun `Can't track a razor-backed kebbit below level 49`() { + val player = createPlayer(Tile(2330, 3562)) + val burrow = createObject("razor_backed_kebbit_burrow", Tile(2331, 3562)) + player.objectOption(burrow, "Inspect") + tick(5) + assertEquals(0, player["razor_backed_kebbit_trail_0", 0]) + } + + @Test + fun `Full inventory keeps the trail and gives no loot`() { + val player = createPlayer(Tile(2353, 3595)) + player.inventory.add("noose_wand") + player.inventory.add("bones", 27) + player.levels.set(Skill.Hunter, 3) + startTrail(player) + + val first = createObject("kebbit_tracks_plant_0", Tile(2358, 3599)) + player.objectOption(first, "Inspect") + tick(10) + val second = createObject("kebbit_tracks_plant_1", Tile(2352, 3603)) + player.objectOption(second, "Inspect") + tick(10) + val bush = createObject("kebbit_bush", Tile(2349, 3604)) + player.objectOption(bush, "Attack") + tick(10) + assertTrue(player.containsMessage("You don't have enough inventory space")) + assertEquals(0.0, player.experience.get(Skill.Hunter)) + assertEquals(4, player["common_kebbit_trail_0", 0]) + + player.inventory.remove("bones", 27) + player.objectOption(bush, "Attack") + tick(5) + assertEquals(1, player.inventory.count("common_kebbit_fur")) + assertEquals(36.0, player.experience.get(Skill.Hunter)) + } + + @Test + fun `Inspecting a burrow mid-trail starts a fresh trail`() { + val player = createPlayer(Tile(2353, 3595)) + player.levels.set(Skill.Hunter, 3) + val burrow = startTrail(player) + + val first = createObject("kebbit_tracks_plant_0", Tile(2358, 3599)) + player.objectOption(first, "Inspect") + tick(10) + assertEquals(4, player["common_kebbit_trail_3", 0]) + + player.objectOption(burrow, "Inspect") + tick(10) + assertEquals(4, player["common_kebbit_trail_0", 0]) + assertEquals(0, player["common_kebbit_trail_3", 0]) + } + + @Test + fun `Can't track a common kebbit below level 3`() { + val player = createPlayer(Tile(2353, 3595)) + startTrail(player) + assertEquals(0, player["common_kebbit_trail_0", 0]) + } +} diff --git a/game/src/test/kotlin/content/skill/hunter/MagicBoxTest.kt b/game/src/test/kotlin/content/skill/hunter/MagicBoxTest.kt new file mode 100644 index 0000000000..f0c8ba7e41 --- /dev/null +++ b/game/src/test/kotlin/content/skill/hunter/MagicBoxTest.kt @@ -0,0 +1,159 @@ +package content.skill.hunter + +import FakeRandom +import WorldTest +import content.entity.player.bank.bank +import interfaceOption +import itemOnItem +import itemOption +import objectOption +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertNotNull +import world.gregs.voidps.engine.client.ui.hasOpen +import world.gregs.voidps.engine.entity.character.player.skill.Skill +import world.gregs.voidps.engine.entity.item.Item +import world.gregs.voidps.engine.entity.obj.GameObjects +import world.gregs.voidps.engine.inv.add +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.type.setRandom +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class MagicBoxTest : WorldTest() { + @Test + fun `Catch an imp`() { + val player = createPlayer() + val start = player.tile + player.inventory.add("magic_box") + player.levels.set(Skill.Hunter, 99) + + player.itemOption("Activate", "magic_box") + tick(3) + assertTrue(GameObjects.at(start).any { it.id == "magic_box" }) + createNPC("imp", player.tile.addY(2)) + + tick(22) + + val trap = GameObjects.at(start).firstOrNull { it.id == "magic_box_caught" } + assertNotNull(trap) + + player.objectOption(trap, "Retrieve") + tick(3) + assertEquals(1, player.inventory.count("imp_in_a_box_2")) + assertEquals(0, player.inventory.count("magic_box")) + assertEquals(450.0, player.experience.get(Skill.Hunter)) + } + + @Test + fun `Can't lay without hunter level`() { + val player = createPlayer() + val start = player.tile + player.inventory.add("magic_box") + player.levels.set(Skill.Hunter, 70) + + player.itemOption("Activate", "magic_box") + tick(3) + assertFalse(GameObjects.at(start).any { it.id == "magic_box" }) + assertEquals(1, player.inventory.count("magic_box")) + } + + @Test + fun `Fail to catch`() { + setRandom(object : FakeRandom() { + override fun nextInt(until: Int) = if (until == 4) 0 else until - 1 + }) + val player = createPlayer() + val start = player.tile + player.inventory.add("magic_box") + player.levels.set(Skill.Hunter, 71) + + player.itemOption("Activate", "magic_box") + tick(3) + assertTrue(GameObjects.at(start).any { it.id == "magic_box" }) + createNPC("imp", player.tile.addY(2)) + + tick(22) + + val trap = GameObjects.at(start).firstOrNull { it.id == "magic_box_fail" } + assertNotNull(trap) + + player.objectOption(trap, "Deactivate") + tick(3) + assertEquals(1, player.inventory.count("magic_box")) + assertEquals(0.0, player.experience.get(Skill.Hunter)) + } + + @Test + fun `Imp in a box banks items with two charges`() { + val player = createPlayer() + player.inventory.add("imp_in_a_box_2") + player.inventory.add("bones", 2) + + player.itemOnItem("bones", "imp_in_a_box_2") + tick(2) + assertEquals(1, player.inventory.count("imp_in_a_box_1")) + assertEquals(0, player.inventory.count("imp_in_a_box_2")) + assertEquals(1, player.inventory.count("bones")) + assertEquals(1, player.bank.count("bones")) + + player.itemOnItem("bones", "imp_in_a_box_1") + tick(2) + assertEquals(0, player.inventory.count("imp_in_a_box_1")) + assertEquals(1, player.inventory.count("magic_box")) + assertEquals(0, player.inventory.count("bones")) + assertEquals(2, player.bank.count("bones")) + } + + @Test + fun `Bank option opens the imp release negotiation form`() { + val player = createPlayer() + player.inventory.add("imp_in_a_box_2") + player.inventory.add("bones", 2) + + player.itemOption("Bank", "imp_in_a_box_2") + tick(2) + assertTrue(player.hasOpen("imp_box")) + + player.interfaceOption("imp_box", "inventory", "Deposit", item = Item("bones"), slot = player.inventory.indexOf("bones")) + tick(2) + assertEquals(1, player.bank.count("bones")) + assertEquals(1, player.inventory.count("imp_in_a_box_1")) + assertTrue(player.hasOpen("imp_box")) + + player.interfaceOption("imp_box", "inventory", "Deposit", item = Item("bones"), slot = player.inventory.indexOf("bones")) + tick(2) + assertEquals(2, player.bank.count("bones")) + assertEquals(1, player.inventory.count("magic_box")) + assertFalse(player.hasOpen("imp_box")) + } + + @Test + fun `Imp box can't be deposited through the form`() { + val player = createPlayer() + player.inventory.add("imp_in_a_box_2") + player.inventory.add("imp_in_a_box_1") + + player.itemOption("Bank", "imp_in_a_box_2") + tick(2) + assertTrue(player.hasOpen("imp_box")) + + player.interfaceOption("imp_box", "inventory", "Deposit", item = Item("imp_in_a_box_1"), slot = player.inventory.indexOf("imp_in_a_box_1")) + tick(2) + assertEquals(0, player.bank.count("imp_in_a_box_1")) + assertEquals(1, player.inventory.count("imp_in_a_box_2")) + } + + @Test + fun `Imp refuses to bank another imp box`() { + val player = createPlayer() + player.inventory.add("imp_in_a_box_2") + player.inventory.add("imp_in_a_box_1") + + player.itemOnItem("imp_in_a_box_1", "imp_in_a_box_2") + tick(2) + assertEquals(1, player.inventory.count("imp_in_a_box_2")) + assertEquals(1, player.inventory.count("imp_in_a_box_1")) + assertEquals(0, player.bank.count("imp_in_a_box_1")) + } +} diff --git a/network/src/main/kotlin/world/gregs/voidps/network/login/protocol/encode/HintEncoder.kt b/network/src/main/kotlin/world/gregs/voidps/network/login/protocol/encode/HintEncoder.kt index f3382c997e..d897a80014 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/login/protocol/encode/HintEncoder.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/login/protocol/encode/HintEncoder.kt @@ -44,20 +44,22 @@ fun Client.arrowHint( radius: Int = 0, model: Int = 65535, ) = send(Protocol.HINT_ARROW) { + // The client always reads a fixed 12 byte body, so every form must be padded to it writeByte((arrowIndex shl 5) or type) writeByte(sprite) - if (sprite >= 0) { - if (type == 1 || type == 10) { - writeShort(entityIndex) - writeInt(0) - writeShort(0) - } else if (type in 2..6) { - writeByte(level) // level - writeShort(x) // x - writeShort(y) // y - writeByte(z) // z? - writeShort(radius) - } - writeShort(model) + if (type == 1 || type == 10) { + writeShort(entityIndex) + writeInt(0) + writeShort(0) + } else if (type in 2..6) { + writeByte(level) // level + writeShort(x) // x + writeShort(y) // y + writeByte(z) // z? + writeShort(radius) + } else { + writeInt(0) + writeInt(0) } + writeShort(model) } \ No newline at end of file