diff --git a/data/area/fremennik_province/rellekka/hunter_area/rellekka_hunter_area.npcs.toml b/data/area/fremennik_province/rellekka/hunter_area/rellekka_hunter_area.npcs.toml index 13f8a93f59..ed06088936 100644 --- a/data/area/fremennik_province/rellekka/hunter_area/rellekka_hunter_area.npcs.toml +++ b/data/area/fremennik_province/rellekka/hunter_area/rellekka_hunter_area.npcs.toml @@ -14,6 +14,8 @@ examine = "A sapphire glacialis. It doesn't look as pretentious as its name soun [sabre_toothed_kyatt] id = 5103 +combat_def = "panther" +respawn_delay = 50 examine = "It's just like a big, white, furry, deadly can opener." [hunting_expert_rellekka] 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 c61c73e54b..c5b27b7ad9 100644 --- a/data/area/kandarin/feldip_hills/feldip_hills.npc-spawns.toml +++ b/data/area/kandarin/feldip_hills/feldip_hills.npc-spawns.toml @@ -174,11 +174,11 @@ spawns = [ { 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 = "spined_larupia_feldip_hills", x = 2544, y = 2910, members = true }, - { id = "spined_larupia_feldip_hills", x = 2550, y = 2904, members = true }, - { id = "spined_larupia_feldip_hills", x = 2556, y = 2895, members = true }, - { id = "spined_larupia_feldip_hills", x = 2563, y = 2888, members = true }, - { id = "spined_larupia_feldip_hills", x = 2573, y = 2883, members = true }, + { 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 }, + { id = "spined_larupia", x = 2563, y = 2888, members = true }, + { id = "spined_larupia", x = 2573, y = 2883, members = true }, { id = "hunting_expert_feldip_hills", x = 2525, y = 2917, members = true }, { id = "giant_spider_mid", x = 2517, y = 2976, members = true }, { id = "ogress_champion", x = 2451, y = 2857 }, diff --git a/data/area/kandarin/feldip_hills/feldip_hills.npcs.toml b/data/area/kandarin/feldip_hills/feldip_hills.npcs.toml index d02d610190..e4fbee651b 100644 --- a/data/area/kandarin/feldip_hills/feldip_hills.npcs.toml +++ b/data/area/kandarin/feldip_hills/feldip_hills.npcs.toml @@ -47,8 +47,11 @@ examine = "A black warlock. The air seems to distort wherever it passes." id = 5088 examine = "It uses its tail to hunt and skewer fish." -[spined_larupia_feldip_hills] +[spined_larupia] id = 5104 +combat_def = "panther" +respawn_delay = 50 +max_hit_slash = 50 examine = "If you tried to ride that, you'd just impale yourself!" [hunting_expert_feldip_hills] diff --git a/data/area/karamja/karamja.npcs.toml b/data/area/karamja/karamja.npcs.toml index 52e1b7525e..b8c708aad3 100644 --- a/data/area/karamja/karamja.npcs.toml +++ b/data/area/karamja/karamja.npcs.toml @@ -72,6 +72,8 @@ examine = "A warrior from Shilo Village." [horned_graahk] id = 5105 +combat_def = "panther" +respawn_delay = 50 examine = "Get in a graahk's way and you're going to know about it...however briefly." [captain_shanks] diff --git a/data/area/morytania/canifis/canifis.npc-spawns.toml b/data/area/morytania/canifis/canifis.npc-spawns.toml index 4b019b137a..953b03a0ff 100644 --- a/data/area/morytania/canifis/canifis.npc-spawns.toml +++ b/data/area/morytania/canifis/canifis.npc-spawns.toml @@ -31,7 +31,6 @@ spawns = [ { id = "zoja", x = 3505, y = 3491 }, { id = "liliya", x = 3479, y = 3498, level = 1 }, { id = "achtryn", x = 3511, y = 3509, members = true }, - { id = "mazchna", x = 3512, y = 3509, members = true }, # North of Mort Myre Swamp { id = "ghoul", x = 3423, y = 3461, members = true }, { id = "ghoul", x = 3426, y = 3465, members = true }, diff --git a/data/skill/hunter/hunter.anims.toml b/data/skill/hunter/hunter.anims.toml index dc143655b4..01d91822df 100644 --- a/data/skill/hunter/hunter.anims.toml +++ b/data/skill/hunter/hunter.anims.toml @@ -58,3 +58,19 @@ id = 3067 [lay_net_trap] id = 5215 + +[tease] +id = 5236 + +[scythe_slash] +id = 2524 + +[big_cat_attack] +id = 5229 + +[big_cat_defend] +id = 5227 + +[big_cat_death] +id = 5234 +ticks = 2 diff --git a/data/skill/hunter/hunter.combat.toml b/data/skill/hunter/hunter.combat.toml new file mode 100644 index 0000000000..8eda932576 --- /dev/null +++ b/data/skill/hunter/hunter.combat.toml @@ -0,0 +1,13 @@ +[panther] +attack_speed = 4 +retreat_range = 20 +defend_anim = "big_cat_defend" +defend_sound = "dog_defend" +death_anim = "big_cat_death" +death_sound = "big_cat_death" + +[panther.melee] +range = 1 +anim = "big_cat_attack" +target_sound = "big_cat_attack" +target_hit = { offense = "slash", max = 70 } diff --git a/data/skill/hunter/hunter.objs.toml b/data/skill/hunter/hunter.objs.toml index 04fa1bce0c..fe1f3e4d73 100644 --- a/data/skill/hunter/hunter.objs.toml +++ b/data/skill/hunter/hunter.objs.toml @@ -144,55 +144,55 @@ id = 28906 [boulder_trap_barb_tailed_kebbit] id = 19207 -[pitfall_0] +[pitfall_sabre_toothed_kyatt_0] id = 19253 -[pitfall_1] +[pitfall_sabre_toothed_kyatt_1] id = 19254 -[pitfall_2] +[pitfall_sabre_toothed_kyatt_2] id = 19255 -[pitfall_3] +[pitfall_sabre_toothed_kyatt_3] id = 19256 -[pitfall_4] +[pitfall_sabre_toothed_kyatt_4] id = 19257 -[pitfall_5] +[pitfall_sabre_toothed_kyatt_5] id = 19258 -[pitfall_6] +[pitfall_spined_larupia_6] id = 19259 -[pitfall_7] +[pitfall_spined_larupia_7] id = 19260 -[pitfall_8] +[pitfall_spined_larupia_8] id = 19261 -[pitfall_9] +[pitfall_spined_larupia_9] id = 19262 -[pitfall_10] +[pitfall_spined_larupia_10] id = 19263 -[pitfall_11] +[pitfall_horned_graahk_11] id = 19264 -[pitfall_12] +[pitfall_horned_graahk_12] id = 19265 -[pitfall_13] +[pitfall_horned_graahk_13] id = 19266 -[pitfall_14] +[pitfall_horned_graahk_14] id = 19267 -[pitfall_15] +[pitfall_horned_graahk_15] id = 19268 -[pitfall_16] +[pitfall_spined_larupia_16] id = 43614 [pitfall] @@ -204,8 +204,20 @@ id = 19228 [pitfall_collapsed] id = 19229 -[pitfall_caught] +[pitfall_caught_spined_larupia] +id = 19232 + +[pitfall_inverse_spined_larupia] +id = 19235 + +[pitfall_caught_horned_graahk] id = 19231 -[pitfall_inverse] +[pitfall_inverse_horned_graahk] id = 19234 + +[pitfall_caught_sabre_toothed_kyatt] +id = 19233 + +[pitfall_inverse_sabre_toothed_kyatt] +id = 19236 diff --git a/data/skill/hunter/hunter.sounds.toml b/data/skill/hunter/hunter.sounds.toml index c23343a1eb..2c426504f0 100644 --- a/data/skill/hunter/hunter.sounds.toml +++ b/data/skill/hunter/hunter.sounds.toml @@ -11,7 +11,7 @@ id = 2632 id = 2654 [pitfall_collapse] -id = 2638 # radius 5 +id = 2638 [panther_death] id = 667 # radius 5 @@ -33,3 +33,15 @@ id = 739 [twitch_net] id = 2652 + +[place_branches] +id = 2639 + +[take_branches] +id = 2649 + +[big_cat_attack] +id = 666 + +[big_cat_death] +id = 667 diff --git a/data/skill/hunter/hunter.tables.toml b/data/skill/hunter/hunter.tables.toml index 46b2754cde..3fa68fbe8b 100644 --- a/data/skill/hunter/hunter.tables.toml +++ b/data/skill/hunter/hunter.tables.toml @@ -1,141 +1,84 @@ -[trap_npcs] -row = "npc" -trap = "string" - -[.hunting_ojibway_trap_npc] -trap = "bird_snare" - -[.hunting_ojibway_trap_npc_off] -trap = "bird_snare" - -[.hunting_sapling_trap_npc] -trap = "swamp_net" - -[.hunting_sapling_trap_npc_off] -trap = "swamp_net" - [traps] -trap = "obj" # the active (set) object -fail = "obj" # the failed/collapsed object (optional) -setup_anim = "anim" -setup_sound = "string" -take_down_anim = "anim" level = "int" level_default = 1 requires = "list" items = "list" max = "int" max_default = 5 -step_away = "boolean" -step_away_default = true -item_message = "string" -collapse_ticks = "int" -collapse_message = "string" npc = "npc" bait = "item" [.bird_snare] -trap = "bird_snare" -fail = "bird_snare_fail" -setup_anim = "lay_trap" -setup_sound = "set_noose" -take_down_anim = "take_trap" items = ["bird_snare"] -collapse_ticks = 100 -collapse_message = "The bird snare that you laid has fallen over." npc = "hunting_ojibway_trap_npc" [.rabbit_snare] -trap = "rabbit_snare" -fail = "rabbit_snare_fail" -setup_anim = "lay_trap_small" -take_down_anim = "lay_trap_small" +#trap = "rabbit_snare" +#fail = "rabbit_snare_fail" +#setup_anim = "lay_trap_small" +#take_down_anim = "lay_trap_small" items = ["rabbit_snare"] -collapse_ticks = 104 -collapse_message = "The snare that you placed has fallen over." +#collapse_ticks = 104 +#collapse_message = "The snare that you placed has fallen over." npc = "hunting_snare_trap_npc" [.box_trap] level = 27 -trap = "box_trap" -fail = "box_trap_fail" -setup_anim = "lay_trap" -take_down_anim = "lay_trap" +#trap = "box_trap" +#fail = "box_trap_fail" +#setup_anim = "lay_trap" +#take_down_anim = "lay_trap" items = ["box_trap"] 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"] +#trap = "boulder_trap_setup" +#fail = "boulder_trap_fail" +#setup_anim = "lay_trap" +#take_down_anim = "lay_trap" +#requires = ["knife"] items = ["logs"] max = 1 -step_away = false npc = "hunting_deadfall_trap_npc" [.swamp_net] level = 29 -trap = "swamp_net_setup" bait = "guam_tar" -fail = "swamp_net_failing" -setup_anim = "lay_net_trap" -setup_sound = "lay_net_trap" -take_down_anim = "take_trap" items = ["rope", "small_fishing_net"] -collapse_ticks = 102 -collapse_message = "The net trap that you set has collapsed." npc = "hunting_sapling_trap_npc" [.orange_net] level = 47 -trap = "orange_net_setup" bait = "marrentill_tar" -setup_anim = "lay_net_trap" -setup_sound = "lay_net_trap" -take_down_anim = "take_trap" items = ["rope", "small_fishing_net"] -collapse_ticks = 102 -collapse_message = "The net trap that you set has collapsed." npc = "hunting_sapling_trap_npc" [.red_net] level = 59 -trap = "red_net_setup" bait = "tarromin_tar" -setup_anim = "lay_net_trap" -setup_sound = "lay_net_trap" -take_down_anim = "take_trap" items = ["rope", "small_fishing_net"] -collapse_ticks = 102 -collapse_message = "The net trap that you set has collapsed." npc = "hunting_sapling_trap_npc" [.black_net] level = 67 -trap = "black_net_setup" bait = "harralander_tar" -setup_anim = "lay_net_trap" -setup_sound = "lay_net_trap" -take_down_anim = "take_trap" items = ["rope", "small_fishing_net"] -collapse_ticks = 102 -collapse_message = "The net trap that you set has collapsed." npc = "hunting_sapling_trap_npc" [.pitfall] level = 31 -trap = "pitfall_0" -setup_anim = "lay_trap_small" -take_down_anim = "lay_trap_small" -requires = ["knife"] items = ["logs"] -max = 1 -item_message = "You need some logs and a knife to set a pitfall trap." -collapse_ticks = 300 -collapse_message = "The pitfall trap that you constructed has collapsed." + +[.pitfall_spined_larupia] +level = 31 + +[.pitfall_horned_graahk] +level = 41 + +[.pitfall_sabre_toothed_kyatt] +level = 51 + # --------------------------------------------------------------------------- # Creatures — keyed by the *caught* object id (the triggered/full-trap object) @@ -145,8 +88,6 @@ trap = "string" # which trap type is used caught_obj = "obj" # object id when creature is caught inside level = "int" # minimum hunter level to lure xp = "int" # xp * 0.1 (stored as tenths for int precision) -catch_anim = "anim" # animation played by creature as it enters trap -fail_anim = "anim" # animation played by creature on escape (optional) loot = "list" # items awarded on catch chance = "range" @@ -155,8 +96,6 @@ trap = "bird_snare" caught_obj = "snare_crimson_swift" level = 1 xp = 340 -catch_anim = "bird_catch" -fail_anim = "bird_fail" loot = ["red_feather", "raw_bird_meat", "bones"] chance = [100, 420] @@ -165,8 +104,6 @@ trap = "bird_snare" caught_obj = "snare_golden_warbler" level = 5 xp = 480 -catch_anim = "bird_catch" -fail_anim = "bird_fail" loot = ["yellow_feather", "raw_bird_meat", "bones"] chance = [92, 400] @@ -175,8 +112,6 @@ trap = "bird_snare" caught_obj = "snare_copper_longtail" level = 9 xp = 610 -catch_anim = "bird_catch" -fail_anim = "bird_fail" loot = ["orange_feather", "raw_bird_meat", "bones"] chance = [85, 390] @@ -185,8 +120,6 @@ trap = "bird_snare" caught_obj = "snare_cerulean_twitch" level = 11 xp = 647 -catch_anim = "bird_catch" -fail_anim = "bird_fail" loot = ["blue_feather", "raw_bird_meat", "bones"] chance = [82, 380] @@ -195,8 +128,6 @@ trap = "bird_snare" caught_obj = "snare_tropical_wagtail" level = 19 xp = 952 -catch_anim = "bird_catch" -fail_anim = "bird_fail" loot = ["stripy_feather", "raw_bird_meat", "bones"] chance = [75, 370] @@ -205,8 +136,6 @@ trap = "bird_snare" caught_obj = "snare_wimpy_bird" level = 39 xp = 1670 -catch_anim = "bird_catch" -fail_anim = "bird_fail" loot = ["wimpy_feather", "raw_bird_meat", "bones"] chance = [65, 350] @@ -215,7 +144,7 @@ trap = "box_trap" caught_obj = "box_trap_grey_chinchompa" level = 53 xp = 1984 -catch_anim = "chinchompa_catch" +#catch_anim = "chinchompa_catch" loot = ["box_trap", "chinchompa"] [.red_chinchompa] @@ -223,7 +152,7 @@ trap = "box_trap" caught_obj = "box_trap_red_chinchompa" level = 63 xp = 2650 -catch_anim = "chinchompa_catch" +#catch_anim = "chinchompa_catch" loot = ["box_trap", "red_chinchompa"] [.barb_tailed_kebbit] @@ -231,15 +160,14 @@ trap = "boulder_trap" caught_obj = "boulder_trap_barb_tailed_kebbit" level = 23 xp = 1680 -catch_anim = "kebbit_catch" -fail_anim = "kebbit_fail" +#catch_anim = "kebbit_catch" +#fail_anim = "kebbit_fail" loot = ["bones", "barb_tail_harpoon"] [.swamp_lizard] trap = "swamp_net" level = 29 xp = 1520 -catch_anim = "net_catch" loot = ["green_salamander"] chance = [52, 360] @@ -247,7 +175,6 @@ chance = [52, 360] trap = "orange_net" level = 47 xp = 2240 -catch_anim = "net_catch" loot = ["orange_salamander"] chance = [16, 288] @@ -255,7 +182,6 @@ chance = [16, 288] trap = "red_net" level = 59 xp = 2720 -catch_anim = "net_catch" loot = ["red_salamander"] chance = [0, 240] @@ -263,7 +189,6 @@ chance = [0, 240] trap = "black_net" level = 67 xp = 3040 -catch_anim = "net_catch" loot = ["black_salamander"] chance = [0, 212] @@ -272,7 +197,7 @@ trap = "box_trap" caught_obj = "box_trap_pawya" level = 66 xp = 4000 -catch_anim = "chinchompa_catch" +#catch_anim = "chinchompa_catch" loot = ["bones", "raw_pawya_meat"] [.grenwall] @@ -280,7 +205,7 @@ trap = "box_trap" caught_obj = "box_trap_grenwall" level = 77 xp = 11000 -catch_anim = "chinchompa_catch" +#catch_anim = "chinchompa_catch" 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"] [.rabbit] @@ -288,6 +213,21 @@ trap = "rabbit_snare" caught_obj = "rabbit_snare_caught" level = 27 xp = 1440 -catch_anim = "lay_trap_small" -fail_anim = "lay_trap_small" +#catch_anim = "lay_trap_small" +#fail_anim = "lay_trap_small" loot = ["rabbit_foot", "raw_rabbit", "bones"] + +[.spined_larupia] +level = 31 +xp = 1800 +loot = ["big_bones", "tatty_larupia_fur"] + +[.horned_graahk] +level = 41 +xp = 2400 +loot = ["big_bones", "tatty_graahk_fur"] + +[.sabre_toothed_kyatt] +level = 55 +xp = 3000 +loot = ["big_bones", "tatty_kyatt_fur"] diff --git a/data/skill/hunter/hunter.varbits.toml b/data/skill/hunter/hunter.varbits.toml index 7f76489be5..12ff313409 100644 --- a/data/skill/hunter/hunter.varbits.toml +++ b/data/skill/hunter/hunter.varbits.toml @@ -1,84 +1,84 @@ -[pitfall_0] +[pitfall_sabre_toothed_kyatt_0] id = 2958 format = "list" values = ["empty", "spiked", "collapsed", "caught", "inverse"] -[pitfall_1] +[pitfall_sabre_toothed_kyatt_1] id = 2959 format = "list" values = ["empty", "spiked", "collapsed", "caught", "inverse"] -[pitfall_2] +[pitfall_sabre_toothed_kyatt_2] id = 2960 format = "list" values = ["empty", "spiked", "collapsed", "caught", "inverse"] -[pitfall_3] +[pitfall_sabre_toothed_kyatt_3] id = 2961 format = "list" values = ["empty", "spiked", "collapsed", "caught", "inverse"] -[pitfall_4] +[pitfall_sabre_toothed_kyatt_4] id = 2962 format = "list" values = ["empty", "spiked", "collapsed", "caught", "inverse"] -[pitfall_5] +[pitfall_sabre_toothed_kyatt_5] id = 2963 format = "list" values = ["empty", "spiked", "collapsed", "caught", "inverse"] -[pitfall_6] +[pitfall_spined_larupia_6] id = 2964 format = "list" values = ["empty", "spiked", "collapsed", "caught", "inverse"] -[pitfall_7] +[pitfall_spined_larupia_7] id = 2965 format = "list" values = ["empty", "spiked", "collapsed", "caught", "inverse"] -[pitfall_8] +[pitfall_spined_larupia_8] id = 2966 format = "list" values = ["empty", "spiked", "collapsed", "caught", "inverse"] -[pitfall_9] +[pitfall_spined_larupia_9] id = 2967 format = "list" values = ["empty", "spiked", "collapsed", "caught", "inverse"] -[pitfall_10] +[pitfall_spined_larupia_10] id = 2968 format = "list" values = ["empty", "spiked", "collapsed", "caught", "inverse"] -[pitfall_11] +[pitfall_horned_graahk_11] id = 2969 format = "list" values = ["empty", "spiked", "collapsed", "caught", "inverse"] -[pitfall_12] +[pitfall_horned_graahk_12] id = 2970 format = "list" values = ["empty", "spiked", "collapsed", "caught", "inverse"] -[pitfall_13] +[pitfall_horned_graahk_13] id = 2971 format = "list" values = ["empty", "spiked", "collapsed", "caught", "inverse"] -[pitfall_14] +[pitfall_horned_graahk_14] id = 2972 format = "list" values = ["empty", "spiked", "collapsed", "caught", "inverse"] -[pitfall_15] +[pitfall_horned_graahk_15] id = 2973 format = "list" values = ["empty", "spiked", "collapsed", "caught", "inverse"] -[pitfall_16] +[pitfall_spined_larupia_16] id = 4189 format = "list" values = ["empty", "spiked", "collapsed", "caught", "inverse"] diff --git a/data/skill/hunter/hunting.gfx.toml b/data/skill/hunter/hunting.gfx.toml index 84dd94a867..bb57527f82 100644 --- a/data/skill/hunter/hunting.gfx.toml +++ b/data/skill/hunter/hunting.gfx.toml @@ -40,5 +40,14 @@ id = 951 [catch_fail] id = 946 -[pitfall_collapse] +[pitfall_collapse_0] +id = 933 + +[pitfall_collapse_1] +id = 934 + +[pitfall_collapse_2] id = 935 + +[pitfall_collapse_3] +id = 936 diff --git a/game/src/main/kotlin/content/area/kandarin/barbarian_outpost/Gunnjorn.kt b/game/src/main/kotlin/content/area/kandarin/barbarian_outpost/Gunnjorn.kt index 2d212a20b6..e4f6a31977 100644 --- a/game/src/main/kotlin/content/area/kandarin/barbarian_outpost/Gunnjorn.kt +++ b/game/src/main/kotlin/content/area/kandarin/barbarian_outpost/Gunnjorn.kt @@ -41,7 +41,7 @@ class Gunnjorn : Script { } else { inventoryFull() // TODO correct message } - } else if (get("gnome_course_advanced_laps", 0) >= 250) { + } else if (get("barbarian_course_advanced_laps", 0) >= 250) { npc("Sure, and congratulations, Player! That took dedication and great dexterity to complete that many laps.") npc("As promised, I'll give you an item you may find useful - an Agile top. You'll find yourself lighter than usual while wearing it.") npc("We barbarians are tough folks, as you know, so it'll even keep you safe if you get drawn into combat.") diff --git a/game/src/main/kotlin/content/entity/combat/Combat.kt b/game/src/main/kotlin/content/entity/combat/Combat.kt index 92b9b25c44..aea58c79d4 100644 --- a/game/src/main/kotlin/content/entity/combat/Combat.kt +++ b/game/src/main/kotlin/content/entity/combat/Combat.kt @@ -119,6 +119,9 @@ class Combat(val combatDefinitions: CombatDefinitions) : } fun retaliate(character: Character, source: Character) { + if (source is NPC && source.def.options.contains("Tease")) { + return + } if (character.dead || character.levels.get(Skill.Constitution) <= 0 || !retaliates(character)) { return } diff --git a/game/src/main/kotlin/content/entity/combat/Target.kt b/game/src/main/kotlin/content/entity/combat/Target.kt index 8f88fab7c5..6c2812b174 100644 --- a/game/src/main/kotlin/content/entity/combat/Target.kt +++ b/game/src/main/kotlin/content/entity/combat/Target.kt @@ -180,6 +180,7 @@ object Target { is NPC if target.def.contains("damage_cap") -> damage.coerceAtMost(target.def["damage_cap"]) is NPC if target.def.contains("immune_death") -> damage.coerceAtMost(target.levels.get(Skill.Constitution) - 10) is NPC if target.id.endsWith("_impling") -> 0 + is NPC if (target.id == "spined_larupia" || target.id == "horned_graahk" || target.id == "sabre_toothed_kyatt") -> 0 else -> damage } } diff --git a/game/src/main/kotlin/content/entity/player/bank/BankWithdraw.kt b/game/src/main/kotlin/content/entity/player/bank/BankWithdraw.kt index e2c59a9b5e..506ebe4c5a 100644 --- a/game/src/main/kotlin/content/entity/player/bank/BankWithdraw.kt +++ b/game/src/main/kotlin/content/entity/player/bank/BankWithdraw.kt @@ -65,7 +65,7 @@ class BankWithdraw : Script { } when (player.bank.transaction.error) { TransactionError.None -> { - if (moved < amount) { + if (moved < amount && !removed) { player.inventoryFull("to withdraw that many") } if (removed) { diff --git a/game/src/main/kotlin/content/skill/hunter/Hunter.kt b/game/src/main/kotlin/content/skill/hunter/Hunter.kt deleted file mode 100644 index 2804a47929..0000000000 --- a/game/src/main/kotlin/content/skill/hunter/Hunter.kt +++ /dev/null @@ -1,206 +0,0 @@ -package content.skill.hunter - -import content.entity.effect.transform -import content.entity.player.inv.item.drop -import content.quest.questCompleted -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.an -import world.gregs.voidps.engine.client.ui.chat.plural -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.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.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.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.carriesItem -import world.gregs.voidps.engine.inv.inventory -import world.gregs.voidps.engine.inv.remove -import world.gregs.voidps.type.Tile - -/** - * Hunter traps have an invisible npc under them which has a hunt mode which - * selects nearby targets to walk to it and attempt to be caught. - */ -class Hunter : Script { - - init { - playerDespawn { - val npcs = NPCs.filter { it["owner", ""] == accountName } - for (npc in npcs) { - NPCs.remove(npc) - } - } - - // TODO ferret, chinchompa, rabbits - - npcSpawn("hunting_*_trap_npc") { - transform("${id}_off") - revert(15) - } - - npcVariableSet("transform_id", "hunting_*_trap_npc") { _, _, to -> - if (to == null) { - despawn(100) - } - } - } - - private fun Player.collapse(npc: NPC, drop: Boolean) { - dec("trap_count") - val tile = npc.tile // TODO tile won't be correct for object traps - val obj = GameObjects.getLayer(tile, ObjectLayer.GROUND) ?: return - obj.remove() - if (drop) { - val id = when (obj.id) { - "snare_crimson_swift", "snare_golden_warbler", "bird_snare_fail" -> "bird_snare" - else -> obj.id - } - dropTrapItems(id, tile) // TODO does baited drop bait? - } - } - - private fun Player.dropTrapItems(id: String, tile: Tile) { - val items = Tables.itemList("traps.$id.items") - for (item in items) { - if (item == "logs") { - continue - } - drop(tile, item) - } - } - - private suspend fun Player.layTrap(trapId: String, obj: GameObject?, floorItem: FloorItem?) { - if (trapId == "box_trap" && !questCompleted("eagles_peak")) { - message("You need to learn how to set a box trap in the Eagle's Peak Quest.") - return - } - var obj = obj - val trap = Rows.getOrNull("traps.$trapId") ?: 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 - } - val message = trap.stringOrNull("item_message") - val requires = trap.itemList("requires") - for (item in requires) { - if (!carriesItem(item)) { - message(message ?: "You need${item.an()} ${item.toLowerSpaceCase()} to lay this trap.") - return - } - } - val items = trap.itemList("items") - for (item in items) { - if (!carriesItem(item) && floorItem != null && floorItem.id != item) { - message(message ?: "You need${item.an()} ${item.toLowerSpaceCase()} to lay this trap.") - return - } - } - arriveDelay() - message("You begin setting up ${if (max == 1) "the" else "a"} trap.", ChatType.Filter) - anim(trap.anim("setup_anim")) - sound(trap.string("setup_sound")) - delay(3) - for (item in items) { - if (floorItem != null && item == floorItem.id) { - FloorItems.remove(floorItem) - continue - } - inventory.remove(item) - } - inc("trap_count") - val trapId = trap.obj("trap") - if (trapId == "pitfall_0" && obj != null) { - set(obj.id, "spiked") - } else if (obj != null) { - obj.replace(trapId) - } else { - NPCs.add(trap.npc("npc"), tile, ticks = 100, owner = this) // TODO check times - obj = GameObjects.add(trapId, tile, ObjectShape.CENTRE_PIECE_STRAIGHT, 0, ticks = 50 * 60) - } - if (trap.bool("step_away")) { - stepAway(obj) - } - } - - private suspend fun Player.dismantleTrap(trapId: String, target: GameObject, tile: Tile) { - val id = Tables.npc("traps.$trapId.npc") - val npc = NPCs.findOrNull(tile, id) ?: return - if (npc["owner", ""] != accountName) { - message("This is not your trap.") - return - } - val trap = Rows.get("traps.$trapId") - val items = trap.itemList("items") - if (inventory.spaces < items.size) { - val slots = items.size - inventory.spaces - message("You don't have enough inventory space. You need $slots more free ${"slot".plural(slots)}.") - return - } - anim(trap.anim("take_down_anim")) - sound("trap_dismantle", delay = 25) - delay(2) - removeTrap(target, npc) - for (item in items) { - inventory.add(item) - } - message("You dismantle the trap.", ChatType.Filter) - } - - private suspend fun Player.collectCatch(creatureId: String, target: GameObject) { - val id = Tables.npc("traps.${target.id.removeSuffix("_caught")}.npc") - val npc = NPCs.findOrNull(target.tile, id) ?: return - if (npc["owner", ""] != accountName) { - message("This is not your trap.") - return - } - val creature = Rows.get("creatures.$creatureId") - val loot = creature.itemList("loot") - 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 - } - message("You dismantle the trap.", ChatType.Filter) - val trap = Rows.get("traps.${creature.string("trap")}") - anim(trap.anim("take_down_anim")) - sound("trap_dismantle", delay = 25) - delay(2) - removeTrap(target, npc) - for (item in loot) { - inventory.add(item) - } - exp(Skill.Hunter, creature.int("xp") / 10.0) - message("You've caught a ${creatureId.toLowerSpaceCase()}.", ChatType.Filter) - } - - private fun Player.removeTrap(target: GameObject, npc: NPC) { - dec("trap_count") - NPCs.remove(npc) - if (target.id.startsWith("pitfall")) { - set(target.id, "empty") - return - } - GameObjects.remove(target) - } -} diff --git a/game/src/main/kotlin/content/skill/hunter/Pitfall.kt b/game/src/main/kotlin/content/skill/hunter/Pitfall.kt new file mode 100644 index 0000000000..9ebd364fa1 --- /dev/null +++ b/game/src/main/kotlin/content/skill/hunter/Pitfall.kt @@ -0,0 +1,189 @@ +package content.skill.hunter + +import content.entity.combat.attacker +import content.entity.combat.dead +import content.entity.combat.target +import content.entity.combat.underAttack +import content.skill.melee.weapon.attackSpeed +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.variable.start +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.mode.combat.CombatApi +import world.gregs.voidps.engine.entity.character.mode.combat.CombatMovement +import world.gregs.voidps.engine.entity.character.npc.NPC +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.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.remove +import world.gregs.voidps.engine.timer.Timer +import world.gregs.voidps.type.Direction +import world.gregs.voidps.type.Tile +import world.gregs.voidps.type.random + +class Pitfall : Script { + init { + npcOperate("Tease") { + val target = it.target + val level = Tables.int("creatures.${target.id}.level") + if (!has(Skill.Hunter, level)) { + message("You need a hunter level of at least $level to catch a ${target.def.name.lowercase()}.") + return@npcOperate + } + if (!inventory.contains("teasing_stick")) { + message("I don't want to lose a finger by poking it. Maybe I should get a professional teasing implement instead.") + return@npcOperate + } + anim("tease") + sound("scythe_slash") + if (!target.dead && !target.underAttack) { + attacker = target + this.target = target + target.mode = CombatMovement(target, this) + target.target = this + target.attacker = this + val delay = target.attackSpeed / 2 + target.start("action_delay", delay) + target.start("under_attack", delay + 8) + } + } + + objectOperate("Trap", "pitfall") { + layTrap(it.target) + } + + objectOperate("Jump", "pitfall_spiked") { (target) -> + val dir = jumpDirection(target, this.tile) + if (dir == Direction.EAST || dir == Direction.WEST) { + walkToDelay(tile.copy(y = tile.y.coerceIn(target.tile.y..target.tile.y + 1))) + } else if (dir == Direction.NORTH || dir == Direction.SOUTH) { + walkToDelay(tile.copy(x = tile.x.coerceIn(target.tile.x..target.tile.x + 1))) + } + anim("agility_pyramid_gap_jump") + areaSound("hunting_jump", target.tile) + exactMoveDelay(tile.add(dir).add(dir).add(dir), delay = 53, direction = dir) + val attacker = attacker + if (attacker !is NPC || attacker.id != "spined_larupia" && attacker.id != "horned_graahk" && attacker.id != "sabre_toothed_kyatt") { + return@objectOperate + } + val offset = when (dir) { + Direction.WEST -> target.tile.addX(2) + Direction.EAST -> target.tile.addX(-2) + Direction.NORTH -> target.tile.addY(-2) + Direction.SOUTH -> target.tile.addY(2) + else -> return@objectOperate + } + if (attacker.tile.within(offset, 3)) { + attacker.walkToDelay(offset) + } else { + delay(2) + } + if (attacker.tile != offset) { + return@objectOperate + } + attacker.walkOverDelay(attacker.tile.add(dir).add(dir)) + attacker.levels.set(Skill.Constitution, 0) + attacker.gfx("pitfall_collapse_${random.nextInt(4)}") + areaSound("pitfall_collapse", target.tile) + set(target.id, "collapsed") + delay(2) + val flip = when { + attacker.id == "horned_graahk" -> dir == Direction.SOUTH || dir == Direction.EAST + attacker.id == "sabre_toothed_kyatt" -> dir == Direction.SOUTH || dir == Direction.WEST + else -> dir == Direction.NORTH || dir == Direction.EAST + } + set(target.id, if (flip) "inverse" else "caught") + CombatApi.stop(attacker, this) + } + + objectOperate("Dismantle", "pitfall_*") { (target) -> + dismantleTrap(target) + } + + for (i in 0..16) { + timerStart("collapse_pitfall_$i") { 100 } // TODO check collapse ticks + timerTick("collapse_pitfall_$i") { + clear("pitfall_$i") + message("The pitfall trap that you constructed has collapsed.") + Timer.CANCEL + } + } + + objectOperate("Trap", "pitfall_*") { + // Temp to avoid #1059 + } + } + + private fun jumpDirection(target: GameObject, tile: Tile): Direction = if (target.rotation == 1 || target.rotation == 3) { + if (tile.x > target.tile.x) Direction.WEST else Direction.EAST + } else { + if (tile.y > target.tile.y) Direction.SOUTH else Direction.NORTH + } + + private suspend fun Player.layTrap(obj: GameObject) { + val trap = Rows.getOrNull("traps.${obj.id.substringBeforeLast("_")}") ?: return + val level = trap.int("level") + if (!has(Skill.Hunter, level, message = false)) { + message("You need a hunter level of at least $level to set a pitfall trap here.") + return + } + if (get(obj.id, "empty") != "empty") { + return + } + val max = Traps.max(levels.get(Skill.Hunter), 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 + } + if (!inventory.contains("knife") || !inventory.contains("logs")) { + message("You need some logs and a knife to set a pitfall trap.") + return + } + arriveDelay() + anim("lay_trap_small") + inventory.remove("logs") + delay(1) + sound("place_branches") + inc("trap_count") + set(obj.id, "spiked") + softTimers.start("collapse_pitfall_${obj.id.substringAfterLast("_")}") + } + + private suspend fun Player.dismantleTrap(target: GameObject) { + val state = get(target.id, "empty") + val creature = Rows.get("creatures.${target.id.removePrefix("pitfall_").substringBeforeLast("_")}") + val items = creature.itemList("loot") + val loot = state == "caught" || state == "inverse" + if (loot && inventory.spaces < items.size) { + val slots = items.size - inventory.spaces + message("You don't have enough inventory space. You need $slots more free ${"slot".plural(slots)}.") + return + } + message("You dismantle the trap.", ChatType.Filter) + anim("lay_trap_small") + delay(1) + sound("take_branches") + dec("trap_count") + set(target.id, "empty") + softTimers.clear("collapse_pitfall_${creature.rowId}") + if (loot) { + message("You've caught a ${creature.rowId.toLowerSpaceCase()}!", type = ChatType.Filter) + for (item in items) { + // TODO lerp chance of replacing tatty with full fur + inventory.add(item) + } + exp(Skill.Hunter, creature.int("xp") / 10.0) + } + } +} diff --git a/game/src/main/kotlin/content/skill/melee/Weapon.kt b/game/src/main/kotlin/content/skill/melee/Weapon.kt index 872e0e08fc..c624dfcb27 100644 --- a/game/src/main/kotlin/content/skill/melee/Weapon.kt +++ b/game/src/main/kotlin/content/skill/melee/Weapon.kt @@ -16,6 +16,7 @@ class Weapon : Script { } variableSet("autocast,spell") { _, _, _ -> updateWeapon(this, weapon) } + variableSet("attack_style") { _, from, to -> if (to == "long_range") { updateWeapon(this, weapon, 2) diff --git a/game/src/test/kotlin/content/skill/hunter/PitfallTest.kt b/game/src/test/kotlin/content/skill/hunter/PitfallTest.kt new file mode 100644 index 0000000000..a4c6ae5373 --- /dev/null +++ b/game/src/test/kotlin/content/skill/hunter/PitfallTest.kt @@ -0,0 +1,140 @@ +package content.skill.hunter + +import WorldTest +import content.entity.combat.attacker +import content.entity.combat.inCombat +import npcOption +import objectOption +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertNull +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 kotlin.test.assertFalse + +class PitfallTest : WorldTest() { + + @Test + fun `Catch a horned graahk`() { + val player = createPlayer(Tile(2777, 3003)) + val pit = GameObjects.find(Tile(2777, 3001), "pitfall_horned_graahk_14") + player.inventory.add("knife") + player.inventory.add("logs") + player.inventory.add("teasing_stick") + player.levels.set(Skill.Hunter, 99) + + player.objectOption(pit, "Trap") + tick(2) + assertEquals("spiked", player["pitfall_horned_graahk_14", "empty"]) + + val graahk = createNPC("horned_graahk", Tile(2777, 3004)) + + player.npcOption(graahk, "Tease") + tick(2) + + player.objectOption(pit, "Jump") + tick(10) + assertEquals(Tile(2777, 3000), player.tile) + assertEquals("inverse", player["pitfall_horned_graahk_14", "empty"]) + + player.objectOption(pit, "Dismantle") + tick(2) + + assertEquals(1, player.inventory.count("big_bones")) + assertEquals(1, player.inventory.count("tatty_graahk_fur")) + assertNotEquals(0.0, player.experience.get(Skill.Hunter)) + } + + @Test + fun `Catch a spined larupia`() { + val player = createPlayer(Tile(2551, 2904)) + val pit = GameObjects.find(Tile(2552, 2904), "pitfall_spined_larupia_16") + player.inventory.add("knife") + player.inventory.add("logs") + player.inventory.add("teasing_stick") + player.levels.set(Skill.Hunter, 99) + + player.objectOption(pit, "Trap") + tick(2) + assertEquals("spiked", player["pitfall_spined_larupia_16", "empty"]) + + val larupia = createNPC("spined_larupia", Tile(2550, 2905)) + + player.npcOption(larupia, "Tease") + tick(2) + + player.objectOption(pit, "Jump") + tick(10) + assertEquals(Tile(2554, 2904), player.tile) + assertEquals("inverse", player["pitfall_spined_larupia_16", "empty"]) + + player.objectOption(pit, "Dismantle") + tick(2) + + assertEquals(1, player.inventory.count("big_bones")) + assertEquals(1, player.inventory.count("tatty_larupia_fur")) + assertNotEquals(0.0, player.experience.get(Skill.Hunter)) + } + + @Test + fun `Catch a sabre toothed kyatt`() { + val player = createPlayer(Tile(2700, 3787)) + val pit = GameObjects.find(Tile(2700, 3785), "pitfall_sabre_toothed_kyatt_1") + player.inventory.add("knife") + player.inventory.add("logs") + player.inventory.add("teasing_stick") + player.levels.set(Skill.Hunter, 99) + + player.objectOption(pit, "Trap") + tick(2) + assertEquals("spiked", player["pitfall_sabre_toothed_kyatt_1", "empty"]) + + val kyatt = createNPC("sabre_toothed_kyatt", Tile(2700, 3788)) + + player.npcOption(kyatt, "Tease") + tick(2) + + player.objectOption(pit, "Jump") + tick(10) + assertEquals(Tile(2700, 3784), player.tile) + assertEquals("inverse", player["pitfall_sabre_toothed_kyatt_1", "empty"]) + + player.objectOption(pit, "Dismantle") + tick(2) + + assertEquals(1, player.inventory.count("big_bones")) + assertEquals(1, player.inventory.count("tatty_kyatt_fur")) + assertNotEquals(0.0, player.experience.get(Skill.Hunter)) + } + + @Test + fun `Can't tease without hunter level`() { + val player = createPlayer() + val graahk = createNPC("horned_graahk", player.tile.addY(1)) + player.inventory.add("teasing_stick") + player.levels.set(Skill.Hunter, 40) + player.npcOption(graahk, "Tease") + tick(2) + + assertFalse(graahk.inCombat) + assertNull(player.attacker) + } + + @Test + fun `Can't setup pitfall trap without hunter level`() { + val player = createPlayer(Tile(2777, 3003)) + val pit = GameObjects.find(Tile(2777, 3001), "pitfall_horned_graahk_14") + player.inventory.add("knife") + player.inventory.add("logs") + player.inventory.add("teasing_stick") + player.levels.set(Skill.Hunter, 40) + + player.objectOption(pit, "Trap") + tick(2) + assertEquals("empty", player["pitfall_horned_graahk_14", "empty"]) + } +} diff --git a/tools/src/main/kotlin/world/gregs/voidps/tools/map/obj/ObjectUsageFinder.kt b/tools/src/main/kotlin/world/gregs/voidps/tools/map/obj/ObjectUsageFinder.kt index 4e6b39ad29..36934cdb1b 100644 --- a/tools/src/main/kotlin/world/gregs/voidps/tools/map/obj/ObjectUsageFinder.kt +++ b/tools/src/main/kotlin/world/gregs/voidps/tools/map/obj/ObjectUsageFinder.kt @@ -31,10 +31,9 @@ object ObjectUsageFinder { } val set = setOf( - 29317, - 29319, - 29318, - 29320, + 19260, + 19261, + 19264 ) private fun matches(obj: MapObject, def: ObjectDefinitionFull) = set.contains(obj.id)