mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-26 06:29:23 +01:00
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:
co-authored by
tralezab
Mothblocks
Aleksej Komarov
Qustinnus
coiax
parent
2f2c9a5fa5
commit
8c69586774
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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, "")
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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>")
|
||||
|
||||
Reference in New Issue
Block a user