Fish refactor. (#65785)

Refactors fish into proper paths.
Removes aquarium behaviour intermediary datums.
Moves fish functionality out of aquarium content component.
Fixes flopping animation resetting on dropping.

Simplifies everything. There should be no player facing changes.

The animate signal is kinda weak method of solving the animate queue issue but it seemed least intrusive to me.
Open to any better ideas.
This commit is contained in:
AnturK
2022-03-31 14:05:55 +13:00
committed by GitHub
parent 36091efb3e
commit fbba5abb72
14 changed files with 581 additions and 551 deletions
@@ -82,4 +82,5 @@
///When the transform or an atom is varedited through vv topic.
#define COMSIG_ATOM_VV_MODIFY_TRANSFORM "atom_vv_modify_transform"
/// generally called before temporary non-parallel animate()s on the atom (animation_duration)
#define COMSIG_ATOM_TEMPORARY_ANIMATION_START "atom_temp_animate_start"
@@ -0,0 +1,7 @@
// Aquarium related signals
#define COMSIG_AQUARIUM_SURFACE_CHANGED "aquarium_surface_changed"
#define COMSIG_AQUARIUM_FLUID_CHANGED "aquarium_fluid_changed"
// Fish signals
#define COMSIG_FISH_STATUS_CHANGED "fish_status_changed"
#define COMSIG_FISH_STIRRED "fish_stirred"
@@ -372,10 +372,6 @@
#define COMSIG_ITEM_AFTERATTACK_SECONDARY "item_afterattack_secondary"
///from base of obj/item/attack_qdeleted(): (atom/target, mob/user, params)
#define COMSIG_ITEM_ATTACK_QDELETED "item_attack_qdeleted"
// 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"
///from /obj/item/assembly/proc/pulsed()
#define COMSIG_ASSEMBLY_PULSED "assembly_pulsed"
+154 -115
View File
@@ -1,8 +1,5 @@
/// 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
@@ -16,17 +13,69 @@
//Current layer for the visual object
var/base_layer
//If flopping animation was applied to parent, tracked to stop it on removal/destroy
var/flopping = FALSE
/datum/component/aquarium_content/Initialize(property_type)
/**
* 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)
*/
/// Icon used for in aquarium sprite
var/icon = 'icons/obj/aquarium.dmi'
/// 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/icon_state
/// Applied to vc object only for use with greyscaled icons.
var/aquarium_vc_color
/// Transformation applied to the visual holder - used when scaled down sprites are used as in aquarium visual
var/matrix/base_transform
/// How the thing will be layered
var/layer_mode = AQUARIUM_LAYER_MODE_AUTO
/// If the starting position is randomised within bounds when inserted into aquarium.
var/randomize_position = FALSE
//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
/// Currently playing animation
var/current_animation
/// Does this behviour need additional processing in aquarium, will be added to SSobj processing on insertion
var/processing = FALSE
/// TODO: Change this into trait checked on aquarium insertion
var/unique = FALSE
/// Proc used to retrieve current animation state from the parent, optional
var/animation_getter
/// Signals of the parent that will trigger animation update
var/animation_update_signals
/datum/component/aquarium_content/Initialize(animation_getter, animation_update_signals)
if(!ismovable(parent))
return COMPONENT_INCOMPATIBLE
if(ispath(property_type, /datum/aquarium_behaviour))
properties = new property_type
src.animation_getter = animation_getter
src.animation_update_signals = animation_update_signals
if(animation_update_signals)
RegisterSignal(parent, animation_update_signals, .proc/generate_animation)
if(istype(parent,/obj/item/fish))
InitializeFromFish()
else if(istype(parent,/obj/item/aquarium_prop))
InitializeFromProp()
else
CRASH("Invalid property type provided for aquarium content component")
properties.parent = src
InitializeOther()
ADD_TRAIT(parent, TRAIT_FISH_CASE_COMPATIBILE, src)
RegisterSignal(parent, COMSIG_MOVABLE_MOVED, .proc/enter_aquarium)
@@ -36,6 +85,53 @@
if(istype(movable_parent.loc, /obj/structure/aquarium))
on_inserted(movable_parent.loc)
/// Sets visuals properties for fish
/datum/component/aquarium_content/proc/InitializeFromFish()
var/obj/item/fish/fish = parent
icon = fish.icon
sprite_height = fish.sprite_height
sprite_width = fish.sprite_width
aquarium_vc_color = fish.aquarium_vc_color
if(fish.dedicated_in_aquarium_icon_state)
icon_state = fish.dedicated_in_aquarium_icon_state
base_transform = matrix()
else
icon_state = fish.icon_state
var/matrix/matrix = matrix()
var/x_scale = fish.sprite_width / fish.source_width
var/y_scale = fish.sprite_height / fish.source_height
matrix.Scale(x_scale, y_scale)
base_transform = matrix
randomize_position = TRUE
/// Sets visuals properties for fish
/datum/component/aquarium_content/proc/InitializeFromProp()
var/obj/item/aquarium_prop/prop = parent
icon = prop.icon
icon_state = prop.icon_state
layer_mode = prop.layer_mode
sprite_height = 32
sprite_width = 32
base_transform = matrix()
unique = TRUE
/// Mostly for admin abuse
/datum/component/aquarium_content/proc/InitializeOther()
sprite_width = 8
sprite_height = 8
var/matrix/matrix = matrix()
var/x_scale = sprite_width / 32
var/y_scale = sprite_height / 32
matrix.Scale(x_scale, y_scale)
base_transform = matrix
/datum/component/aquarium_content/PreTransfer()
. = ..()
REMOVE_TRAIT(parent, TRAIT_FISH_CASE_COMPATIBILE, src)
@@ -43,9 +139,7 @@
/datum/component/aquarium_content/Destroy(force, silent)
if(current_aquarium)
remove_from_aquarium()
stop_flopping()
QDEL_NULL(vc_obj)
QDEL_NULL(properties)
return ..()
/datum/component/aquarium_content/proc/enter_aquarium(datum/source, OldLoc, Dir, Forced)
@@ -53,17 +147,14 @@
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)
if(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)
if(fish_or_prop.type == parent.type)
return FALSE
return TRUE
@@ -72,8 +163,9 @@
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(processing)
START_PROCESSING(SSobj, src)
//If we don't have vc object yet build it
if(!vc_obj)
@@ -81,7 +173,7 @@
//Set default position and layer
set_vc_base_position()
generate_animation()
generate_animation(reset = TRUE)
//Finally add it to to objects vis_contents
current_aquarium.vis_contents |= vc_obj
@@ -90,21 +182,11 @@
/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()
generate_animation(reset = TRUE) //our animation start point changed, gotta redo
/datum/component/aquarium_content/proc/on_fluid_changed()
SIGNAL_HANDLER
properties.on_fluid_changed()
generate_animation()
/datum/component/aquarium_content/proc/remove_visual_from_aquarium()
current_aquarium.vis_contents -= vc_obj
@@ -113,17 +195,29 @@
/// 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)
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
/// Applies icon,color and base scaling to our visual holder
/datum/component/aquarium_content/proc/apply_appearance(obj/effect/holder)
holder.icon = icon
holder.icon_state = icon_state
holder.transform = matrix(base_transform)
if(aquarium_vc_color)
holder.color = aquarium_vc_color
/// Actually animates the vc holder
/datum/component/aquarium_content/proc/generate_animation()
switch(properties.current_animation)
/datum/component/aquarium_content/proc/generate_animation(reset=FALSE)
if(!current_aquarium)
return
var/next_animation = animation_getter ? call(parent,animation_getter)() : null
if(current_animation == next_animation && !reset)
return
current_animation = next_animation
switch(current_animation)
if(AQUARIUM_ANIMATION_FISH_SWIM)
swim_animation()
return
@@ -134,8 +228,8 @@
/// 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/avg_width = round(sprite_width / 2)
var/avg_height = round(sprite_height / 2)
var/list/aq_properties = current_aquarium.get_surface_properties()
var/px_min = aq_properties[AQUARIUM_PROPERTIES_PX_MIN] + avg_width - 16
@@ -160,7 +254,7 @@
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()
var/matrix/dir_mx = matrix(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)
@@ -168,85 +262,35 @@
/datum/component/aquarium_content/proc/dead_animation()
//Set base_py to lowest possible value
var/avg_height = round(properties.sprite_height / 2)
var/avg_height = round(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.
#define PAUSE_BETWEEN_PHASES 15
#define PAUSE_BETWEEN_FLOPS 2
#define FLOP_COUNT 2
#define FLOP_DEGREE 20
#define FLOP_SINGLE_MOVE_TIME 1.5
#define JUMP_X_DISTANCE 5
#define JUMP_Y_DISTANCE 6
/// This animation should be applied to actual parent atom instead of vc_object.
/proc/flop_animation(atom/movable/animation_target)
var/pause_between = PAUSE_BETWEEN_PHASES + rand(1, 5) //randomized a bit so fish are not in sync
animate(animation_target, time = pause_between, loop = -1)
//move nose down and up
for(var/_ in 1 to FLOP_COUNT)
var/matrix/up_matrix = matrix()
up_matrix.Turn(FLOP_DEGREE)
var/matrix/down_matrix = matrix()
down_matrix.Turn(-FLOP_DEGREE)
animate(transform = down_matrix, time = FLOP_SINGLE_MOVE_TIME, loop = -1)
animate(transform = up_matrix, time = FLOP_SINGLE_MOVE_TIME, loop = -1)
animate(transform = matrix(), time = FLOP_SINGLE_MOVE_TIME, loop = -1, easing = BOUNCE_EASING | EASE_IN)
animate(time = PAUSE_BETWEEN_FLOPS, loop = -1)
//bounce up and down
animate(time = pause_between, loop = -1, flags = ANIMATION_PARALLEL)
var/jumping_right = FALSE
var/up_time = 3 * FLOP_SINGLE_MOVE_TIME / 2
for(var/_ in 1 to FLOP_COUNT)
jumping_right = !jumping_right
var/x_step = jumping_right ? JUMP_X_DISTANCE/2 : -JUMP_X_DISTANCE/2
animate(time = up_time, pixel_y = JUMP_Y_DISTANCE , pixel_x=x_step, loop = -1, flags= ANIMATION_RELATIVE, easing = BOUNCE_EASING | EASE_IN)
animate(time = up_time, pixel_y = -JUMP_Y_DISTANCE, pixel_x=x_step, loop = -1, flags= ANIMATION_RELATIVE, easing = BOUNCE_EASING | EASE_OUT)
animate(time = PAUSE_BETWEEN_FLOPS, loop = -1)
#undef PAUSE_BETWEEN_PHASES
#undef PAUSE_BETWEEN_FLOPS
#undef FLOP_COUNT
#undef FLOP_DEGREE
#undef FLOP_SINGLE_MOVE_TIME
#undef JUMP_X_DISTANCE
#undef JUMP_Y_DISTANCE
/// Starts flopping animation
/datum/component/aquarium_content/proc/start_flopping()
if(!flopping && istype(parent,/obj/item/fish)) //Requires update_transform/animate_wrappers to be less restrictive.
flopping = TRUE
flop_animation(parent)
/// Stops flopping animation
/datum/component/aquarium_content/proc/stop_flopping()
if(flopping)
flopping = FALSE
animate(parent, transform = matrix()) //stop animation
/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(randomize_position)
randomize_base_position()
if(base_layer)
current_aquarium.free_layer(base_layer)
base_layer = current_aquarium.request_layer(properties.layer_mode)
base_layer = current_aquarium.request_layer(layer_mode)
vc_obj.layer = base_layer
/datum/component/aquarium_content/proc/randomize_base_position()
var/list/aq_properties = current_aquarium.get_surface_properties()
var/avg_width = round(sprite_width / 2)
var/avg_height = round(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
/datum/component/aquarium_content/proc/on_removed(datum/source, atom/movable/gone, direction)
SIGNAL_HANDLER
if(parent != gone)
@@ -254,12 +298,7 @@
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
/datum/component/aquarium_content/proc/on_tank_stasis()
// Stop processing until inserted into aquarium again.
stop_flopping()
STOP_PROCESSING(SSobj, properties)
@@ -2,9 +2,3 @@
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)
. = ..()
@@ -1,33 +0,0 @@
/**
* 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
//If element is added to something already in aquarium, just create the component.
var/atom/movable/movable_target = target
if(istype(movable_target.loc, /obj/structure/aquarium))
create_aquarium_component(movable_target)
else //otherwise the component will be created when trying to insert the thing.
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)
+1
View File
@@ -1340,6 +1340,7 @@ GLOBAL_DATUM_INIT(fire_overlay, /mutable_appearance, mutable_appearance('icons/e
alpha = 0
transform = animation_matrix
SEND_SIGNAL(src, COMSIG_ATOM_TEMPORARY_ANIMATION_START, 3)
// This is instant on byond's end, but to our clients this looks like a quick drop
animate(src, alpha = old_alpha, pixel_x = old_x, pixel_y = old_y, transform = old_transform, time = 3, easing = CUBIC_EASING)
+5 -7
View File
@@ -25,7 +25,7 @@ at the cost of risking a vicious bite.**/
/obj/item/restraints/handcuffs/cable/pink = 1,
/obj/item/restraints/handcuffs/alien = 2,
/obj/item/coin/bananium = 9,
/datum/aquarium_behaviour/fish/ratfish = 10,
/obj/item/fish/ratfish = 10,
/obj/item/knife/butcher = 5,
/obj/item/coin/mythril = 1,
)
@@ -33,16 +33,14 @@ at the cost of risking a vicious bite.**/
/obj/structure/moisture_trap/Initialize(mapload)
. = ..()
ADD_TRAIT(src, TRAIT_FISH_SAFE_STORAGE, TRAIT_GENERIC)
AddElement(/datum/element/swabable, CELL_LINE_TABLE_MOIST, CELL_VIRUS_TABLE_GENERIC, rand(2,4), 20)
if(prob(40))
critter_infested = FALSE
if(prob(75))
var/picked_item = pick_weight(loot_table)
if(ispath(picked_item, /datum/aquarium_behaviour/fish))
hidden_item = generate_fish(src, picked_item)
else
hidden_item = new picked_item(src)
AddElement(/datum/element/swabable, CELL_LINE_TABLE_MOIST, CELL_VIRUS_TABLE_GENERIC, rand(2,4), 20)
ADD_TRAIT(src, TRAIT_FISH_SAFE_STORAGE, TRAIT_GENERIC)
hidden_item = new picked_item(src)
/obj/structure/moisture_trap/Destroy()
if(hidden_item)
+28 -6
View File
@@ -37,14 +37,22 @@
///Current layers in use by aquarium contents
var/list/used_layers = list()
var/alive_fish = 0
var/dead_fish = 0
/// /obj/item/fish in the aquarium - does not include things with aquarium visuals that are not fish
var/list/tracked_fish = list()
/obj/structure/aquarium/Initialize(mapload)
. = ..()
update_appearance()
RegisterSignal(src,COMSIG_PARENT_ATTACKBY, .proc/feed_feedback)
/obj/structure/aquarium/Entered(atom/movable/arrived, atom/old_loc, list/atom/old_locs)
. = ..()
if(istype(arrived,/obj/item/fish))
tracked_fish += arrived
/obj/structure/aquarium/Exited(atom/movable/gone, direction)
. = ..()
tracked_fish -= gone
/obj/structure/aquarium/proc/request_layer(layer_type)
/**
@@ -117,9 +125,6 @@
update_appearance()
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))
@@ -138,7 +143,6 @@
/obj/structure/aquarium/interact(mob/user)
if(!broken && user.pulling && 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)
@@ -169,6 +173,13 @@
/obj/structure/aquarium/proc/admire(mob/user)
to_chat(user,span_notice("You take a moment to watch [src]."))
if(do_after(user, 5 SECONDS, target = src))
var/alive_fish = 0
var/dead_fish = 0
for(var/obj/item/fish/fish in tracked_fish)
if(fish.status == FISH_ALIVE)
alive_fish++
else
dead_fish++
//Check if there are live fish - good mood
//All fish dead - bad mood.
//No fish - nothing.
@@ -255,3 +266,14 @@
#undef AQUARIUM_LAYER_STEP
#undef AQUARIUM_MIN_OFFSET
#undef AQUARIUM_MAX_OFFSET
/obj/structure/aquarium/prefilled/Initialize(mapload)
. = ..()
new /obj/item/aquarium_prop/rocks(src)
new /obj/item/aquarium_prop/seaweed(src)
new /obj/item/fish/goldfish(src)
new /obj/item/fish/angelfish(src)
new /obj/item/fish/guppy(src)
+2 -297
View File
@@ -1,301 +1,8 @@
//Fish breeding stops if fish count exceeds this.
#define AQUARIUM_MAX_BREEDING_POPULATION 20
// 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/available_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/fishmeat
/// Won't breed more than this amount in single aquarium.
var/stable_population = 1
/// Last time new fish was created
var/last_breeding
/// How long it takes to produce new fish
var/breeding_timeout = 2 MINUTES
/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
parent.stop_flopping()
/datum/aquarium_behaviour/fish/before_removal()
. = ..()
if(status == FISH_DEAD)
parent.current_aquarium.dead_fish -= 1
else
parent.current_aquarium.alive_fish -= 1
//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
if(!QDELETED(parent) && status != FISH_DEAD)
parent.start_flopping()
/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(required_fluid_type != AQUARIUM_FLUID_ANADROMOUS)
if(aquarium.fluid_type != required_fluid_type)
return FALSE
else
if(aquarium.fluid_type != AQUARIUM_FLUID_SALTWATER && aquarium.fluid_type != AQUARIUM_FLUID_FRESHWATER)
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
process_health(delta_time)
if(status != FISH_DEAD && ready_to_reproduce())
try_to_reproduce()
/datum/aquarium_behaviour/fish/proc/process_health(delta_time)
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
adjust_health(health + health_change_per_second * delta_time)
/datum/aquarium_behaviour/fish/proc/adjust_health(amt)
health = clamp(amt, 0, initial(health))
if(health <= 0)
death()
/datum/aquarium_behaviour/fish/proc/death()
STOP_PROCESSING(SSobj, src)
status = FISH_DEAD
var/message = span_notice("\The [name] dies.")
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)
parent.stop_flopping()
update_animation()
/datum/aquarium_behaviour/fish/proc/ready_to_reproduce()
return parent.current_aquarium && parent.current_aquarium.allow_breeding && health == initial(health) && stable_population > 1 && world.time - last_breeding >= breeding_timeout
/datum/aquarium_behaviour/fish/proc/try_to_reproduce()
if(!parent.current_aquarium)
return
if(parent.current_aquarium.alive_fish + parent.current_aquarium.dead_fish >= AQUARIUM_MAX_BREEDING_POPULATION) //so aquariums full of fish don't need to do these expensive checks
return
var/list/other_fish_of_same_type = list()
for(var/atom/movable/fish_or_prop in parent.current_aquarium)
if(fish_or_prop == parent.parent)
continue
var/datum/component/aquarium_content/other_content = fish_or_prop.GetComponent(/datum/component/aquarium_content)
if(other_content && other_content.properties.type == type)
other_fish_of_same_type += other_content.properties
if(length(other_fish_of_same_type) >= stable_population)
return
var/datum/aquarium_behaviour/fish/second_fish
for(var/datum/aquarium_behaviour/fish/other_fish in other_fish_of_same_type)
if(other_fish.ready_to_reproduce())
second_fish = other_fish
break
if(second_fish)
generate_fish(parent.current_aquarium,type)
last_breeding = world.time
second_fish.last_breeding = world.time
/// This path exists mostly for admin abuse.
/datum/aquarium_behaviour/fish/auto
name = "automatic fish"
@@ -309,6 +16,4 @@
holder.appearance = parent.parent
holder.transform = base_transform()
holder.dir = WEST
#undef AQUARIUM_MAX_BREEDING_POPULATION
*/
+53 -18
View File
@@ -25,13 +25,14 @@
component_type = /datum/component/storage/concrete/fish_case
/obj/item/storage/fish_case/Initialize(mapload)
ADD_TRAIT(src, TRAIT_FISH_SAFE_STORAGE, TRAIT_GENERIC) // Before populate so fish instatiates in ready container already
. = ..()
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())
var/fish_type = select_fish_type()
new fish_type(src)
/obj/item/storage/fish_case/random/proc/select_fish_type()
return random_fish_type()
@@ -47,14 +48,16 @@
/obj/item/storage/fish_case/syndicate/PopulateContents()
. = ..()
generate_fish(src, pick(/datum/aquarium_behaviour/fish/donkfish, /datum/aquarium_behaviour/fish/emulsijack))
var/fish_type = pick(/obj/item/fish/donkfish, /obj/item/fish/emulsijack)
new fish_type(src)
/obj/item/storage/fish_case/tiziran
name = "imported fish case"
/obj/item/storage/fish_case/tiziran/PopulateContents()
. = ..()
generate_fish(src, pick(/datum/aquarium_behaviour/fish/dwarf_moonfish, /datum/aquarium_behaviour/fish/gunner_jellyfish, /datum/aquarium_behaviour/fish/needlefish, /datum/aquarium_behaviour/fish/armorfish))
var/fish_type = pick(/obj/item/fish/dwarf_moonfish, /obj/item/fish/gunner_jellyfish, /obj/item/fish/needlefish, /obj/item/fish/armorfish)
new fish_type(src)
///Book detailing where to get the fish and their properties.
/obj/item/book/fish_catalog
@@ -78,25 +81,26 @@
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
for(var/_fish_type in subtypesof(/obj/item/fish))
var/obj/item/fish/fish = _fish_type
var/list/fish_data = list()
if(!initial(fish_behaviour.show_in_catalog))
if(!initial(fish.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.available_in_random_cases) ? "[AQUARIUM_COMPANY] Fish Packs" : "Unknown"
var/datum/reagent/food_type = initial(fish_behaviour.food)
fish_data["name"] = initial(fish.name)
fish_data["desc"] = initial(fish.desc)
fish_data["fluid"] = initial(fish.required_fluid_type)
fish_data["temp_min"] = initial(fish.required_temperature_min)
fish_data["temp_max"] = initial(fish.required_temperature_max)
fish_data["icon"] = sanitize_css_class_name("[initial(fish.icon)][initial(fish.icon_state)]")
fish_data["color"] = initial(fish.color)
fish_data["source"] = initial(fish.available_in_random_cases) ? "[AQUARIUM_COMPANY] Fish Packs" : "Unknown"
var/datum/reagent/food_type = initial(fish.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)
// TODO: Custom entries
.["fish_info"] = fish_info
.["sponsored_by"] = AQUARIUM_COMPANY
@@ -121,12 +125,43 @@
/obj/item/aquarium_prop
name = "generic aquarium prop"
desc = "very boring"
icon = 'icons/obj/aquarium.dmi'
w_class = WEIGHT_CLASS_TINY
var/layer_mode = AQUARIUM_LAYER_MODE_BOTTOM
/obj/item/aquarium_prop/Initialize(mapload)
. = ..()
AddComponent(/datum/component/aquarium_content)
/obj/item/aquarium_prop/rocks
name = "rocks"
icon_state = "rocks"
/obj/item/aquarium_prop/seaweed_top
name = "dense seaweeds"
icon_state = "seaweeds_front"
layer_mode = AQUARIUM_LAYER_MODE_TOP
/obj/item/aquarium_prop/seaweed
name = "seaweeds"
icon_state = "seaweeds_back"
layer_mode = AQUARIUM_LAYER_MODE_BOTTOM
/obj/item/aquarium_prop/rockfloor
name = "rock floor"
icon_state = "rockfloor"
layer_mode = AQUARIUM_LAYER_MODE_BOTTOM
/obj/item/aquarium_prop/treasure
name = "tiny treasure chest"
icon_state = "treasure"
layer_mode = AQUARIUM_LAYER_MODE_BOTTOM
/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)
for(var/prop_type in subtypesof(/obj/item/aquarium_prop))
new prop_type(src)
+326 -61
View File
@@ -2,25 +2,300 @@
/obj/item/fish
name = "generic looking aquarium fish"
desc = "very bland"
icon = 'icons/obj/aquarium.dmi'
icon_state = "bugfish"
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 base_path(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
/// Resulting width of aquarium visual icon - default size of "fish_greyscale" state
var/sprite_width = 3
/// Resulting height of aquarium visual icon - default size of "fish_greyscale" state
var/sprite_height = 3
/// Original width of aquarium visual icon - used to calculate scaledown factor
var/source_width = 32
/// Original height of aquarium visual icon - used to calculate scaledown factor
var/source_height = 32
/// If present this icon will be used for in-aquarium visual for the fish instead of icon_state
var/dedicated_in_aquarium_icon_state
/// If present aquarium visual will be this color
var/aquarium_vc_color
/// 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/available_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/fishmeat
/// Won't breed more than this amount in single aquarium.
var/stable_population = 1
/// Last time new fish was created
var/last_breeding
/// How long it takes to produce new fish
var/breeding_timeout = 2 MINUTES
var/flopping = FALSE
var/in_stasis = FALSE
/obj/item/fish/Initialize(mapload)
. = ..()
if(fillet_type)
AddElement(/datum/element/processable, TOOL_KNIFE, fillet_type, 1, 5)
AddComponent(/datum/component/aquarium_content, .proc/get_aquarium_animation, list(COMSIG_FISH_STATUS_CHANGED,COMSIG_FISH_STIRRED))
RegisterSignal(src, COMSIG_ATOM_TEMPORARY_ANIMATION_START, .proc/on_temp_animation)
check_environment_after_movement()
if(status != FISH_DEAD)
START_PROCESSING(SSobj, src)
/obj/item/fish/Moved(atom/OldLoc, Dir)
. = ..()
check_environment_after_movement()
/obj/item/fish/proc/enter_stasis()
in_stasis = TRUE
// Stop processing until inserted into aquarium again.
stop_flopping()
STOP_PROCESSING(SSobj, src)
/obj/item/fish/proc/exit_stasis()
in_stasis = FALSE
if(status != FISH_DEAD)
START_PROCESSING(SSobj, src)
/obj/item/fish/proc/on_aquarium_insertion(obj/structure/aquarium)
if(isnull(last_feeding)) //Fish start fed.
last_feeding = world.time
RegisterSignal(aquarium, COMSIG_ATOM_EXITED, .proc/aquarium_exited)
RegisterSignal(aquarium, COMSIG_PARENT_ATTACKBY, .proc/attack_reaction)
/obj/item/fish/proc/aquarium_exited(datum/source, atom/movable/gone, direction)
SIGNAL_HANDLER
if(src != gone)
return
UnregisterSignal(source,list(COMSIG_ATOM_EXITED,COMSIG_PARENT_ATTACKBY))
/// Our aquarium is hit with stuff
/obj/item/fish/proc/attack_reaction(datum/source, obj/item/thing, mob/user, params)
SIGNAL_HANDLER
if(is_food(thing))
on_feeding(thing.reagents)
return COMPONENT_NO_AFTERATTACK
else
//stirred effect
SEND_SIGNAL(src, COMSIG_FISH_STIRRED)
/obj/item/fish/proc/is_food(obj/item/thing)
return istype(thing, /obj/item/fish_feed)
/obj/item/fish/proc/on_feeding(datum/reagents/feed_reagents)
if(feed_reagents.has_reagent(food))
last_feeding = world.time
/obj/item/fish/proc/check_environment_after_movement()
if(QDELETED(src)) //we don't care anymore
return
// Apply/remove stasis as needed
if(HAS_TRAIT(loc, TRAIT_FISH_SAFE_STORAGE))
enter_stasis()
else if(in_stasis)
exit_stasis()
// Do additional stuff
var/in_aquarium = istype(loc,/obj/structure/aquarium)
if(in_aquarium)
on_aquarium_insertion(loc)
// Start flopping if outside of fish container
var/should_be_flopping = status == FISH_ALIVE && !HAS_TRAIT(loc,TRAIT_FISH_SAFE_STORAGE) && !in_aquarium
if(should_be_flopping)
start_flopping()
else
stop_flopping()
/obj/item/fish/process(delta_time)
if(in_stasis || status != FISH_ALIVE)
return
process_health(delta_time)
if(ready_to_reproduce())
try_to_reproduce()
/obj/item/fish/proc/set_status(new_status)
switch(new_status)
if(FISH_ALIVE)
status = FISH_ALIVE
health = initial(health) // this is admin option anyway
START_PROCESSING(SSobj, src)
if(FISH_DEAD)
status = FISH_DEAD
STOP_PROCESSING(SSobj, src)
stop_flopping()
var/message = span_notice("\The [name] dies.")
if(istype(loc,/obj/structure/aquarium))
loc.visible_message(message)
else
visible_message(message)
SEND_SIGNAL(src, COMSIG_FISH_STATUS_CHANGED)
/obj/item/fish/proc/get_aquarium_animation()
var/obj/structure/aquarium/aquarium = loc
if(!istype(aquarium) || aquarium.fluid_type == AQUARIUM_FLUID_AIR || status == FISH_DEAD)
return AQUARIUM_ANIMATION_FISH_DEAD
else
return AQUARIUM_ANIMATION_FISH_SWIM
/// Checks if our current environment lets us live.
/obj/item/fish/proc/proper_environment()
var/obj/structure/aquarium/aquarium = loc
if(!istype(aquarium))
return FALSE
if(required_fluid_type != AQUARIUM_FLUID_ANADROMOUS)
if(aquarium.fluid_type != required_fluid_type)
return FALSE
else
if(aquarium.fluid_type != AQUARIUM_FLUID_SALTWATER && aquarium.fluid_type != AQUARIUM_FLUID_FRESHWATER)
return FALSE
if(aquarium.fluid_temp < required_temperature_min || aquarium.fluid_temp > required_temperature_max)
return FALSE
return TRUE
/obj/item/fish/proc/process_health(delta_time)
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
adjust_health(health + health_change_per_second * delta_time)
/obj/item/fish/proc/adjust_health(amt)
health = clamp(amt, 0, initial(health))
if(health <= 0)
set_status(FISH_DEAD)
/obj/item/fish/proc/ready_to_reproduce()
var/obj/structure/aquarium/aquarium = loc
if(!istype(aquarium))
return FALSE
return aquarium.allow_breeding && health == initial(health) && stable_population > 1 && world.time - last_breeding >= breeding_timeout
//Fish breeding stops if fish count exceeds this.
#define AQUARIUM_MAX_BREEDING_POPULATION 20
/obj/item/fish/proc/try_to_reproduce()
var/obj/structure/aquarium/aquarium = loc
if(!istype(aquarium))
return
if(length(aquarium.tracked_fish) >= AQUARIUM_MAX_BREEDING_POPULATION) //so aquariums full of fish don't need to do these expensive checks
return
var/list/other_fish_of_same_type = list()
for(var/obj/item/fish/fish_in_aquarium in aquarium)
if(fish_in_aquarium == src || fish_in_aquarium.type != type)
continue
other_fish_of_same_type += fish_in_aquarium
if(length(other_fish_of_same_type) >= stable_population)
return
var/obj/item/fish/second_fish
for(var/obj/item/fish/other_fish in other_fish_of_same_type)
if(other_fish.ready_to_reproduce())
second_fish = other_fish
break
if(second_fish)
new type(loc) //could use child_type var
last_breeding = world.time
second_fish.last_breeding = world.time
#undef AQUARIUM_MAX_BREEDING_POPULATION
#define PAUSE_BETWEEN_PHASES 15
#define PAUSE_BETWEEN_FLOPS 2
#define FLOP_COUNT 2
#define FLOP_DEGREE 20
#define FLOP_SINGLE_MOVE_TIME 1.5
#define JUMP_X_DISTANCE 5
#define JUMP_Y_DISTANCE 6
/// This animation should be applied to actual parent atom instead of vc_object.
/proc/flop_animation(atom/movable/animation_target)
var/pause_between = PAUSE_BETWEEN_PHASES + rand(1, 5) //randomized a bit so fish are not in sync
animate(animation_target, time = pause_between, loop = -1)
//move nose down and up
for(var/_ in 1 to FLOP_COUNT)
var/matrix/up_matrix = matrix()
up_matrix.Turn(FLOP_DEGREE)
var/matrix/down_matrix = matrix()
down_matrix.Turn(-FLOP_DEGREE)
animate(transform = down_matrix, time = FLOP_SINGLE_MOVE_TIME, loop = -1)
animate(transform = up_matrix, time = FLOP_SINGLE_MOVE_TIME, loop = -1)
animate(transform = matrix(), time = FLOP_SINGLE_MOVE_TIME, loop = -1, easing = BOUNCE_EASING | EASE_IN)
animate(time = PAUSE_BETWEEN_FLOPS, loop = -1)
//bounce up and down
animate(time = pause_between, loop = -1, flags = ANIMATION_PARALLEL)
var/jumping_right = FALSE
var/up_time = 3 * FLOP_SINGLE_MOVE_TIME / 2
for(var/_ in 1 to FLOP_COUNT)
jumping_right = !jumping_right
var/x_step = jumping_right ? JUMP_X_DISTANCE/2 : -JUMP_X_DISTANCE/2
animate(time = up_time, pixel_y = JUMP_Y_DISTANCE , pixel_x=x_step, loop = -1, flags= ANIMATION_RELATIVE, easing = BOUNCE_EASING | EASE_IN)
animate(time = up_time, pixel_y = -JUMP_Y_DISTANCE, pixel_x=x_step, loop = -1, flags= ANIMATION_RELATIVE, easing = BOUNCE_EASING | EASE_OUT)
animate(time = PAUSE_BETWEEN_FLOPS, loop = -1)
#undef PAUSE_BETWEEN_PHASES
#undef PAUSE_BETWEEN_FLOPS
#undef FLOP_COUNT
#undef FLOP_DEGREE
#undef FLOP_SINGLE_MOVE_TIME
#undef JUMP_X_DISTANCE
#undef JUMP_Y_DISTANCE
/// Starts flopping animation
/obj/item/fish/proc/start_flopping()
if(!flopping) //Requires update_transform/animate_wrappers to be less restrictive.
flopping = TRUE
flop_animation(src)
/// Stops flopping animation
/obj/item/fish/proc/stop_flopping()
if(flopping)
flopping = FALSE
animate(src, transform = matrix()) //stop animation
/// Refreshes flopping animation after temporary animation finishes
/obj/item/fish/proc/on_temp_animation(datum/source, animation_duration)
if(animation_duration > 0)
addtimer(CALLBACK(src, .proc/refresh_flopping), animation_duration)
/obj/item/fish/proc/refresh_flopping()
if(flopping)
flop_animation(src)
/// Returns random fish, using random_case_rarity probabilities.
/proc/random_fish_type(case_fish_only=TRUE, required_fluid)
@@ -30,20 +305,18 @@
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)
for(var/_fish_type in subtypesof(/obj/item/fish))
var/obj/item/fish/fish = _fish_type
if(required_fluid && initial(fish.required_fluid_type) != required_fluid)
continue
if(initial(fish_behavior.available_in_random_cases) || !case_fish_only)
chance_table[fish_behavior] = initial(fish_behavior.random_case_rarity)
if(initial(fish.available_in_random_cases) || !case_fish_only)
chance_table[fish] = initial(fish.random_case_rarity)
probability_table[argkey] = chance_table
return pick_weight(probability_table[argkey])
// Actual fish definitions below - there's no specific paths, they are autogenerated from behaviours
// Freshwater fish
/datum/aquarium_behaviour/fish/goldfish
/obj/item/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"
@@ -52,7 +325,7 @@
stable_population = 3
/datum/aquarium_behaviour/fish/angelfish
/obj/item/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"
@@ -62,7 +335,7 @@
stable_population = 3
/datum/aquarium_behaviour/fish/guppy
/obj/item/fish/guppy
name = "guppy"
desc = "Guppy is also known as rainbow fish because of the brightly colored body and fins."
icon_state = "guppy"
@@ -73,7 +346,7 @@
stable_population = 6
/datum/aquarium_behaviour/fish/plasmatetra
/obj/item/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"
@@ -82,7 +355,7 @@
stable_population = 3
/datum/aquarium_behaviour/fish/catfish
/obj/item/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"
@@ -91,17 +364,9 @@
stable_population = 3
/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
available_in_random_cases = FALSE
// Saltwater fish below
/datum/aquarium_behaviour/fish/clownfish
/obj/item/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"
@@ -112,7 +377,7 @@
stable_population = 4
/datum/aquarium_behaviour/fish/cardinal
/obj/item/fish/cardinal
name = "cardinalfish"
desc = "Cardinalfish are often found near sea urchins, where the fish hide when threatened."
icon_state = "cardinalfish"
@@ -121,7 +386,7 @@
stable_population = 4
/datum/aquarium_behaviour/fish/greenchromis
/obj/item/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"
@@ -129,9 +394,9 @@
aquarium_vc_color = "#00ff00"
required_fluid_type = AQUARIUM_FLUID_SALTWATER
stable_population = 5
/datum/aquarium_behaviour/fish/firefish
stable_population = 5
/obj/item/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"
@@ -141,7 +406,7 @@
stable_population = 3
/datum/aquarium_behaviour/fish/pufferfish
/obj/item/fish/pufferfish
name = "pufferfish"
desc = "One Pufferfish contains enough toxins in its liver to kill 30 people."
icon_state = "pufferfish"
@@ -151,7 +416,7 @@
stable_population = 3
/datum/aquarium_behaviour/fish/lanternfish
/obj/item/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"
@@ -165,7 +430,7 @@
stable_population = 3
//Tiziran Fish
/datum/aquarium_behaviour/fish/dwarf_moonfish
/obj/item/fish/dwarf_moonfish
name = "dwarf moonfish"
desc = "Ordinarily in the wild, the Zagoskian moonfish is around the size of a tuna, however through selective breeding a smaller breed suitable for being kept as an aquarium pet has been created."
icon_state = "dwarf_moonfish"
@@ -173,7 +438,7 @@
stable_population = 2
fillet_type = /obj/item/food/fishmeat/moonfish
/datum/aquarium_behaviour/fish/gunner_jellyfish
/obj/item/fish/gunner_jellyfish
name = "gunner jellyfish"
desc = "So called due to their resemblance to an artillery shell, the gunner jellyfish is native to Tizira, where it is enjoyed as a delicacy. Produces a mild hallucinogen that is destroyed by cooking."
icon_state = "gunner_jellyfish"
@@ -181,7 +446,7 @@
stable_population = 4
fillet_type = /obj/item/food/fishmeat/gunner_jellyfish
/datum/aquarium_behaviour/fish/needlefish
/obj/item/fish/needlefish
name = "needlefish"
desc = "A tiny, transparent fish which resides in large schools in the oceans of Tizira. A common food for other, larger fish."
icon_state = "needlefish"
@@ -189,7 +454,7 @@
stable_population = 12
fillet_type = null
/datum/aquarium_behaviour/fish/armorfish
/obj/item/fish/armorfish
name = "armorfish"
desc = "A small shellfish native to Tizira's oceans, known for its exceptionally hard shell. Consumed similarly to prawns."
icon_state = "armorfish"
@@ -201,10 +466,10 @@
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)
for(var/fish_type in subtypesof(/obj/item/fish))
new fish_type(src)
/datum/aquarium_behaviour/fish/donkfish
/obj/item/fish/donkfish
name = "donk co. company patent donkfish"
desc = "A lab-grown donkfish. Its invention was an accident for the most part, as it was intended to be consumed in donk pockets. Unfortunately, it tastes horrible, so it has now become a pseudo-mascot."
icon_state = "donkfish"
@@ -213,7 +478,7 @@
stable_population = 4
fillet_type = /obj/item/food/fishmeat/donkfish
/datum/aquarium_behaviour/fish/emulsijack
/obj/item/fish/emulsijack
name = "toxic emulsijack"
desc = "Ah, the terrifying emulsijack. Created in a laboratory, this slimey, scaleless fish emits an invisible toxin that emulsifies other fish for it to feed on. Its only real use is for completely ruining a tank."
icon_state = "emulsijack"
@@ -221,22 +486,21 @@
required_fluid_type = AQUARIUM_FLUID_ANADROMOUS
stable_population = 3
/datum/aquarium_behaviour/fish/emulsijack/process(delta_time = SSOBJ_DT)
/obj/item/fish/emulsijack/process(delta_time = SSOBJ_DT)
var/emulsified = FALSE
if(parent.current_aquarium)
for(var/obj/item/fish/victim in parent.current_aquarium.contents)
var/datum/component/aquarium_content/content_component = victim.GetComponent(/datum/component/aquarium_content)
var/datum/aquarium_behaviour/fish/fish_properties = content_component.properties
if(istype(fish_properties, /datum/aquarium_behaviour/fish/emulsijack))
var/obj/structure/aquarium/aquarium = loc
if(istype(aquarium))
for(var/obj/item/fish/victim in aquarium)
if(istype(victim, /obj/item/fish/emulsijack))
continue //no team killing
fish_properties.adjust_health((fish_properties.health - 3) * delta_time) //the victim may heal a bit but this will quickly kill
victim.adjust_health((victim.health - 3) * delta_time) //the victim may heal a bit but this will quickly kill
emulsified = TRUE
if(emulsified)
adjust_health((health + 3) * delta_time)
last_feeding = world.time //emulsijack feeds on the emulsion!
..()
/datum/aquarium_behaviour/fish/ratfish
/obj/item/fish/ratfish
name = "ratfish"
desc = "A rat exposed to the murky waters of maintenance too long. Any higher power, if it revealed itself, would state that the ratfish's continued existence is extremely unwelcome."
icon_state = "ratfish"
@@ -245,6 +509,7 @@
stable_population = 10 //set by New, but this is the default config value
fillet_type = /obj/item/food/meat/slab/human/mutant/zombie //eww...
/datum/aquarium_behaviour/fish/ratfish/New()
/obj/item/fish/ratfish/Initialize(mapload)
. = ..()
//stable pop reflects the config for how many mice migrate. powerful...
stable_population = CONFIG_GET(number/mice_roundstart)
+2 -2
View File
@@ -2,8 +2,8 @@
name = "fish"
/datum/asset/spritesheet/fish/create_spritesheets()
for (var/path in subtypesof(/datum/aquarium_behaviour/fish))
var/datum/aquarium_behaviour/fish/fish_type = path
for (var/path in subtypesof(/obj/item/fish))
var/obj/item/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]")
+1 -1
View File
@@ -204,6 +204,7 @@
#include "code\__DEFINES\dcs\signals\signals_customizable.dm"
#include "code\__DEFINES\dcs\signals\signals_cytology.dm"
#include "code\__DEFINES\dcs\signals\signals_datum.dm"
#include "code\__DEFINES\dcs\signals\signals_fish.dm"
#include "code\__DEFINES\dcs\signals\signals_food.dm"
#include "code\__DEFINES\dcs\signals\signals_gib.dm"
#include "code\__DEFINES\dcs\signals\signals_global.dm"
@@ -914,7 +915,6 @@
#include "code\datums\elements\curse_announcement.dm"
#include "code\datums\elements\cursed.dm"
#include "code\datums\elements\death_drops.dm"
#include "code\datums\elements\deferred_aquarium_content.dm"
#include "code\datums\elements\delete_on_drop.dm"
#include "code\datums\elements\deliver_first.dm"
#include "code\datums\elements\digitalcamo.dm"