Adds aquariums and aquarium fish. (#56343) (#2920)

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>

Co-authored-by: AnturK <AnturK@users.noreply.github.com>
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:
SkyratBot
2021-01-28 03:27:25 +01:00
committed by GitHub
parent 8c3d2811c2
commit 3000d75ad5
24 changed files with 1285 additions and 4 deletions
+27
View File
@@ -0,0 +1,27 @@
#define AQUARIUM_ANIMATION_FISH_SWIM "fish"
#define AQUARIUM_ANIMATION_FISH_DEAD "dead"
#define AQUARIUM_PROPERTIES_PX_MIN "px_min"
#define AQUARIUM_PROPERTIES_PX_MAX "px_max"
#define AQUARIUM_PROPERTIES_PY_MIN "py_min"
#define AQUARIUM_PROPERTIES_PY_MAX "py_max"
#define AQUARIUM_LAYER_MODE_BOTTOM "bottom"
#define AQUARIUM_LAYER_MODE_TOP "top"
#define AQUARIUM_LAYER_MODE_AUTO "auto"
#define FISH_ALIVE "alive"
#define FISH_DEAD "dead"
#define MIN_AQUARIUM_TEMP T0C
#define MAX_AQUARIUM_TEMP T0C + 100
#define FISH_RARITY_BASIC 1000
#define FISH_RARITY_RARE 400
#define FISH_RARITY_VERY_RARE 200
#define FISH_RARITY_GOOD_LUCK_FINDING_THIS 1
#define AQUARIUM_FLUID_FRESHWATER "Freshwater"
#define AQUARIUM_FLUID_SALTWATER "Saltwater"
#define AQUARIUM_FLUID_SULPHWATEVER "Sulphuric Water"
#define AQUARIUM_FLUID_AIR "Air"
+6
View File
@@ -1000,3 +1000,9 @@
#define COMSIG_HUMAN_EARLY_UNARMED_ATTACK "human_early_unarmed_attack"
///from mob/living/carbon/human/UnarmedAttack(): (atom/target, proximity)
#define COMSIG_HUMAN_MELEE_UNARMED_ATTACK "human_melee_unarmed_attack"
// Aquarium related signals
#define COMSIG_AQUARIUM_BEFORE_INSERT_CHECK "aquarium_about_to_be_inserted"
#define COMSIG_AQUARIUM_SURFACE_CHANGED "aquarium_surface_changed"
#define COMSIG_AQUARIUM_FLUID_CHANGED "aquarium_fluid_changed"
+2
View File
@@ -318,6 +318,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define TRAIT_NO_TELEPORT "no-teleport" //you just can't
#define TRAIT_FOOD_GRILLED "food_grilled"
#define TRAIT_NEEDS_TWO_HANDS "needstwohands" //The items needs two hands to be carried
#define TRAIT_FISH_SAFE_STORAGE "fish_case" //Fish in this won't die
#define TRAIT_FISH_CASE_COMPATIBILE "fish_case_compatibile" //Stuff that can go inside fish cases
//quirk traits
#define TRAIT_ALCOHOL_TOLERANCE "alcohol_tolerance"
+212
View File
@@ -0,0 +1,212 @@
/// Allows movables to be inserted/displayed in aquariums.
/datum/component/aquarium_content
/// This is a datum that describes our in-aquarium functionality
var/datum/aquarium_behaviour/properties
/// Keeps track of our current aquarium.
var/obj/structure/aquarium/current_aquarium
//This is visual effect holder that will end up in aquarium's vis_contents
var/obj/effect/vc_obj
/// Base px offset of the visual object in current aquarium aka current base position
var/base_px = 0
/// Base px offset of the visual object in current aquarium aka current base position
var/base_py = 0
//Current layer for the visual object
var/base_layer
/datum/component/aquarium_content/Initialize(property_type)
if(!ismovable(parent))
return COMPONENT_INCOMPATIBLE
if(ispath(property_type, /datum/aquarium_behaviour))
properties = new property_type
else
CRASH("Invalid property type provided for aquarium content component")
properties.parent = src
ADD_TRAIT(parent, TRAIT_FISH_CASE_COMPATIBILE, src)
RegisterSignal(parent, COMSIG_MOVABLE_MOVED, .proc/enter_aquarium)
/datum/component/aquarium_content/PreTransfer()
. = ..()
REMOVE_TRAIT(parent, TRAIT_FISH_CASE_COMPATIBILE, src)
/datum/component/aquarium_content/Destroy(force, silent)
if(current_aquarium)
remove_from_aquarium()
QDEL_NULL(vc_obj)
QDEL_NULL(properties)
return ..()
/datum/component/aquarium_content/proc/enter_aquarium(datum/source, OldLoc, Dir, Forced)
SIGNAL_HANDLER
var/atom/movable/movable_parent = parent
if(istype(movable_parent.loc, /obj/structure/aquarium))
on_inserted(movable_parent.loc)
if(HAS_TRAIT(movable_parent.loc, TRAIT_FISH_SAFE_STORAGE))
on_tank_stasis()
/datum/component/aquarium_content/proc/is_ready_to_insert(obj/structure/aquarium/aquarium)
//This is kinda awful but we're unaware of other fish
if(properties.unique)
for(var/atom/movable/fish_or_prop in aquarium)
if(fish_or_prop == parent)
continue
var/datum/component/aquarium_content/other_content = fish_or_prop.GetComponent(/datum/component/aquarium_content)
if(other_content && other_content.properties.type == properties.type)
return FALSE
return TRUE
/datum/component/aquarium_content/proc/on_inserted(atom/aquarium)
current_aquarium = aquarium
RegisterSignal(current_aquarium, COMSIG_ATOM_EXITED, .proc/on_removed)
RegisterSignal(current_aquarium, COMSIG_AQUARIUM_SURFACE_CHANGED, .proc/on_surface_changed)
RegisterSignal(current_aquarium, COMSIG_AQUARIUM_FLUID_CHANGED,.proc/on_fluid_changed)
RegisterSignal(current_aquarium, COMSIG_PARENT_ATTACKBY, .proc/attack_reaction)
properties.on_inserted()
//If we don't have vc object yet build it
if(!vc_obj)
vc_obj = generate_base_vc()
//Set default position and layer
set_vc_base_position()
generate_animation()
//Finally add it to to objects vis_contents
current_aquarium.vis_contents |= vc_obj
/// Aquarium surface changed in some way, we need to recalculate base position and aninmation
/datum/component/aquarium_content/proc/on_surface_changed()
SIGNAL_HANDLER
set_vc_base_position()
generate_animation() //our animation start point changed, gotta redo
/// Our aquarium is hit with stuff
/datum/component/aquarium_content/proc/attack_reaction(datum/source, obj/item/thing, mob/user, params)
SIGNAL_HANDLER
if(istype(thing, /obj/item/fish_feed))
properties.on_feeding(thing.reagents)
return COMPONENT_NO_AFTERATTACK
else
//stirred effect
generate_animation()
/datum/component/aquarium_content/proc/on_fluid_changed()
SIGNAL_HANDLER
properties.on_fluid_changed()
/datum/component/aquarium_content/proc/remove_visual_from_aquarium()
current_aquarium.vis_contents -= vc_obj
if(base_layer)
current_aquarium.free_layer(base_layer)
/// Generates common visual object, propeties that don't depend on aquarium surface
/datum/component/aquarium_content/proc/generate_base_vc()
if(!properties)
CRASH("Generating visual without properties.")
var/obj/effect/visual = new
properties.apply_appearance(visual)
visual.vis_flags |= VIS_INHERIT_ID | VIS_INHERIT_PLANE //plane so it shows properly in containers on inventory ui for handheld cases
return visual
/// Actually animates the vc holder
/datum/component/aquarium_content/proc/generate_animation()
switch(properties.current_animation)
if(AQUARIUM_ANIMATION_FISH_SWIM)
swim_animation()
return
if(AQUARIUM_ANIMATION_FISH_DEAD)
dead_animation()
return
/// Create looping random path animation, pixel offsets parameters include offsets already
/datum/component/aquarium_content/proc/swim_animation()
var/avg_width = round(properties.sprite_width / 2)
var/avg_height = round(properties.sprite_height / 2)
var/list/aq_properties = current_aquarium.get_surface_properties()
var/px_min = aq_properties[AQUARIUM_PROPERTIES_PX_MIN] + avg_width - 16
var/px_max = aq_properties[AQUARIUM_PROPERTIES_PX_MAX] - avg_width - 16
var/py_min = aq_properties[AQUARIUM_PROPERTIES_PY_MIN] + avg_height - 16
var/py_max = aq_properties[AQUARIUM_PROPERTIES_PY_MAX] - avg_width - 16
var/origin_x = base_px
var/origin_y = base_py
var/prev_x = origin_x
var/prev_y = origin_y
animate(vc_obj, pixel_x = origin_x, time = 0, loop = -1) //Just to start the animation
var/move_number = rand(3, 5) //maybe unhardcode this
for(var/i in 1 to move_number)
//If it's last movement, move back to start otherwise move to some random point
var/target_x = i == move_number ? origin_x : rand(px_min,px_max) //could do with enforcing minimal delta for prettier zigzags
var/target_y = i == move_number ? origin_y : rand(py_min,py_max)
var/dx = prev_x - target_x
var/dy = prev_y - target_y
prev_x = target_x
prev_y = target_y
var/dist = abs(dx) + abs(dy)
var/eyeballed_time = dist * 2 //2ds per px
//Face the direction we're going
var/matrix/dir_mx = properties.base_transform()
if(dx <= 0) //assuming default sprite is facing left here
dir_mx.Scale(-1, 1)
animate(transform = dir_mx, time = 0, loop = -1)
animate(pixel_x = target_x, pixel_y = target_y, time = eyeballed_time, loop = -1)
/datum/component/aquarium_content/proc/dead_animation()
//Set base_py to lowest possible value
var/avg_height = round(properties.sprite_height / 2)
var/list/aq_properties = current_aquarium.get_surface_properties()
var/py_min = aq_properties[AQUARIUM_PROPERTIES_PY_MIN] + avg_height - 16
base_py = py_min
animate(vc_obj, pixel_y = py_min, time = 1) //flop to bottom and end current animation.
/// Floating to top animation.
/datum/component/aquarium_content/proc/float_animation()
return
/datum/component/aquarium_content/proc/set_vc_base_position()
var/list/aq_properties = current_aquarium.get_surface_properties()
if(properties.randomize_position)
var/avg_width = round(properties.sprite_width / 2)
var/avg_height = round(properties.sprite_height / 2)
var/px_min = aq_properties[AQUARIUM_PROPERTIES_PX_MIN] + avg_width - 16
var/px_max = aq_properties[AQUARIUM_PROPERTIES_PX_MAX] - avg_width - 16
var/py_min = aq_properties[AQUARIUM_PROPERTIES_PY_MIN] + avg_height - 16
var/py_max = aq_properties[AQUARIUM_PROPERTIES_PY_MAX] - avg_width - 16
base_px = rand(px_min,px_max)
base_py = rand(py_min,py_max)
vc_obj.pixel_x = base_px
vc_obj.pixel_y = base_py
if(base_layer)
current_aquarium.free_layer(base_layer)
base_layer = current_aquarium.request_layer(properties.layer_mode)
vc_obj.layer = base_layer
/datum/component/aquarium_content/proc/on_removed(datum/source, atom/movable/mover)
SIGNAL_HANDLER
if(mover != parent)
return
remove_from_aquarium()
/datum/component/aquarium_content/proc/remove_from_aquarium()
properties.before_removal()
UnregisterSignal(current_aquarium, list(COMSIG_AQUARIUM_SURFACE_CHANGED, COMSIG_AQUARIUM_FLUID_CHANGED, COMSIG_PARENT_ATTACKBY, COMSIG_ATOM_EXITED))
remove_visual_from_aquarium()
current_aquarium = null
//We do not stop processing properties here. We want fish to die outside of aquariums after first insert. We only stop processing in properties.death or destroy
/datum/component/aquarium_content/proc/on_tank_stasis()
// Stop processing until inserted into aquarium again.
STOP_PROCESSING(SSobj, properties)
@@ -1076,3 +1076,13 @@
/obj/item/grenade/gas_crystal/zauker_crystal = 1
)
category = CAT_MISC
/datum/crafting_recipe/aquarium
name = "Aquarium"
result = /obj/structure/aquarium
time = 10 SECONDS
reqs = list(/obj/item/stack/sheet/metal = 15,
/obj/item/stack/sheet/glass = 10,
/obj/item/aquarium_kit = 1
)
category = CAT_MISC
@@ -0,0 +1,10 @@
/datum/component/storage/concrete/fish_case
max_items = 1
can_hold_trait = TRAIT_FISH_CASE_COMPATIBILE
can_hold_description = "fish and aquarium equipment"
/datum/component/storage/concrete/fish_case/can_be_inserted(obj/item/I, stop_messages, mob/M)
/// Activate deferred components if any.
SEND_SIGNAL(I, COMSIG_AQUARIUM_BEFORE_INSERT_CHECK)
. = ..()
+4 -2
View File
@@ -8,7 +8,9 @@
var/list/can_hold //if this is set, only items, and their children, will fit
var/list/cant_hold //if this is set, items, and their children, won't fit
var/list/exception_hold //if set, these items will be the exception to the max size of object that can fit.
var/list/exception_hold //if set, these items will be the exception to the max size of object that can fit.
/// If set can only contain stuff with this single trait present.
var/list/can_hold_trait
var/can_hold_description
@@ -630,7 +632,7 @@
if(!stop_messages)
to_chat(M, "<span class='warning'>[host] cannot hold [I]!</span>")
return FALSE
if(is_type_in_typecache(I, cant_hold) || HAS_TRAIT(I, TRAIT_NO_STORAGE_INSERT)) //Items which this container can't hold.
if(is_type_in_typecache(I, cant_hold) || HAS_TRAIT(I, TRAIT_NO_STORAGE_INSERT) || (can_hold_trait && !HAS_TRAIT(I, can_hold_trait))) //Items which this container can't hold.
if(!stop_messages)
to_chat(M, "<span class='warning'>[host] cannot hold [I]!</span>")
return FALSE
@@ -0,0 +1,27 @@
/**
* Create /datum/component/aquarium_content with the preset path on the target right before being inserted into aquarium and deletes itself.
* Used to save memory from aquarium properties on common objects/stacks that won't see aquarium in 99 out of 100 rounds.
*/
/datum/element/deferred_aquarium_content
element_flags = ELEMENT_BESPOKE
id_arg_index = 2
var/aquarium_content_type
/datum/element/deferred_aquarium_content/Attach(datum/target, aquarium_content_type)
. = ..()
if(!ismovable(target))
return ELEMENT_INCOMPATIBLE
if(!aquarium_content_type)
CRASH("Deferred aquarium content missing behaviour type.")
src.aquarium_content_type = aquarium_content_type
RegisterSignal(target, COMSIG_AQUARIUM_BEFORE_INSERT_CHECK, .proc/create_aquarium_component)
/datum/element/deferred_aquarium_content/Detach(datum/target)
. = ..()
UnregisterSignal(target, COMSIG_AQUARIUM_BEFORE_INSERT_CHECK)
/datum/element/deferred_aquarium_content/proc/create_aquarium_component(datum/source)
SIGNAL_HANDLER
source.AddComponent(/datum/component/aquarium_content, aquarium_content_type)
Detach(source)
@@ -305,3 +305,8 @@
description = "<span class='warning'>OW!! That was even worse than a regular noogie!</span>\n"
mood_change = -4
timeout = 1 MINUTES
/datum/mood_event/aquarium_negative
description = "<span class='warning'>All the fish are dead...</span>\n"
mood_change = -3
timeout = 1.5 MINUTES
@@ -218,3 +218,8 @@
description = "<span class='nicegreen'>HA! What a rube, they never stood a chance...</span>\n"
mood_change = 4
timeout = 1.5 MINUTES
/datum/mood_event/aquarium_positive
description = "<span class='nicegreen'>Watching fish in aquarium is calming.</span>\n"
mood_change = 3
timeout = 1.5 MINUTES
+5
View File
@@ -34,6 +34,11 @@
name = "imitation carp fillet"
desc = "Almost just like the real thing, kinda."
/obj/item/food/carpmeat/icantbeliveitsnotcarp
name = "fish fillet"
desc = "A fillet of unspecified fish meat."
food_reagents = list(/datum/reagent/consumable/nutriment/protein = 4, /datum/reagent/consumable/nutriment/vitamin = 2) //No carpotoxin
/obj/item/food/fishfingers
name = "fish fingers"
desc = "A finger of fish."
+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
@@ -2373,6 +2373,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>")
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 78 KiB

