Adds aquariums and aquarium fish. (#56343)

Co-authored-by: tralezab <spamqetuo2@gmail.com>
Co-authored-by: Mothblocks <35135081+Jared-Fogle@users.noreply.github.com>
Co-authored-by: Aleksej Komarov <stylemistake@gmail.com>
Co-authored-by: Qustinnus <Floydje123@hotmail.com>
Co-authored-by: coiax <yellowbounder@gmail.com>
This commit is contained in:
AnturK
2021-01-27 12:44:36 -08:00
committed by GitHub
co-authored by tralezab Mothblocks Aleksej Komarov Qustinnus coiax
parent 2f2c9a5fa5
commit 8c69586774
24 changed files with 1285 additions and 4 deletions
+251
View File
@@ -0,0 +1,251 @@
#define AQUARIUM_LAYER_STEP 0.01
/// Aquarium content layer offsets
#define AQUARIUM_MIN_OFFSET 0.01
#define AQUARIUM_MAX_OFFSET 1
/obj/structure/aquarium
name = "aquarium"
density = TRUE
anchored = TRUE
icon = 'icons/obj/aquarium.dmi'
icon_state = "aquarium_base"
integrity_failure = 0.3
var/fluid_type = AQUARIUM_FLUID_FRESHWATER
var/fluid_temp = MIN_AQUARIUM_TEMP
var/min_fluid_temp = MIN_AQUARIUM_TEMP
var/max_fluid_temp = MAX_AQUARIUM_TEMP
var/lamp = FALSE
var/glass_icon_state = "aquarium_glass"
var/broken_glass_icon_state = "aquarium_glass_broken"
//This is the area where fish can swim
var/aquarium_zone_min_px = 2
var/aquarium_zone_max_px = 31
var/aquarium_zone_min_py = 10
var/aquarium_zone_max_py = 24
var/list/fluid_types = list(AQUARIUM_FLUID_SALTWATER, AQUARIUM_FLUID_FRESHWATER, AQUARIUM_FLUID_SULPHWATEVER, AQUARIUM_FLUID_AIR)
var/panel_open = TRUE
///Current layers in use by aquarium contents
var/list/used_layers = list()
var/alive_fish = 0
var/dead_fish = 0
/obj/structure/aquarium/Initialize()
. = ..()
update_icon()
RegisterSignal(src,COMSIG_PARENT_ATTACKBY, .proc/feed_feedback)
/obj/structure/aquarium/proc/request_layer(layer_type)
/**
* base aq layer
* min_offset = this value is returned on bottom layer mode
* min_offset + 0.1 fish1
* min_offset + 0.2 fish2
* ... these layers are returned for auto layer mode and tracked by used_layers
* min_offset + max_offset = this value is returned for top layer mode
* min_offset + max_offset + 1 = this is used for glass overlay
*/
//optional todo: hook up sending surface changed on aquarium changing layers
switch(layer_type)
if(AQUARIUM_LAYER_MODE_BOTTOM)
return layer + AQUARIUM_MIN_OFFSET
if(AQUARIUM_LAYER_MODE_TOP)
return layer + AQUARIUM_MAX_OFFSET
if(AQUARIUM_LAYER_MODE_AUTO)
var/chosen_layer = layer + AQUARIUM_MIN_OFFSET + AQUARIUM_LAYER_STEP
while((chosen_layer in used_layers) && (chosen_layer <= layer + AQUARIUM_MAX_OFFSET))
chosen_layer += AQUARIUM_LAYER_STEP
used_layers += chosen_layer
return chosen_layer
/obj/structure/aquarium/proc/free_layer(value)
used_layers -= value
/obj/structure/aquarium/proc/get_surface_properties()
. = list()
.[AQUARIUM_PROPERTIES_PX_MIN] = aquarium_zone_min_px
.[AQUARIUM_PROPERTIES_PX_MAX] = aquarium_zone_max_px
.[AQUARIUM_PROPERTIES_PY_MIN] = aquarium_zone_min_py
.[AQUARIUM_PROPERTIES_PY_MAX] = aquarium_zone_max_py
/obj/structure/aquarium/update_overlays()
. = ..()
if(panel_open)
. += "panel"
//Glass overlay goes on top of everything else.
var/mutable_appearance/glass_overlay = mutable_appearance(icon,broken ? broken_glass_icon_state : glass_icon_state,layer=AQUARIUM_MAX_OFFSET-1)
. += glass_overlay
/obj/structure/aquarium/examine(mob/user)
. = ..()
. += "<span class='notice'>Alt-click to [panel_open ? "close" : "open"] the control panel.</span>"
/obj/structure/aquarium/AltClick(mob/user)
if(!user.canUseTopic(src, BE_CLOSE))
return ..()
panel_open = !panel_open
update_icon()
/obj/structure/aquarium/wrench_act(mob/living/user, obj/item/I)
if(default_unfasten_wrench(user,I))
return TRUE
/obj/structure/aquarium/attackby(obj/item/I, mob/living/user, params)
if(broken)
var/obj/item/stack/sheet/glass/glass = I
if(istype(glass))
if(glass.get_amount() < 2)
to_chat(user, "<span class='warning'>You need two glass sheets to fix the case!</span>")
return
to_chat(user, "<span class='notice'>You start fixing [src]...</span>")
if(do_after(user, 2 SECONDS, target = src))
glass.use(2)
broken = FALSE
obj_integrity = max_integrity
update_icon()
return TRUE
else
// This signal exists so we common items instead of adding component on init can just register creation of one in response.
// This way we can avoid the cost of 9999 aquarium components on rocks that will never see water in their life.
SEND_SIGNAL(I,COMSIG_AQUARIUM_BEFORE_INSERT_CHECK,src)
var/datum/component/aquarium_content/content_component = I.GetComponent(/datum/component/aquarium_content)
if(content_component && content_component.is_ready_to_insert(src))
if(user.transferItemToLoc(I,src))
update_icon()
return TRUE
else
return ..()
return ..()
/obj/structure/aquarium/proc/feed_feedback(datum/source, obj/item/thing, mob/user, params)
SIGNAL_HANDLER
if(istype(thing, /obj/item/fish_feed))
to_chat(user,"<span class='notice'>You feed the fish.</span>")
return NONE
/obj/structure/aquarium/interact(mob/user)
if(!broken && user.pulling && user.a_intent == INTENT_GRAB && isliving(user.pulling))
var/mob/living/living_pulled = user.pulling
SEND_SIGNAL(living_pulled, COMSIG_AQUARIUM_BEFORE_INSERT_CHECK,src)
var/datum/component/aquarium_content/content_component = living_pulled.GetComponent(/datum/component/aquarium_content)
if(content_component && content_component.is_ready_to_insert(src))
try_to_put_mob_in(user)
else if(panel_open)
. = ..() //call base ui_interact
else
admire(user)
/// Tries to put mob pulled by the user in the aquarium after a delay
/obj/structure/aquarium/proc/try_to_put_mob_in(mob/user)
if(user.pulling && user.a_intent == INTENT_GRAB && isliving(user.pulling))
var/mob/living/living_pulled = user.pulling
if(living_pulled.buckled || living_pulled.has_buckled_mobs())
to_chat(user, "<span class='warning'>[living_pulled] is attached to something!</span>")
return
user.visible_message("<span class='danger'>[user] starts to put [living_pulled] into [src]!</span>")
if(do_after(user, 10 SECONDS, target = src))
if(QDELETED(living_pulled) || user.pulling != living_pulled || living_pulled.buckled || living_pulled.has_buckled_mobs())
return
var/datum/component/aquarium_content/content_component = living_pulled.GetComponent(/datum/component/aquarium_content)
if(content_component || content_component.is_ready_to_insert(src))
return
user.visible_message("<span class='danger'>[user] stuffs [living_pulled] into [src]!</span>")
living_pulled.forceMove(src)
update_icon()
///Apply mood bonus depending on aquarium status
/obj/structure/aquarium/proc/admire(mob/user)
to_chat(user,"<span class='notice'>You take a moment to watch [src].</span>")
if(do_after(user, 5 SECONDS, target = src))
//Check if there are live fish - good mood
//All fish dead - bad mood.
//No fish - nothing.
if(alive_fish > 0)
SEND_SIGNAL(user, COMSIG_ADD_MOOD_EVENT, "aquarium", /datum/mood_event/aquarium_positive)
else if(dead_fish > 0)
SEND_SIGNAL(user, COMSIG_ADD_MOOD_EVENT, "aquarium", /datum/mood_event/aquarium_negative)
// Could maybe scale power of this mood with number/types of fish
/obj/structure/aquarium/ui_data(mob/user)
. = ..()
.["fluid_type"] = fluid_type
.["temperature"] = fluid_temp
var/list/content_data = list()
for(var/atom/movable/fish in contents)
content_data += list(list("name"=fish.name,"ref"=ref(fish)))
.["contents"] = content_data
/obj/structure/aquarium/ui_static_data(mob/user)
. = ..()
//I guess these should depend on the fluid so lava critters can get high or stuff below water freezing point but let's keep it simple for now.
.["minTemperature"] = min_fluid_temp
.["maxTemperature"] = max_fluid_temp
.["fluidTypes"] = fluid_types
/obj/structure/aquarium/ui_act(action, params)
. = ..()
if(.)
return
var/mob/user = usr
switch(action)
if("temperature")
var/temperature = params["temperature"]
if(isnum(temperature))
fluid_temp = clamp(temperature, min_fluid_temp, max_fluid_temp)
. = TRUE
if("fluid")
if(params["fluid"] in fluid_types)
fluid_type = params["fluid"]
SEND_SIGNAL(src, COMSIG_AQUARIUM_FLUID_CHANGED, fluid_type)
. = TRUE
if("remove")
var/atom/movable/inside = locate(params["ref"]) in contents
if(inside)
if(isitem(inside))
user.put_in_hands(inside)
else
inside.forceMove(get_turf(src))
to_chat(user,"<span class='notice'>You take out [inside] from [src].</span>")
/obj/structure/aquarium/ui_interact(mob/user, datum/tgui/ui)
. = ..()
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "Aquarium", name)
ui.open()
/obj/structure/aquarium/obj_break(damage_flag)
. = ..()
if(!broken)
aquarium_smash()
/obj/structure/aquarium/proc/aquarium_smash()
broken = TRUE
var/possible_destinations_for_fish = list()
var/droploc = drop_location()
if(isturf(droploc))
possible_destinations_for_fish = get_adjacent_open_turfs(droploc)
else
possible_destinations_for_fish = list(droploc)
playsound(src, 'sound/effects/glassbr3.ogg', 100, TRUE)
for(var/atom/movable/fish in contents)
fish.forceMove(pick(possible_destinations_for_fish))
if(fluid_type != AQUARIUM_FLUID_AIR)
var/datum/reagents/reagent_splash = new()
reagent_splash.add_reagent(/datum/reagent/water, 30)
chem_splash(droploc, 3, list(reagent_splash))
update_icon()
#undef AQUARIUM_LAYER_STEP
#undef AQUARIUM_MIN_OFFSET
#undef AQUARIUM_MAX_OFFSET
+259
View File
@@ -0,0 +1,259 @@
// Configuration objects defining prop/fish behaviour in the aquarium
// These are used as a base for autogenerating the actual instances
/datum/aquarium_behaviour
var/name = "base aquarium element"
var/desc = "<insert funny description here>"
var/unique = FALSE
var/icon = 'icons/obj/aquarium.dmi'
/// Icon state used for catalog/autogenerated fish item. Also used as basis for in aquarium visual if dedicated_in_aquarium_icon_state is not set.
var/icon_state
/// Applied and item/catalog for use with greyscaled icons.
var/color
/// How the thing will be layered
var/layer_mode = AQUARIUM_LAYER_MODE_AUTO
/// If the starting position is randomised within bounds.
var/randomize_position = FALSE
/**
* Fish sprite how to:
* Need to be centered on 16,16 in the dmi and facing left by default.
* sprite_height/sprite_width is the size it will have in aquarium and used to control animation boundaries.
* source_height/source_width is the size of the original icon (ideally only the non-empty parts)
*/
/// If this is set this icon state will be used for the holder while icon_state will only be used for item/catalog. Transformation from source_width/height WON'T be applied.
var/dedicated_in_aquarium_icon_state
/// Applied to vc object only for use with greyscaled icons.
var/aquarium_vc_color
//Target sprite size for path/position calculations.
var/sprite_height = 3
var/sprite_width = 3
//This is the size of the source sprite. This will be used to calculate scale down factor.
var/source_width = 32
var/source_height = 32
/// Current animation
var/current_animation
/// Does this behviour need additional processing in aquarium, will be added to SSobj processing on insertion
var/processing = FALSE
/// Our holder component
var/datum/component/aquarium_content/parent
/// Applies icon,color and base scaling to our visual holder
/datum/aquarium_behaviour/proc/apply_appearance(obj/effect/holder)
holder.icon = icon
if(dedicated_in_aquarium_icon_state)
holder.icon_state = dedicated_in_aquarium_icon_state
else
holder.icon_state = icon_state
holder.transform = base_transform()
if(aquarium_vc_color)
holder.color = aquarium_vc_color
// Generates scaling matrix for our visual
/datum/aquarium_behaviour/proc/base_transform()
var/matrix/matrix = matrix()
if(!dedicated_in_aquarium_icon_state)
var/x_scale = sprite_width / source_width
var/y_scale = sprite_height / source_height
matrix.Scale(x_scale, y_scale)
return matrix
/// Called when fed.
/datum/aquarium_behaviour/proc/on_feeding(datum/reagents/feed_reagents)
return
/// Called when aquarium fluid changes.
/datum/aquarium_behaviour/proc/on_fluid_changed()
return
/// Called to update current animation based on state
/datum/aquarium_behaviour/proc/update_animation()
return
/// Called when inserted into new aquarium
/datum/aquarium_behaviour/proc/on_inserted()
if(processing)
START_PROCESSING(SSobj, src)
/// Called just before the object gets removed from the aquarium
/datum/aquarium_behaviour/proc/before_removal()
return
/datum/aquarium_behaviour/Destroy(force, ...)
STOP_PROCESSING(SSobj, src)
parent = null
return ..()
/datum/aquarium_behaviour/prop
name = "aquarium prop"
unique = TRUE
layer_mode = AQUARIUM_LAYER_MODE_BOTTOM
// Prop sprites do not need any scaling they go 1:1
sprite_width = 32
sprite_height = 32
source_width = 32
source_height = 32
/datum/aquarium_behaviour/prop/rocks
name = "rocks"
icon_state = "rocks"
/datum/aquarium_behaviour/prop/seaweed_top
name = "dense seaweeds"
icon_state = "seaweeds_front"
layer_mode = AQUARIUM_LAYER_MODE_TOP
/datum/aquarium_behaviour/prop/seaweed
name = "seaweeds"
icon_state = "seaweeds_back"
layer_mode = AQUARIUM_LAYER_MODE_BOTTOM
/datum/aquarium_behaviour/prop/rockfloor
name = "rock floor"
icon_state = "rockfloor"
layer_mode = AQUARIUM_LAYER_MODE_BOTTOM
/datum/aquarium_behaviour/prop/treasure
name = "tiny treasure chest"
icon_state = "treasure"
layer_mode = AQUARIUM_LAYER_MODE_BOTTOM
/datum/aquarium_behaviour/fish
name = "generic fish"
icon_state = "fish_greyscale"
randomize_position = TRUE //We have random starting point of our swim path
current_animation = AQUARIUM_ANIMATION_FISH_SWIM
processing = TRUE
// Default size of the "greyscale_fish" icon_state
sprite_height = 3
sprite_width = 3
/// Required fluid type for this fish to live.
var/required_fluid_type = AQUARIUM_FLUID_FRESHWATER
/// Required minimum temperature for the fish to live.
var/required_temperature_min = MIN_AQUARIUM_TEMP
/// Maximum possible temperature for the fish to live.
var/required_temperature_max = MAX_AQUARIUM_TEMP
/// What type of reagent this fish needs to be fed.
var/food = /datum/reagent/consumable/nutriment
/// How often the fish needs to be fed
var/feeding_frequency = 5 MINUTES
/// Time of last feedeing
var/last_feeding
/// Fish status
var/status = FISH_ALIVE
/// Current fish health. Dies at 0.
var/health = 100
/// Should this fish type show in fish catalog
var/show_in_catalog = TRUE
/// Should this fish spawn in random fish cases
var/availible_in_random_cases = TRUE
/// How rare this fish is in the random cases
var/random_case_rarity = FISH_RARITY_BASIC
/// Fish autogenerated from this behaviour will be processable into this
var/fillet_type = /obj/item/food/carpmeat/icantbeliveitsnotcarp
/datum/aquarium_behaviour/fish/on_inserted()
. = ..()
if(isnull(last_feeding)) //Fish start fed.
last_feeding = world.time
if(status == FISH_DEAD)
parent.current_aquarium.dead_fish += 1
else
parent.current_aquarium.alive_fish += 1
/datum/aquarium_behaviour/fish/before_removal()
. = ..()
if(status == FISH_DEAD)
parent.current_aquarium.dead_fish -= 1
else
parent.current_aquarium.alive_fish -= 1
/datum/aquarium_behaviour/fish/on_fluid_changed()
//In case we'll flop to bottom from this or go back to swimming.
update_animation()
/// Updates our animation variable based on state and prompts component to animate it.
/datum/aquarium_behaviour/fish/update_animation()
var/obj/structure/aquarium/aquarium = parent.current_aquarium
var/old_animation = current_animation
if(!aquarium || aquarium.fluid_type == AQUARIUM_FLUID_AIR || status == FISH_DEAD)
current_animation = AQUARIUM_ANIMATION_FISH_DEAD
else
current_animation = AQUARIUM_ANIMATION_FISH_SWIM
if(aquarium && old_animation != current_animation)
parent.generate_animation()
/datum/aquarium_behaviour/fish/on_feeding(datum/reagents/feed_reagents)
if(feed_reagents.has_reagent(food))
last_feeding = world.time
/// Checks if our current environment lets us live.
/datum/aquarium_behaviour/fish/proc/proper_environment()
var/obj/structure/aquarium/aquarium = parent.current_aquarium
if(!aquarium)
return FALSE
if(aquarium.fluid_type != required_fluid_type)
return FALSE
if(aquarium.fluid_temp < required_temperature_min || aquarium.fluid_temp > required_temperature_max)
return FALSE
return TRUE
/datum/aquarium_behaviour/fish/process(delta_time)
set waitfor = FALSE
var/health_change_per_second = 0
if(!proper_environment())
health_change_per_second -= 3 //Dying here
if(world.time - last_feeding >= feeding_frequency)
health_change_per_second -= 0.5 //Starving
else
health_change_per_second += 0.5 //Slowly healing
health = clamp(health + health_change_per_second * delta_time, 0, initial(health))
if(health <= 0)
death()
/datum/aquarium_behaviour/fish/proc/death()
STOP_PROCESSING(SSobj, src)
status = FISH_DEAD
var/message = "<span class='notice'>\The [name] dies.</span>"
if(parent.current_aquarium)
parent.current_aquarium.visible_message(message)
parent.current_aquarium.alive_fish -= 1
parent.current_aquarium.dead_fish += 1
else
var/atom/movable/AM = parent.parent
AM.visible_message(message)
update_animation()
/// This path exists mostly for admin abuse.
/datum/aquarium_behaviour/fish/auto
name = "automatic fish"
desc = "generates fish appearance automatically from component parent appearance"
availible_in_random_cases = FALSE
sprite_width = 8
sprite_height = 8
show_in_catalog = FALSE
/datum/aquarium_behaviour/fish/auto/apply_appearance(obj/effect/holder)
holder.appearance = parent.parent
holder.transform = base_transform()
holder.dir = WEST
+143
View File
@@ -0,0 +1,143 @@
// Fish path used for autogenerated fish
/obj/item/fish
name = "generic looking aquarium fish"
desc = "very bland"
w_class = WEIGHT_CLASS_TINY
/// Automatically generates object of given base path from the behaviour type in loc
/proc/generate_fish(loc,behaviour_type,base_path=/obj/item/fish)
var/datum/aquarium_behaviour/behaviour = behaviour_type
var/obj/item/fish = new /obj/item/fish(loc)
fish.name = initial(behaviour.name)
fish.icon = initial(behaviour.icon)
fish.icon_state = initial(behaviour.icon_state)
fish.desc = initial(behaviour.desc)
if(initial(behaviour.color))
fish.add_atom_colour(initial(behaviour.color), FIXED_COLOUR_PRIORITY)
if(ispath(behaviour_type,/datum/aquarium_behaviour/fish))
var/datum/aquarium_behaviour/fish/fish_behaviour = behaviour_type
var/fillet_type = initial(fish_behaviour.fillet_type)
if(fillet_type)
fish.AddElement(/datum/element/processable, TOOL_KNIFE, fillet_type, 1, 5)
fish.AddElement(/datum/element/deferred_aquarium_content, behaviour_type)
return fish
/// Returns random fish, using random_case_rarity probabilities.
/proc/random_fish_type(case_fish_only=TRUE, required_fluid)
var/static/probability_table
var/argkey = "fish_[required_fluid]_[case_fish_only]" //If this expands more extract bespoke element arg generation to some common helper.
if(!probability_table || !probability_table[argkey])
if(!probability_table)
probability_table = list()
var/chance_table = list()
for(var/_fish_behavior in subtypesof(/datum/aquarium_behaviour/fish))
var/datum/aquarium_behaviour/fish/fish_behavior = _fish_behavior
if(required_fluid && initial(fish_behavior.required_fluid_type) != required_fluid)
continue
if(initial(fish_behavior.availible_in_random_cases) || !case_fish_only)
chance_table[fish_behavior] = initial(fish_behavior.random_case_rarity)
probability_table[argkey] = chance_table
return pickweight(probability_table[argkey])
// Actual fish definitions below - there's no specific paths, they are autogenerated from behaviours
// Freshwater fish
/datum/aquarium_behaviour/fish/goldfish
name = "goldfish"
desc = "Despite common belief, goldfish do not have three-second memories. They can actually remember things that happened up to three months ago."
icon_state = "goldfish"
sprite_width = 8
sprite_height = 8
/datum/aquarium_behaviour/fish/angelfish
name = "angelfish"
desc = "Young Angelfish often live in groups, while adults prefer solitary life. They become territorial and aggressive toward other fish when they reach adulthood."
icon_state = "angelfish"
dedicated_in_aquarium_icon_state = "bigfish"
sprite_height = 7
source_height = 7
/datum/aquarium_behaviour/fish/guppy
name = "guppy"
desc = "Guppy is also known as rainbow fish because of the brightly colored body and fins."
icon_state = "guppy"
dedicated_in_aquarium_icon_state = "fish_greyscale"
aquarium_vc_color = "#91AE64"
sprite_width = 8
sprite_height = 5
/datum/aquarium_behaviour/fish/plasmatetra
name = "plasma tetra"
desc = "Due to their small size, tetras are prey to many predators in their watery world, including eels, crustaceans, and invertebrates."
icon_state = "plastetra"
dedicated_in_aquarium_icon_state = "fish_greyscale"
aquarium_vc_color = "#D30EB0"
/datum/aquarium_behaviour/fish/catfish
name = "cory catfish"
desc = "A catfish has about 100,000 taste buds, and their bodies are covered with them to help detect chemicals present in the water and also to respond to touch."
icon_state = "catfish"
dedicated_in_aquarium_icon_state = "fish_greyscale"
aquarium_vc_color = "#907420"
/datum/aquarium_behaviour/fish/spacecarp
name = "space carp"
desc = "This space predator fish is known to cause yearly damage to space stations during their migrations."
icon_state = "carp"
sprite_width = 8
sprite_height = 8
availible_in_random_cases = FALSE
// Saltwater fish below
/datum/aquarium_behaviour/fish/clownfish
name = "clownfish"
desc = "Clownfish catch prey by swimming onto the reef, attracting larger fish, and luring them back to the anemone. The anemone will sting and eat the larger fish, leaving the remains for the clownfish."
icon_state = "clownfish"
dedicated_in_aquarium_icon_state = "clownfish_small"
required_fluid_type = AQUARIUM_FLUID_SALTWATER
sprite_width = 8
sprite_height = 5
/datum/aquarium_behaviour/fish/cardinal
name = "cardinalfish"
desc = "Cardinalfish are often found near sea urchins, where the fish hide when threatened."
icon_state = "cardinalfish"
dedicated_in_aquarium_icon_state = "fish_greyscale"
required_fluid_type = AQUARIUM_FLUID_SALTWATER
/datum/aquarium_behaviour/fish/greenchromis
name = "green chromis"
desc = "The Chromis can vary in color from blue to green depending on the lighting and distance from the lights."
icon_state = "greenchromis"
dedicated_in_aquarium_icon_state = "fish_greyscale"
aquarium_vc_color = "#00ff00"
required_fluid_type = AQUARIUM_FLUID_SALTWATER
/datum/aquarium_behaviour/fish/firefish
name = "firefish goby"
desc = "To communicate in the wild, the firefish uses its dorsal fin to alert others of potential danger."
icon_state = "firefish"
sprite_width = 6
sprite_height = 5
required_fluid_type = AQUARIUM_FLUID_SALTWATER
/datum/aquarium_behaviour/fish/pufferfish
name = "pufferfish"
desc = "One Pufferfish contains enough toxins in its liver to kill 30 people."
icon_state = "pufferfish"
required_fluid_type = AQUARIUM_FLUID_SALTWATER
sprite_width = 8
sprite_height = 8
/datum/aquarium_behaviour/fish/lanternfish
name = "lanternfish"
desc = "Typically found in areas below 6600 feet below the surface of the ocean, they live in complete darkness."
icon_state = "lanternfish"
required_fluid_type = AQUARIUM_FLUID_SALTWATER
random_case_rarity = FISH_RARITY_VERY_RARE
source_width = 28
source_height = 21
sprite_width = 8
sprite_height = 8
+125
View File
@@ -0,0 +1,125 @@
#define AQUARIUM_COMPANY "Aquatech Ltd."
// Fish feed can
/obj/item/fish_feed
name = "fish feed can"
desc = "Autogenerates nutritious fish feed based on sample inside."
icon = 'icons/obj/aquarium.dmi'
icon_state = "fish_feed"
w_class = WEIGHT_CLASS_TINY
/obj/item/fish_feed/Initialize()
. = ..()
create_reagents(5, OPENCONTAINER)
reagents.add_reagent(/datum/reagent/consumable/nutriment, 1) //Default fish diet
// Stasis fish case container for moving fish between aquariums safely.
/obj/item/storage/fish_case
name = "stasis fish case"
desc = "A small case keeping the fish inside in stasis."
icon_state = "fishbox"
inhand_icon_state = "syringe_kit"
lefthand_file = 'icons/mob/inhands/equipment/medical_lefthand.dmi'
righthand_file = 'icons/mob/inhands/equipment/medical_righthand.dmi'
component_type = /datum/component/storage/concrete/fish_case
/obj/item/storage/fish_case/Initialize()
. = ..()
ADD_TRAIT(src, TRAIT_FISH_SAFE_STORAGE, TRAIT_GENERIC)
/// Fish case with single random fish inside.
/obj/item/storage/fish_case/random/PopulateContents()
. = ..()
generate_fish(src, select_fish_type())
/obj/item/storage/fish_case/random/proc/select_fish_type()
return random_fish_type()
/obj/item/storage/fish_case/random/freshwater/select_fish_type()
return random_fish_type(required_fluid=AQUARIUM_FLUID_FRESHWATER)
/// Book detailing where to get the fish and their properties.
/obj/item/book/fish_catalog
name = "Fish Encyclopedia"
desc = "Indexes all fish known to mankind (and related species)"
icon_state = "fishbook"
dat = "Lot of fish stuff" //book wrappers could use cleaning so this is not necessary
/obj/item/book/fish_catalog/on_read(mob/user)
ui_interact(user)
/obj/item/book/fish_catalog/ui_interact(mob/user, datum/tgui/ui)
. = ..()
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "FishCatalog", name)
ui.open()
/obj/item/book/fish_catalog/ui_static_data(mob/user)
. = ..()
var/static/fish_info
if(!fish_info)
fish_info = list()
for(var/_fish_behaviour in subtypesof(/datum/aquarium_behaviour/fish))
var/datum/aquarium_behaviour/fish/fish_behaviour = _fish_behaviour
var/list/fish_data = list()
if(!initial(fish_behaviour.show_in_catalog))
continue
fish_data["name"] = initial(fish_behaviour.name)
fish_data["desc"] = initial(fish_behaviour.desc)
fish_data["fluid"] = initial(fish_behaviour.required_fluid_type)
fish_data["temp_min"] = initial(fish_behaviour.required_temperature_min)
fish_data["temp_max"] = initial(fish_behaviour.required_temperature_max)
fish_data["icon"] = sanitize_css_class_name("[initial(fish_behaviour.icon)][initial(fish_behaviour.icon_state)]")
fish_data["color"] = initial(fish_behaviour.color)
fish_data["source"] = initial(fish_behaviour.availible_in_random_cases) ? "[AQUARIUM_COMPANY] Fish Packs" : "Unknown"
var/datum/reagent/food_type = initial(fish_behaviour.food)
if(food_type != /datum/reagent/consumable/nutriment)
fish_data["feed"] = initial(food_type.name)
else
fish_data["feed"] = "[AQUARIUM_COMPANY] Fish Feed"
fish_info += list(fish_data)
.["fish_info"] = fish_info
.["sponsored_by"] = AQUARIUM_COMPANY
/obj/item/book/fish_catalog/ui_assets(mob/user)
return list(
get_asset_datum(/datum/asset/spritesheet/fish)
)
/obj/item/aquarium_kit
name = "DIY Aquarium Construction Kit"
desc = "Everything you need to build your own aquarium.(Raw materials sold separately)"
icon = 'icons/obj/aquarium.dmi'
icon_state = "construction_kit"
w_class = WEIGHT_CLASS_TINY
/obj/item/aquarium_kit/attack_self(mob/user)
. = ..()
to_chat(user,"<span class='notice'>There's instruction and tools necessary to build aquarium inside. All you need is to start crafting.</span>")
/obj/item/aquarium_prop
name = "generic aquarium prop"
desc = "very boring"
w_class = WEIGHT_CLASS_TINY
/obj/item/storage/box/aquarium_props
name = "aquarium props box"
desc = "All you need to make your aquarium look good"
/obj/item/storage/box/aquarium_props/PopulateContents()
for(var/prop_type in subtypesof(/datum/aquarium_behaviour/prop))
generate_fish(src, prop_type, /obj/item/aquarium_prop)
/obj/item/storage/box/fish_debug
name = "box full of fish"
/obj/item/storage/box/fish_debug/PopulateContents()
for(var/fish_type in subtypesof(/datum/aquarium_behaviour/fish))
generate_fish(src,fish_type)
#undef AQUARIUM_COMPANY
@@ -516,3 +516,22 @@
assets = list(
"safe_dial.png" = 'html/safe_dial.png'
)
/datum/asset/spritesheet/fish
name = "fish"
/datum/asset/spritesheet/fish/register()
for (var/path in subtypesof(/datum/aquarium_behaviour/fish))
var/datum/aquarium_behaviour/fish/fish_type = path
var/fish_icon = initial(fish_type.icon)
var/fish_icon_state = initial(fish_type.icon_state)
var/id = sanitize_css_class_name("[fish_icon][fish_icon_state]")
if(sprites[id]) //no dupes
continue
Insert(id, fish_icon, fish_icon_state)
..()
/// Removes all non-alphanumerics from the text, keep in mind this can lead to id conflicts
/proc/sanitize_css_class_name(name)
var/static/regex/regex = new(@"[^a-zA-Z0-9]","g")
return replacetext(name, regex, "")
+21
View File
@@ -2364,6 +2364,27 @@
crate_name = "art supply crate"
crate_type = /obj/structure/closet/crate/wooden
/datum/supply_pack/misc/aquarium_kit
name = "Aquarium Kit"
desc = "Everything you need to start your own aquarium. Contains aquarium construction kit, fish catalog, feed can and three freshwater fish from our collection."
cost = CARGO_CRATE_VALUE * 10
contains = list(/obj/item/book/fish_catalog,
/obj/item/storage/fish_case/random/freshwater,
/obj/item/storage/fish_case/random/freshwater,
/obj/item/storage/fish_case/random/freshwater,
/obj/item/fish_feed,
/obj/item/storage/box/aquarium_props,
/obj/item/aquarium_kit)
crate_name = "aquarium kit crate"
crate_type = /obj/structure/closet/crate/wooden
/datum/supply_pack/misc/aquarium_fish
name = "Aquarium Fish Case"
desc = "An aquarium fish handpicked by monkeys from our collection."
cost = CARGO_CRATE_VALUE * 2
contains = list(/obj/item/storage/fish_case/random)
crate_name = "aquarium fish crate"
/datum/supply_pack/misc/bicycle
name = "Bicycle"
desc = "Nanotrasen reminds all employees to never toy with powers outside their control."
+5 -2
View File
@@ -225,10 +225,13 @@
/obj/item/book/attack_self(mob/user)
if(!user.can_read(src))
return
user.visible_message("<span class='notice'>[user] opens a book titled \"[title]\" and begins reading intently.</span>")
SEND_SIGNAL(user, COMSIG_ADD_MOOD_EVENT, "book_nerd", /datum/mood_event/book_nerd)
on_read(user)
/obj/item/book/proc/on_read(mob/user)
if(dat)
user << browse("<TT><I>Penned by [author].</I></TT> <BR>" + "[dat]", "window=book[window_size != null ? ";size=[window_size]" : ""]")
user.visible_message("<span class='notice'>[user] opens a book titled \"[title]\" and begins reading intently.</span>")
SEND_SIGNAL(user, COMSIG_ADD_MOOD_EVENT, "book_nerd", /datum/mood_event/book_nerd)
onclose(user, "book")
else
to_chat(user, "<span class='notice'>This book is completely blank!</span>")