mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-01-28 10:01:58 +00:00
<!-- Write **BELOW** The Headers and **ABOVE** The comments else it may not be viewable. --> <!-- You can view Contributing.MD for a detailed description of the pull request process. --> ## About The Pull Request <!-- Describe The Pull Request. Please be sure every change is documented or this can delay review and even discourage maintainers from merging your PR! --> Places a check to see if there is a fishing count regen list, and creates a new one if there is not. Then apply a 30 minutes regen on things that have no regen listed. Removes explosions and NPC's from fishing both message in a bottle and unique items. Adjust the tables of Lavaland and Icemoon maps. Made sure both fishing sources have skeleton keys, tendril chests and runite on both tables, to equalize the fishing experience a bit. Removes the failing conditions to the Unit Tests related to fishing, as they depended on this base npc behaviour. I dont have the knowledge to fix the UT beyond that. ## Why It's Good For The Game <!-- Argue for the merits of your changes and how they benefit the game, especially if they are controversial and/or far reaching. If you can't actually explain WHY what you are doing will improve the game, then it probably isn't good for the game in the first place. --> Fixes #90972 Unique Stuff is good to spawn only for players, not for soulless npc's and random explosions. If the rounds extend, is good for content to still exist instead of being lost forever. Gives the chance to players to persevere should the round last that long. We had a lot of duds on Lavaland compared to Icemoon, this just equalizes it. Adds Runite to fishing in Lavaland as its used to make Runite fishng rods, which are good for fishing, it also doesnt alter the balance of the game like adamantium does. Tendril Crates on the Icemoon equalizes the loot pool, and makes for an unique approach to artifacts, albeit slow and super rng based. Having Skeleton keys appear along with the crates means not only miners get some juice of the whole ordeal, which to be fair miners rarely sit down and fish if they want a tendril chest, they just go and get it. Tests: Been a test merge since Feb 20 with no issues reported on downstream: https://github.com/NovaSector/NovaSector/pull/4916 also local:  ## Changelog <!-- If your PR modifies aspects of the game that can be concretely observed by players or admins you should add a changelog. If your change does NOT meet this description, remove this section. Be sure to properly mark your PRs to prevent unnecessary GBP loss. You can read up on GBP and its effects on PRs in the tgstation guides for contributors. Please note that maintainers freely reserve the right to remove and add tags should they deem it appropriate. You can attempt to finagle the system all you want, but it's best to shoot for clear communication right off the bat. --> 🆑 add: To future proof and in case we get long rounds, all limited fishing items regenerate after 30 minutes if they dont have a regeneration time. add: Skeleton keys, Tendril chests, Runite can be found in Lava and Plasma lakes now, although they are rare! balance: Explosions and NPC's no longer nets you uniques from fishing... Fish if you want them! /🆑 <!-- Both 🆑's are required for the changelog to work! You can put your name to the right of the first 🆑 if you want to overwrite your GitHub username as author ingame. --> <!-- You can use multiple of the same prefix (they're only used for the icon ingame) and delete the unneeded ones. Despite some of the tags, changelogs should generally represent how a player might be affected by the changes rather than a summary of the PR's contents. -->
681 lines
28 KiB
Plaintext
681 lines
28 KiB
Plaintext
GLOBAL_LIST_INIT_TYPED(preset_fish_sources, /datum/fish_source, init_subtypes_w_path_keys(/datum/fish_source, list()))
|
|
|
|
/**
|
|
* When adding new fishable rewards to a table/counts, you can specify an icon to show in place of the
|
|
* generic fish icon in the minigame UI should the user have the TRAIT_REVEAL_FISH trait, by adding it to
|
|
* this list.
|
|
*
|
|
* A lot of the icons here may be a tad inaccurate, but since we're limited to the free font awesome icons we
|
|
* have access to, we got to make do.
|
|
*/
|
|
GLOBAL_LIST_INIT(specific_fish_icons, generate_specific_fish_icons())
|
|
|
|
/proc/generate_specific_fish_icons()
|
|
var/list/return_list = zebra_typecacheof(list(
|
|
/datum/data/vending_product = FISH_ICON_COIN,
|
|
/mob/living/basic/axolotl = FISH_ICON_CRITTER,
|
|
/obj/effect/spawner/random/frog = FISH_ICON_CRITTER,
|
|
/mob/living/basic/carp = FISH_ICON_DEF,
|
|
/mob/living/basic/mining = FISH_ICON_HOSTILE,
|
|
/mob/living/basic/skeleton = FISH_ICON_BONE,
|
|
/mob/living/basic/stickman = FISH_ICON_HOSTILE,
|
|
/obj/effect/decal/remains = FISH_ICON_BONE,
|
|
/obj/effect/mob_spawn/corpse = FISH_ICON_BONE,
|
|
/obj/effect/spawner/message_in_a_bottle = FISH_ICON_BOTTLE,
|
|
/obj/item/coin = FISH_ICON_COIN,
|
|
/obj/item/fish = FISH_ICON_DEF,
|
|
/obj/item/fish/armorfish = FISH_ICON_CRAB,
|
|
/obj/item/fish/boned = FISH_ICON_BONE,
|
|
/obj/item/fish/chainsawfish = FISH_ICON_WEAPON,
|
|
/obj/item/fish/chasm_crab = FISH_ICON_CRAB,
|
|
/obj/item/fish/gunner_jellyfish = FISH_ICON_JELLYFISH,
|
|
/obj/item/fish/holo/crab = FISH_ICON_CRAB,
|
|
/obj/item/fish/holo/puffer = FISH_ICON_CHUNKY,
|
|
/obj/item/fish/jumpercable = FISH_ICON_ELECTRIC,
|
|
/obj/item/fish/lavaloop = FISH_ICON_WEAPON,
|
|
/obj/item/fish/mastodon = FISH_ICON_BONE,
|
|
/obj/item/fish/pike/armored = FISH_ICON_WEAPON,
|
|
/obj/item/fish/pufferfish = FISH_ICON_CHUNKY,
|
|
/obj/item/fish/sand_crab = FISH_ICON_CRAB,
|
|
/obj/item/fish/skin_crab = FISH_ICON_CRAB,
|
|
/obj/item/fish/slimefish = FISH_ICON_SLIME,
|
|
/obj/item/fish/sludgefish = FISH_ICON_SLIME,
|
|
/obj/item/fish/starfish = FISH_ICON_STAR,
|
|
/obj/item/fish/stingray = FISH_ICON_WEAPON,
|
|
/obj/item/fish/swordfish = FISH_ICON_WEAPON,
|
|
/obj/item/fish/zipzap = FISH_ICON_ELECTRIC,
|
|
/obj/item/fishing_rod = FISH_ICON_COIN,
|
|
/obj/item/instrument/trumpet/spectral = FISH_ICON_BONE,
|
|
/obj/item/instrument/saxophone/spectral = FISH_ICON_BONE,
|
|
/obj/item/instrument/trombone/spectral = FISH_ICON_BONE,
|
|
/obj/item/knife/carp = FISH_ICON_WEAPON,
|
|
/obj/item/seeds/grass = FISH_ICON_SEED,
|
|
/obj/item/seeds/random = FISH_ICON_SEED,
|
|
/obj/item/storage/wallet = FISH_ICON_COIN,
|
|
/obj/item/stack/sheet/bone = FISH_ICON_BONE,
|
|
/obj/item/stack/sheet/mineral = FISH_ICON_GEM,
|
|
/obj/item/stack/ore = FISH_ICON_GEM,
|
|
/obj/item/survivalcapsule/fishing = FISH_ICON_COIN,
|
|
/obj/structure/closet/crate = FISH_ICON_COIN,
|
|
/obj/structure/mystery_box = FISH_ICON_COIN,
|
|
))
|
|
|
|
return_list[FISHING_RANDOM_SEED] = FISH_ICON_SEED
|
|
return_list[FISHING_RANDOM_ORGAN] = FISH_ICON_ORGAN
|
|
return_list[FISHING_VENDING_CHUCK] = FISH_ICON_COIN
|
|
return return_list
|
|
|
|
/**
|
|
* Where the fish actually come from - every fishing spot has one assigned but multiple fishing holes
|
|
* can share single source, ie single shared one for ocean/lavaland river
|
|
*/
|
|
/datum/fish_source
|
|
/**
|
|
* Fish catch weight table - these are relative weights
|
|
*
|
|
*/
|
|
var/list/fish_table = list()
|
|
/// If a key from fish_table is present here, that fish is availible in limited quantity and is reduced by one on successful fishing
|
|
var/list/fish_counts = list()
|
|
/// Any limited quantity stuff in this list will be readded to the counts after a while
|
|
var/list/fish_count_regen = list()
|
|
/// A list of stuff that's currently waiting to be readded to fish_counts
|
|
var/list/currently_on_regen
|
|
/// Text shown as baloon alert when you roll a dud in the table
|
|
var/duds = list("it was nothing", "the hook is empty")
|
|
/// Baseline difficulty for fishing in this spot. THIS IS ADDED TO THE DEFAULT DIFFICULTY OF THE MINIGAME (15)
|
|
var/fishing_difficulty = FISHING_DEFAULT_DIFFICULTY
|
|
/// How the spot type is described in fish catalog section about fish sources, will be skipped if null
|
|
var/catalog_description
|
|
/// Background image name from /datum/asset/simple/fishing_minigame
|
|
var/background = "background_default"
|
|
var/fish_source_flags = NONE
|
|
/// If FISH_SOURCE_FLAG_EXPLOSIVE_MALUS is set, this will track of how much we're "exhausting" the system by bombing it repeatedly.
|
|
var/explosive_fishing_score = 0
|
|
///When linked to a fishing portal, this will be the icon_state of this option in the radial menu
|
|
var/radial_state = "default"
|
|
///When selected by the fishing portal, this will be the icon_state of the overlay shown on the machine.
|
|
var/overlay_state = "portal_aquarium"
|
|
///If set, this overrides the upper and lower bounds of how long you should wait during the waiting phase of the minigame.
|
|
var/list/wait_time_range
|
|
|
|
/// Mindless mobs that can fish will never pull up items on this list
|
|
var/static/list/profound_fisher_blacklist = typecacheof(list(
|
|
/mob/living/basic/mining/lobstrosity,
|
|
/obj/structure/closet/crate/necropolis/tendril,
|
|
))
|
|
|
|
///List of multipliers used to make fishes more common compared to everything else depending on bait quality, indexed from best to worst.
|
|
var/static/weight_result_multiplier = list(
|
|
TRAIT_GREAT_QUALITY_BAIT = 9,
|
|
TRAIT_GOOD_QUALITY_BAIT = 3.5,
|
|
TRAIT_BASIC_QUALITY_BAIT = 2,
|
|
)
|
|
///List of exponents used to level out the table weight differences between fish depending on bait quality.
|
|
var/static/weight_leveling_exponents = list(
|
|
TRAIT_GREAT_QUALITY_BAIT = 0.7,
|
|
TRAIT_GOOD_QUALITY_BAIT = 0.55,
|
|
TRAIT_BASIC_QUALITY_BAIT = 0.4,
|
|
)
|
|
|
|
//If set, fish types native to this source won't die if left on these turfs.
|
|
var/list/associated_safe_turfs
|
|
//list of subtypes of associated safe turfs that are NOT safe
|
|
var/list/safe_turfs_blacklist
|
|
|
|
/datum/fish_source/New()
|
|
if(!SSfishing.initialized && associated_safe_turfs) //This is only needed during world init
|
|
associated_safe_turfs = typecacheof(associated_safe_turfs)
|
|
if(safe_turfs_blacklist)
|
|
associated_safe_turfs -= typecacheof(safe_turfs_blacklist)
|
|
if(!PERFORM_ALL_TESTS(focus_only/fish_sources_tables))
|
|
return
|
|
for(var/path in fish_counts)
|
|
if(!(path in fish_table))
|
|
stack_trace("path [path] found in the 'fish_counts' list but not in the 'fish_table'")
|
|
if(wait_time_range && length(wait_time_range) != 2)
|
|
stack_trace("wait_time_range for [type] is set but has length different than two")
|
|
for(var/path in fish_counts) //we give anything unique an auto 30 min regen, that way if the round is extended you still get content.
|
|
if (!(path in fish_count_regen))
|
|
fish_count_regen[path] = 30 MINUTES
|
|
|
|
/datum/fish_source/Destroy()
|
|
if(explosive_fishing_score)
|
|
STOP_PROCESSING(SSprocessing, src)
|
|
return ..()
|
|
|
|
///Called when src is set as the fish source of a fishing spot component
|
|
/datum/fish_source/proc/on_fishing_spot_init(datum/component/fishing_spot/spot)
|
|
return
|
|
|
|
///Called whenever a fishing spot with this fish source attached is deleted
|
|
/datum/fish_source/proc/on_fishing_spot_del(datum/component/fishing_spot/spot)
|
|
|
|
/// Can we fish in this spot at all. Returns DENIAL_REASON or null if we're good to go
|
|
/datum/fish_source/proc/reason_we_cant_fish(obj/item/fishing_rod/rod, mob/fisherman, atom/parent)
|
|
return rod.reason_we_cant_fish(src)
|
|
|
|
/// Called below above proc, in case the fishing source has anything to do that isn't denial
|
|
/datum/fish_source/proc/on_start_fishing(obj/item/fishing_rod/rod, mob/fisherman, atom/parent)
|
|
return
|
|
|
|
///Comsig proc from the fishing minigame for 'calculate_difficulty'
|
|
/datum/fish_source/proc/calculate_difficulty_minigame(datum/fishing_challenge/challenge, reward_path, obj/item/fishing_rod/rod, mob/fisherman, list/difficulty_holder)
|
|
SIGNAL_HANDLER
|
|
SHOULD_NOT_OVERRIDE(TRUE)
|
|
difficulty_holder[1] += calculate_difficulty(reward_path, rod, fisherman)
|
|
|
|
// Difficulty modifier added by the fisher's skill level
|
|
if(!(challenge.special_effects & FISHING_MINIGAME_RULE_NO_EXP))
|
|
difficulty_holder[1] += fisherman.mind?.get_skill_modifier(/datum/skill/fishing, SKILL_VALUE_MODIFIER)
|
|
|
|
if(challenge.special_effects & FISHING_MINIGAME_RULE_KILL)
|
|
challenge.RegisterSignal(src, COMSIG_FISH_SOURCE_REWARD_DISPENSED, TYPE_PROC_REF(/datum/fishing_challenge, hurt_fish))
|
|
|
|
/**
|
|
* Calculates the difficulty of the minigame:
|
|
*
|
|
* This includes the source's fishing difficulty, that of the fish, the rod,
|
|
* favorite and disliked baits, fish traits and the fisherman skill.
|
|
*
|
|
* For non-fish, it's just the source's fishing difficulty minus the fisherman skill.
|
|
*/
|
|
/datum/fish_source/proc/calculate_difficulty(result, obj/item/fishing_rod/rod, mob/fisherman)
|
|
. = fishing_difficulty
|
|
|
|
// Difficulty modifier added by having the Settler quirk
|
|
if(HAS_TRAIT(fisherman, TRAIT_EXPERT_FISHER))
|
|
. += EXPERT_FISHER_DIFFICULTY_MOD
|
|
|
|
// Difficulty modifier added by the rod
|
|
. += rod.difficulty_modifier
|
|
|
|
var/is_fish_instance = isfish(result)
|
|
if(!ispath(result,/obj/item/fish) && !is_fish_instance)
|
|
// In the future non-fish rewards can have variable difficulty calculated here
|
|
return
|
|
|
|
var/obj/item/fish/caught_fish = result
|
|
|
|
//Just to clarify when we should use the path instead of the fish, which can be both a path and an instance.
|
|
var/result_path = is_fish_instance ? caught_fish.type : result
|
|
|
|
// Baseline fish difficulty
|
|
. += initial(caught_fish.fishing_difficulty_modifier)
|
|
|
|
var/list/fish_properties = SSfishing.fish_properties[result_path]
|
|
if(rod.bait)
|
|
var/obj/item/bait = rod.bait
|
|
//Fav bait makes it easier
|
|
var/list/fav_bait = fish_properties[FISH_PROPERTIES_FAV_BAIT]
|
|
for(var/bait_identifer in fav_bait)
|
|
if(is_matching_bait(bait, bait_identifer))
|
|
. += FAV_BAIT_DIFFICULTY_MOD
|
|
//Disliked bait makes it harder
|
|
var/list/disliked_bait = fish_properties[FISH_PROPERTIES_BAD_BAIT]
|
|
for(var/bait_identifer in disliked_bait)
|
|
if(is_matching_bait(bait, bait_identifer))
|
|
. += DISLIKED_BAIT_DIFFICULTY_MOD
|
|
|
|
// Matching/not matching fish traits and equipment
|
|
var/list/fish_traits
|
|
if(is_fish_instance)
|
|
fish_traits = caught_fish.fish_traits
|
|
else
|
|
fish_traits = fish_properties[FISH_PROPERTIES_TRAITS]
|
|
|
|
var/additive_mod = 0
|
|
var/multiplicative_mod = 1
|
|
for(var/fish_trait in fish_traits)
|
|
var/datum/fish_trait/trait = GLOB.fish_traits[fish_trait]
|
|
var/list/mod = trait.difficulty_mod(rod, fisherman)
|
|
additive_mod += mod[ADDITIVE_FISHING_MOD]
|
|
multiplicative_mod *= mod[MULTIPLICATIVE_FISHING_MOD]
|
|
|
|
. += additive_mod
|
|
. *= multiplicative_mod
|
|
|
|
|
|
///Comsig proc from the fishing minigame for 'roll_reward'
|
|
/datum/fish_source/proc/roll_reward_minigame(datum/source, obj/item/fishing_rod/rod, mob/fisherman, atom/location, list/rewards)
|
|
SIGNAL_HANDLER
|
|
SHOULD_NOT_OVERRIDE(TRUE)
|
|
rewards += roll_reward(rod, fisherman, location)
|
|
|
|
/// Returns a typepath, instance or another special value which we use for dispensing a reward later.
|
|
/datum/fish_source/proc/roll_reward(obj/item/fishing_rod/rod, mob/fisherman, atom/location)
|
|
return pick_weight(get_modified_fish_table(rod, fisherman, location)) || FISHING_DUD
|
|
|
|
/// Version of roll_reward() that blacklists objects that shouldn't be caught by ai-controlled mobs.
|
|
/datum/fish_source/proc/roll_mindless_reward(obj/item/fishing_rod/rod, mob/fisherman, atom/location)
|
|
var/list/final_table = get_modified_fish_table(rod, fisherman, location)
|
|
final_table -= profound_fisher_blacklist
|
|
return pick_weight(final_table) || FISHING_DUD
|
|
|
|
/**
|
|
* Used to register signals or add traits and the such right after conditions have been cleared
|
|
* and before the minigame starts.
|
|
*/
|
|
/datum/fish_source/proc/pre_challenge_started(obj/item/fishing_rod/rod, mob/user, datum/fishing_challenge/challenge)
|
|
return
|
|
|
|
///Proc called when the challenge is interrupted within the fish source code.
|
|
/datum/fish_source/proc/interrupt_challenge(reason)
|
|
SEND_SIGNAL(src, COMSIG_FISHING_SOURCE_INTERRUPT_CHALLENGE, reason)
|
|
|
|
/**
|
|
* Proc called when the COMSIG_MOB_COMPLETE_FISHING signal is sent.
|
|
* Check if we've succeeded. If so, write into memory and dispense the reward.
|
|
*/
|
|
/datum/fish_source/proc/on_challenge_completed(mob/user, datum/fishing_challenge/challenge, success)
|
|
SIGNAL_HANDLER
|
|
SHOULD_CALL_PARENT(TRUE)
|
|
UnregisterSignal(user, COMSIG_MOB_COMPLETE_FISHING)
|
|
if(!success)
|
|
return
|
|
var/atom/movable/reward = dispense_reward(challenge.reward_path, user, challenge.location, challenge.used_rod)
|
|
if(reward)
|
|
user.add_mob_memory(/datum/memory/caught_fish, protagonist = user, deuteragonist = reward.name)
|
|
SEND_SIGNAL(challenge.used_rod, COMSIG_FISHING_ROD_CAUGHT_FISH, reward, user)
|
|
challenge.used_rod.on_reward_caught(reward, user)
|
|
|
|
/// Gives out the reward if possible
|
|
/datum/fish_source/proc/dispense_reward(reward_path, mob/fisherman, atom/fishing_spot, obj/item/fishing_rod/rod)
|
|
var/atom/movable/reward = simple_dispense_reward(reward_path, get_turf(fisherman), fishing_spot)
|
|
if(!reward) //balloon alert instead
|
|
fisherman.balloon_alert(fisherman, pick(duds))
|
|
return
|
|
if(isitem(reward)) //Try to put it in hand
|
|
INVOKE_ASYNC(fisherman, TYPE_PROC_REF(/mob, put_in_hands), reward)
|
|
else if(istype(reward, /obj/effect/spawner)) // Do not attempt to forceMove() a spawner. It will break things, and the spawned item should already be at the mob's turf by now.
|
|
fisherman.balloon_alert(fisherman, "caught something!")
|
|
return
|
|
fisherman.balloon_alert(fisherman, "caught [reward]!")
|
|
if (isfish(reward))
|
|
ADD_TRAIT(reward, TRAIT_NO_FISHING_ACHIEVEMENT, TRAIT_GENERIC)
|
|
|
|
return reward
|
|
|
|
///Simplified version of dispense_reward that doesn't need a fisherman.
|
|
/datum/fish_source/proc/simple_dispense_reward(reward_path, atom/spawn_location, atom/fishing_spot)
|
|
if(isnull(reward_path))
|
|
return null
|
|
var/area/area = get_area(fishing_spot)
|
|
if(!(area.area_flags & UNLIMITED_FISHING) && !isnull(fish_counts[reward_path])) // This is limited count result
|
|
//Somehow, we're trying to spawn an expended reward.
|
|
if(fish_counts[reward_path] <= 0)
|
|
return null
|
|
fish_counts[reward_path] -= 1
|
|
var/regen_time = fish_count_regen?[reward_path]
|
|
if(regen_time)
|
|
LAZYADDASSOC(currently_on_regen, reward_path, 1)
|
|
if(currently_on_regen[reward_path] == 1)
|
|
addtimer(CALLBACK(src, PROC_REF(regen_count), reward_path), regen_time)
|
|
|
|
var/atom/movable/reward = spawn_reward(reward_path, spawn_location, fishing_spot)
|
|
SEND_SIGNAL(src, COMSIG_FISH_SOURCE_REWARD_DISPENSED, reward)
|
|
return reward
|
|
|
|
/datum/fish_source/proc/regen_count(reward_path)
|
|
if(!LAZYACCESS(currently_on_regen, reward_path))
|
|
return
|
|
fish_counts[reward_path] += 1
|
|
currently_on_regen[reward_path] -= 1
|
|
if(currently_on_regen[reward_path] <= 0)
|
|
LAZYREMOVE(currently_on_regen, reward_path)
|
|
return
|
|
var/regen_time = fish_count_regen[reward_path]
|
|
addtimer(CALLBACK(src, PROC_REF(regen_count), reward_path), regen_time)
|
|
|
|
/// Spawns a reward from a atom path right where the fisherman is. Part of the dispense_reward() logic.
|
|
/datum/fish_source/proc/spawn_reward(reward_path, atom/spawn_location, atom/fishing_spot)
|
|
if(reward_path == FISHING_DUD)
|
|
return
|
|
if(ismovable(reward_path))
|
|
var/atom/movable/reward = reward_path
|
|
reward.forceMove(spawn_location)
|
|
return reward
|
|
if(ispath(reward_path, /datum/chasm_detritus))
|
|
return GLOB.chasm_detritus_types[reward_path].dispense_detritus(spawn_location, fishing_spot)
|
|
if(!ispath(reward_path, /atom/movable))
|
|
CRASH("Unsupported /datum path [reward_path] passed to fish_source/proc/spawn_reward()")
|
|
var/atom/movable/reward = new reward_path(spawn_location)
|
|
if(isfish(reward))
|
|
var/obj/item/fish/caught_fish = reward
|
|
caught_fish.randomize_size_and_weight()
|
|
return reward
|
|
|
|
/// Returns the fish table, with with the unavailable items from fish_counts removed.
|
|
/datum/fish_source/proc/get_fish_table(atom/location, from_explosion = FALSE)
|
|
var/list/table = fish_table.Copy()
|
|
//message bottles cannot spawn from explosions. They're meant to be one-time messages (rarely) and photos from past rounds
|
|
//and it would suck if the pool of bottle messages were constantly being emptied by explosive fishing.
|
|
if(from_explosion)
|
|
table -= /obj/effect/spawner/message_in_a_bottle
|
|
for(var/result in table)
|
|
if(!isnull(fish_counts[result]) && fish_counts[result] <= 0)
|
|
table -= result
|
|
return table
|
|
|
|
/// Builds a fish weights table modified by bait/rod/user properties
|
|
/datum/fish_source/proc/get_modified_fish_table(obj/item/fishing_rod/rod, mob/fisherman, atom/location)
|
|
var/obj/item/bait = rod.bait
|
|
///An exponent used to level out the table weight differences between fish depending on bait quality.
|
|
var/leveling_exponent = 0
|
|
///Multiplier used to make fishes more common compared to everything else.
|
|
var/result_multiplier = 1
|
|
|
|
var/list/final_table = get_fish_table(location)
|
|
|
|
if(bait)
|
|
for(var/trait in weight_result_multiplier)
|
|
if(HAS_TRAIT(bait, trait))
|
|
result_multiplier = weight_result_multiplier[trait]
|
|
leveling_exponent = weight_leveling_exponents[trait]
|
|
break
|
|
|
|
|
|
if(HAS_TRAIT(rod, TRAIT_ROD_REMOVE_FISHING_DUD))
|
|
final_table -= FISHING_DUD
|
|
|
|
if(!fisherman.client)
|
|
final_table -= /obj/effect/spawner/message_in_a_bottle // avoids npc's to get messages in a bottle. Fish for them!
|
|
|
|
for(var/result in final_table)
|
|
final_table[result] *= rod.hook.get_hook_bonus_multiplicative(result)
|
|
final_table[result] += rod.hook.get_hook_bonus_additive(result)//Decide on order here so it can be multiplicative
|
|
|
|
if(ispath(result, /mob/living) && bait && (HAS_TRAIT(bait, TRAIT_GOOD_QUALITY_BAIT) || HAS_TRAIT(bait, TRAIT_GREAT_QUALITY_BAIT)))
|
|
final_table[result] = round(final_table[result] * result_multiplier, 1)
|
|
|
|
else if(ispath(result, /obj/item/fish) || isfish(result))
|
|
if(bait)
|
|
final_table[result] = round(final_table[result] * result_multiplier, 1)
|
|
var/mult = bait.check_bait(result)
|
|
final_table[result] = round(final_table[result] * mult, 1)
|
|
if(mult > 1 && HAS_TRAIT(bait, TRAIT_BAIT_ALLOW_FISHING_DUD))
|
|
final_table -= FISHING_DUD
|
|
else
|
|
final_table[result] = round(final_table[result] * FISH_WEIGHT_MULT_WITHOUT_BAIT, 1) //Fishing without bait is not going to be easy
|
|
|
|
// Apply fish trait modifiers
|
|
final_table[result] = get_fish_trait_catch_mods(final_table[result], result, rod, fisherman, location)
|
|
|
|
if(final_table[result] <= 0)
|
|
final_table -= result
|
|
|
|
|
|
if(leveling_exponent)
|
|
level_out_fish(final_table, leveling_exponent)
|
|
|
|
return final_table
|
|
|
|
///A proc that levels out the weights of various fish, leading to rarer fishes being more common.
|
|
/datum/fish_source/proc/level_out_fish(list/table, exponent)
|
|
var/highest_fish_weight
|
|
var/list/collected_fish_weights = list()
|
|
for(var/fishable in table)
|
|
if(ispath(fishable, /obj/item/fish) || isfish(fishable))
|
|
var/fish_weight = table[fishable]
|
|
collected_fish_weights[fishable] = fish_weight
|
|
if(fish_weight > highest_fish_weight)
|
|
highest_fish_weight = fish_weight
|
|
|
|
for(var/fish in collected_fish_weights)
|
|
var/difference = highest_fish_weight - collected_fish_weights[fish]
|
|
if(!difference)
|
|
continue
|
|
table[fish] += round(difference**exponent, 1)
|
|
|
|
/datum/fish_source/proc/get_fish_trait_catch_mods(weight, obj/item/fish/fish, obj/item/fishing_rod/rod, mob/user, atom/location)
|
|
var/is_fish_instance = isfish(fish)
|
|
if(!ispath(fish, /obj/item/fish) && !is_fish_instance)
|
|
return weight
|
|
var/multiplier = 1
|
|
var/list/fish_traits
|
|
if(is_fish_instance)
|
|
fish_traits = fish.fish_traits
|
|
else
|
|
fish_traits = SSfishing.fish_properties[fish][FISH_PROPERTIES_TRAITS]
|
|
for(var/fish_trait in fish_traits)
|
|
var/datum/fish_trait/trait = GLOB.fish_traits[fish_trait]
|
|
var/list/mod = trait.catch_weight_mod(rod, user, location, is_fish_instance ? fish.type : fish)
|
|
weight += mod[ADDITIVE_FISHING_MOD]
|
|
multiplier *= mod[MULTIPLICATIVE_FISHING_MOD]
|
|
|
|
return round(weight * multiplier, 1)
|
|
|
|
///returns true if this fishing spot has fish that are shown in the catalog.
|
|
/datum/fish_source/proc/has_known_fishes(atom/location)
|
|
var/show_anyway = fish_source_flags & FISH_SOURCE_FLAG_IGNORE_HIDDEN_ON_CATALOG
|
|
for(var/reward in get_fish_table(location))
|
|
if(!ispath(reward, /obj/item/fish) && !isfish(reward))
|
|
continue
|
|
var/obj/item/fish/prototype = reward
|
|
if(!show_anyway && initial(prototype.fish_flags) & FISH_FLAG_SHOW_IN_CATALOG)
|
|
return TRUE
|
|
return FALSE
|
|
|
|
///Add a string with the names of catchable fishes to the examine text.
|
|
/datum/fish_source/proc/get_catchable_fish_names(mob/user, atom/location, list/examine_text)
|
|
var/list/known_fishes = list()
|
|
var/show_anyway = fish_source_flags & FISH_SOURCE_FLAG_IGNORE_HIDDEN_ON_CATALOG
|
|
|
|
var/obj/item/fishing_rod/rod = user.get_active_held_item()
|
|
var/list/final_table
|
|
if(!istype(rod) || !rod.hook)
|
|
rod = null
|
|
else
|
|
final_table = get_modified_fish_table(rod, user, location)
|
|
|
|
var/total_weight = 0
|
|
var/list/rodless_weights = list()
|
|
var/total_rod_weight = 0
|
|
var/list/rod_weights = list()
|
|
var/list/table = get_fish_table(location)
|
|
for(var/reward in table)
|
|
var/weight = table[reward]
|
|
var/final_weight
|
|
if(rod)
|
|
total_weight += weight
|
|
final_weight = final_table[reward]
|
|
total_rod_weight += final_weight
|
|
if(!ispath(reward, /obj/item/fish) && !isfish(reward))
|
|
continue
|
|
var/obj/item/fish/prototype = reward
|
|
if(!show_anyway && !(initial(prototype.fish_flags) & FISH_FLAG_SHOW_IN_CATALOG))
|
|
continue
|
|
if(rod)
|
|
rodless_weights[reward] = weight
|
|
rod_weights[reward] = final_weight
|
|
else
|
|
known_fishes += initial(prototype.name)
|
|
|
|
if(rod)
|
|
for(var/reward in rodless_weights)
|
|
var/percent_weight = rodless_weights[reward] / total_weight
|
|
var/percent_rod_weight = rod_weights[reward] / total_rod_weight
|
|
var/obj/item/fish/prototype = reward
|
|
var/init_name = initial(prototype.name)
|
|
var/ratio = percent_rod_weight ? percent_weight/percent_rod_weight : INFINITY
|
|
if(ratio < 0.9)
|
|
init_name = span_bold(init_name)
|
|
if(ratio < 0.3)
|
|
init_name = "<u>[init_name]</u>"
|
|
else if(ratio > 1.1)
|
|
init_name = span_small(init_name)
|
|
known_fishes += init_name
|
|
|
|
if(!length(known_fishes))
|
|
return
|
|
|
|
var/info = "You can catch the following fish here"
|
|
|
|
if(rod)
|
|
info = span_tooltip("In bold are fish you're more likely to catch with the current setup. The opposite is true for the smaller font", info)
|
|
examine_text += span_info("[info]: [english_list(known_fishes)].")
|
|
|
|
///How much the explosive_fishing_score impacts explosive fishing. The higher the value, the stronger the malus for repeated calls
|
|
#define EXPLOSIVE_FISHING_MALUS_EXPONENT 0.55
|
|
///How much the explosive_fishing_score is reduced each second.
|
|
#define EXPLOSIVE_FISHING_RECOVERY_RATE 0.18
|
|
|
|
/datum/fish_source/proc/spawn_reward_from_explosion(atom/location, severity)
|
|
SIGNAL_HANDLER
|
|
if(fish_source_flags & FISH_SOURCE_FLAG_EXPLOSIVE_NONE)
|
|
return
|
|
var/multiplier = 1
|
|
if(fish_source_flags & FISH_SOURCE_FLAG_EXPLOSIVE_MALUS)
|
|
if(explosive_fishing_score <= 0)
|
|
explosive_fishing_score = 1
|
|
START_PROCESSING(SSprocessing, src)
|
|
else
|
|
explosive_fishing_score++
|
|
multiplier = explosive_fishing_score**-EXPLOSIVE_FISHING_MALUS_EXPONENT
|
|
for(var/i in 1 to (severity + 2))
|
|
if(!prob((100 + 100 * severity)/i * multiplier))
|
|
continue
|
|
var/reward_loot = pick_weight(get_fish_table(location, from_explosion = TRUE))
|
|
var/atom/spawn_location = isturf(location) ? location : location.drop_location()
|
|
var/atom/movable/reward = simple_dispense_reward(reward_loot, spawn_location, location)
|
|
if(isnull(reward))
|
|
continue
|
|
if(isfish(reward))
|
|
var/obj/item/fish/fish = reward
|
|
fish.set_status(FISH_DEAD, silent = TRUE)
|
|
if(isitem(reward))
|
|
reward.pixel_x = rand(-9, 9)
|
|
reward.pixel_y = rand(-9, 9)
|
|
if(severity >= EXPLODE_DEVASTATE)
|
|
reward.ex_act(EXPLODE_LIGHT)
|
|
|
|
/datum/fish_source/process(seconds_per_tick)
|
|
explosive_fishing_score -= EXPLOSIVE_FISHING_RECOVERY_RATE * seconds_per_tick
|
|
if(explosive_fishing_score <= 0)
|
|
STOP_PROCESSING(SSprocessing, src)
|
|
explosive_fishing_score = 0
|
|
|
|
#undef EXPLOSIVE_FISHING_MALUS_EXPONENT
|
|
#undef EXPLOSIVE_FISHING_RECOVERY_RATE
|
|
|
|
///Called when releasing a fish in a fishing spot with the TRAIT_CATCH_AND_RELEASE trait.
|
|
/datum/fish_source/proc/readd_fish(atom/location, obj/item/fish/fish, mob/living/releaser)
|
|
if(releaser)
|
|
var/is_morbid = HAS_MIND_TRAIT(releaser, TRAIT_MORBID)
|
|
var/is_naive = HAS_MIND_TRAIT(releaser, TRAIT_NAIVE)
|
|
if(fish.status == FISH_DEAD) //ded fish won't repopulate the sea.
|
|
if(is_naive || is_morbid)
|
|
releaser.add_mood_event("fish_released", /datum/mood_event/fish_released, is_morbid && !is_naive, fish)
|
|
if(((fish.type in fish_table) != is_morbid) || is_naive)
|
|
releaser.add_mood_event("fish_released", /datum/mood_event/fish_released, is_morbid && !is_naive, fish)
|
|
//don't do anything if the fish is dead, not native to this fish source or has no limited amount.
|
|
if(fish.status == FISH_DEAD || isnull(fish_table[fish.type]) || isnull(fish_counts[fish.type]))
|
|
return
|
|
//ditto if no restrictions apply
|
|
var/area/area = get_area(location)
|
|
if(area.area_flags & UNLIMITED_FISHING)
|
|
return
|
|
//If this fish population isn't recovering from recent losses, we just increase it.
|
|
if(!LAZYACCESS(currently_on_regen, fish.type))
|
|
fish_counts[fish.type] += 1
|
|
else
|
|
regen_count(fish.type)
|
|
|
|
/**
|
|
* Called by /datum/autowiki/fish_sources unless the catalog entry for this fish source is null.
|
|
* It should Return a list of entries with keys named "name", "icon", "weight" and "notes"
|
|
* detailing the contents of this fish source.
|
|
*/
|
|
/datum/fish_source/proc/generate_wiki_contents(datum/autowiki/fish_sources/wiki)
|
|
var/list/data = list()
|
|
var/list/only_fish = list()
|
|
|
|
var/total_weight = 0
|
|
var/total_weight_without_bait = 0
|
|
var/total_weight_no_fish = 0
|
|
|
|
var/list/tables_by_quality = list()
|
|
var/list/total_weight_by_quality = list()
|
|
var/list/total_weight_by_quality_no_fish = list()
|
|
|
|
for(var/obj/item/fish/fish as anything in fish_table)
|
|
var/weight = fish_table[fish]
|
|
if(fish != FISHING_DUD)
|
|
total_weight += weight
|
|
if(!ispath(fish, /obj/item/fish))
|
|
total_weight_without_bait += weight
|
|
total_weight_no_fish += weight
|
|
continue
|
|
if(initial(fish.fish_flags) & FISH_FLAG_SHOW_IN_CATALOG)
|
|
only_fish += fish
|
|
total_weight_without_bait += round(fish_table[fish] * FISH_WEIGHT_MULT_WITHOUT_BAIT, 1)
|
|
|
|
for(var/trait in weight_result_multiplier)
|
|
var/list/table_copy = fish_table.Copy()
|
|
table_copy -= FISHING_DUD
|
|
var/exponent = weight_leveling_exponents[trait]
|
|
var/multiplier = weight_result_multiplier[trait]
|
|
for(var/fish as anything in table_copy)
|
|
if(!ispath(fish, /obj/item/fish))
|
|
continue
|
|
table_copy[fish] = round(table_copy[fish] * multiplier, 1)
|
|
|
|
level_out_fish(table_copy, exponent)
|
|
tables_by_quality[trait] = table_copy
|
|
|
|
var/tot_weight = 0
|
|
var/tot_weight_no_fish = 0
|
|
for(var/result in table_copy)
|
|
var/weight = table_copy[result]
|
|
tot_weight += weight
|
|
if(!ispath(result, /obj/item/fish))
|
|
tot_weight_no_fish += weight
|
|
total_weight_by_quality[trait] = tot_weight
|
|
total_weight_by_quality_no_fish[trait] = tot_weight_no_fish
|
|
|
|
//show the improved weights in ascending orders for fish.
|
|
tables_by_quality = reverseList(tables_by_quality)
|
|
|
|
if(FISHING_DUD in fish_table)
|
|
data += LIST_VALUE_WRAP_LISTS(list(
|
|
FISH_SOURCE_AUTOWIKI_NAME = FISH_SOURCE_AUTOWIKI_DUD,
|
|
FISH_SOURCE_AUTOWIKI_ICON = "",
|
|
FISH_SOURCE_AUTOWIKI_WEIGHT = PERCENT(fish_table[FISHING_DUD]/total_weight_without_bait),
|
|
FISH_SOURCE_AUTOWIKI_WEIGHT_SUFFIX = "WITHOUT A BAIT",
|
|
FISH_SOURCE_AUTOWIKI_NOTES = "Unless you have a magnet or rescue hook or you know what you're doing, always use a bait",
|
|
))
|
|
|
|
for(var/obj/item/fish/fish as anything in only_fish)
|
|
var/weight = fish_table[fish]
|
|
var/deets = "Can be caught indefinitely"
|
|
if(fish in fish_counts)
|
|
deets = "It's quite rare and can only be caught up to [fish_counts[fish]] times"
|
|
if(fish in fish_count_regen)
|
|
deets += " every [DisplayTimeText(fish::breeding_timeout)]"
|
|
var/list/weight_deets = list()
|
|
for(var/trait in tables_by_quality)
|
|
weight_deets += "[round(PERCENT(tables_by_quality[trait][fish]/total_weight_by_quality[trait]), 0.1)]%"
|
|
var/weight_suffix = "([english_list(weight_deets, and_text = ", ")])"
|
|
data += LIST_VALUE_WRAP_LISTS(list(
|
|
FISH_SOURCE_AUTOWIKI_NAME = wiki.escape_value(full_capitalize(initial(fish.name))),
|
|
FISH_SOURCE_AUTOWIKI_ICON = FISH_AUTOWIKI_FILENAME(fish),
|
|
FISH_SOURCE_AUTOWIKI_WEIGHT = PERCENT(weight/total_weight),
|
|
FISH_SOURCE_AUTOWIKI_WEIGHT_SUFFIX = weight_suffix,
|
|
FISH_SOURCE_AUTOWIKI_NOTES = deets,
|
|
))
|
|
|
|
if(total_weight_no_fish) //There are things beside fish that we can catch.
|
|
var/list/weight_deets = list()
|
|
for(var/trait in tables_by_quality)
|
|
weight_deets += "[round(PERCENT(total_weight_by_quality_no_fish[trait]/total_weight_by_quality[trait]), 0.1)]%"
|
|
var/weight_suffix = "([english_list(weight_deets, and_text = ", ")])"
|
|
data += LIST_VALUE_WRAP_LISTS(list(
|
|
FISH_SOURCE_AUTOWIKI_NAME = FISH_SOURCE_AUTOWIKI_OTHER,
|
|
FISH_SOURCE_AUTOWIKI_ICON = FISH_SOURCE_AUTOWIKI_QUESTIONMARK,
|
|
FISH_SOURCE_AUTOWIKI_WEIGHT = PERCENT(total_weight_no_fish/total_weight),
|
|
FISH_SOURCE_AUTOWIKI_WEIGHT_SUFFIX = weight_suffix,
|
|
FISH_SOURCE_AUTOWIKI_NOTES = "Who knows what it may be. Try and find out",
|
|
))
|
|
|
|
return data
|