+8
View File
@@ -28,6 +28,7 @@
#include "code\__DEFINES\admin.dm"
#include "code\__DEFINES\ai.dm"
#include "code\__DEFINES\antagonists.dm"
#include "code\__DEFINES\aquarium.dm"
#include "code\__DEFINES\atmospherics.dm"
#include "code\__DEFINES\atom_hud.dm"
#include "code\__DEFINES\bitfields.dm"
@@ -441,6 +442,7 @@
#include "code\datums\components\_component.dm"
#include "code\datums\components\acid.dm"
#include "code\datums\components\anti_magic.dm"
#include "code\datums\components\aquarium.dm"
#include "code\datums\components\areabound.dm"
#include "code\datums\components\armor_plate.dm"
#include "code\datums\components\bane.dm"
@@ -544,6 +546,7 @@
#include "code\datums\components\storage\concrete\_concrete.dm"
#include "code\datums\components\storage\concrete\bag_of_holding.dm"
#include "code\datums\components\storage\concrete\bluespace.dm"
#include "code\datums\components\storage\concrete\fish_case.dm"
#include "code\datums\components\storage\concrete\implant.dm"
#include "code\datums\components\storage\concrete\pockets.dm"
#include "code\datums\components\storage\concrete\rped.dm"
@@ -617,6 +620,7 @@
#include "code\datums\elements\caltrop.dm"
#include "code\datums\elements\cleaning.dm"
#include "code\datums\elements\climbable.dm"
#include "code\datums\elements\deferred_aquarium_content.dm"
#include "code\datums\elements\digitalcamo.dm"
#include "code\datums\elements\dryable.dm"
#include "code\datums\elements\earhealing.dm"
@@ -1676,6 +1680,10 @@
#include "code\modules\antagonists\wizard\equipment\soulstone.dm"
#include "code\modules\antagonists\wizard\equipment\spellbook.dm"
#include "code\modules\antagonists\xeno\xeno.dm"
#include "code\modules\aquarium\aquarium.dm"
#include "code\modules\aquarium\aquarium_behaviour.dm"
#include "code\modules\aquarium\fish.dm"
#include "code\modules\aquarium\misc.dm"
#include "code\modules\assembly\assembly.dm"
#include "code\modules\assembly\bomb.dm"
#include "code\modules\assembly\doorcontrol.dm"
+59
View File
@@ -0,0 +1,59 @@
import { useBackend } from '../backend';
import { Button, Dropdown, Knob, LabeledControls, Section } from '../components';
import { Window } from '../layouts';
export const Aquarium = (props, context) => {
const { act, data } = useBackend(context);
const {
temperature,
fluid_type,
minTemperature,
maxTemperature,
fluidTypes,
contents,
} = data;
return (
<Window
width={450}
height={400}
resizable>
<Window.Content>
<Section title="Aquarium Controls">
<LabeledControls>
<LabeledControls.Item label="Temperature">
<Knob
size={1.25}
mb={1}
value={temperature}
unit="K"
minValue={minTemperature}
maxValue={maxTemperature}
step={1}
stepPixelSize={1}
onDrag={(e, value) => act('temperature', {
temperature: value,
})} />
</LabeledControls.Item>
<LabeledControls.Item label="Fluid">
{fluidTypes.map(f => (
<Button
key={f}
content={f}
selected={fluid_type === f}
onClick={() => act('fluid', { fluid: f })} />
))}
</LabeledControls.Item>
</LabeledControls>
</Section>
<Section title="Contents">
{contents.map(movable => (
<Button
key={movable.ref}
content={movable.name}
onClick={() => act('remove', { ref: movable.ref })} />
))}
</Section>
</Window.Content>
</Window>
);
};
@@ -0,0 +1,82 @@
import { sortBy } from 'common/collections';
import { flow } from 'common/fp';
import { classes } from 'common/react';
import { useBackend, useLocalState } from '../backend';
import { Box, Button, LabeledList, Section, Stack } from '../components';
import { Window } from '../layouts';
import { capitalize } from 'common/string';
export const FishCatalog = (props, context) => {
const { act, data } = useBackend(context);
const {
fish_info,
sponsored_by,
} = data;
const fish_by_name = flow([
sortBy(fish => fish.name),
])(data.fish_info || []);
const [
currentFish,
setCurrentFish,
] = useLocalState(context, 'currentFish', null);
return (
<Window
width={500}
height={300}
resizable>
<Window.Content>
<Stack fill>
<Stack.Item width="120px">
<Section fill scrollable>
{fish_by_name.map(f => (
<Button
key={f.name}
fluid
color="transparent"
selected={f === currentFish}
onClick={() => { setCurrentFish(f); }}>
{f.name}
</Button>
))}
</Section>
</Stack.Item>
<Stack.Item grow basis={0}>
<Section
fill
scrollable
title={currentFish
? capitalize(currentFish.name)
: sponsored_by + " Fish Index"}>
{currentFish && (
<LabeledList>
<LabeledList.Item label="Description">
{currentFish.desc}
</LabeledList.Item>
<LabeledList.Item label="Water type">
{currentFish.fluid}
</LabeledList.Item>
<LabeledList.Item label="Temperature">
{currentFish.temp_min} to {currentFish.temp_max}
</LabeledList.Item>
<LabeledList.Item label="Feeding">
{currentFish.feed}
</LabeledList.Item>
<LabeledList.Item label="Acquisition">
{currentFish.source}
</LabeledList.Item>
<LabeledList.Item label="Illustration">
<Box
className={classes([
'fish32x32',
currentFish.icon,
])} />
</LabeledList.Item>
</LabeledList>
)}
</Section>
</Stack.Item>
</Stack>
</Window.Content>
</Window>
);
};