From e6253c78129a09bbc89be1fd8e7cd73791967974 Mon Sep 17 00:00:00 2001 From: Ghom <42542238+Ghommie@users.noreply.github.com> Date: Mon, 4 Nov 2024 22:48:25 +0100 Subject: [PATCH] Adds a score for all species of fish that you've caught. (#86049) ## About The Pull Request I'm adding a score that tracks which types of fish you've caught across multiple rounds. To do so, I had to add a new score subtype that manages the score value not being a number. Thankfully the achievement code is fairly flexible so not a whole lot had to be done, although I've to add a new column to the achievements table in the DB, because the 'value' is for integers, while we need one for text strings ~~(the contents of the list are converted to text with a delimiter before being saved cuz I'm not sure if and how our DM slash SQL integration handles using lists directly and I don't want to waste time finding it out)~~. EDIT: It's mostly done beside the reviews that are going to point out things that need to be changed. The UI changes are done. It's time for reviews. Here are screenshots of the UI with all fish still uncatched beside one (I've since then the typo on its name and removed an extra zero from the index number, as well as a nit with the spacing between cells): ![immagine](https://github.com/user-attachments/assets/a1dcfeb6-6d26-461e-aaa1-97c619f5cbfa) ![immagine](https://github.com/user-attachments/assets/768f6621-c992-4932-9bca-979dd1e43d6f) ## Why It's Good For The Game We have about dozens over dozens of different fish in the game now, many of which are just fluff anyway. It's getting to the point it's perhaps doable to add a score or something to be a braggard about. ## Changelog :cl: add: Added a new score that keeps track of all different fish that you've caught between shifts. server: Added a new schema table to store the aforementioned entries and the ckeys associated to them, with an additional timestamp column. /:cl: --- SQL/database_changelog.md | 19 +- SQL/tgstation_schema.sql | 10 + SQL/tgstation_schema_prefixed.sql | 10 + code/__DEFINES/achievements.dm | 3 + .../dcs/signals/signals_subsystem.dm | 3 + code/__DEFINES/fish.dm | 5 + code/__DEFINES/subsystems.dm | 2 +- code/__HELPERS/maths.dm | 8 + code/controllers/subsystem/achievements.dm | 3 +- code/controllers/subsystem/dbcore.dm | 10 +- .../subsystem/processing/fishing.dm | 45 +++- code/datums/achievements/_achievement_data.dm | 41 +-- code/datums/achievements/_awards.dm | 186 +++++++++++-- code/datums/achievements/misc_scores.dm | 69 +++++ code/modules/fishing/fish/_fish.dm | 7 + code/modules/fishing/fish/types/air_space.dm | 5 + code/modules/fishing/fish/types/anadromous.dm | 3 + code/modules/fishing/fish/types/freshwater.dm | 8 + .../modules/fishing/fish/types/holographic.dm | 7 + code/modules/fishing/fish/types/mining.dm | 7 + code/modules/fishing/fish/types/ruins.dm | 3 + code/modules/fishing/fish/types/saltwater.dm | 12 + code/modules/fishing/fish/types/station.dm | 8 +- code/modules/fishing/fish/types/syndicate.dm | 5 + code/modules/fishing/fish/types/tiziran.dm | 4 + code/modules/fishing/fishing_minigame.dm | 5 + code/modules/fishing/sources/_fish_source.dm | 1 - code/modules/unit_tests/fish_unit_tests.dm | 2 + .../packages/tgui/interfaces/Achievements.jsx | 147 ---------- .../packages/tgui/interfaces/Achievements.tsx | 255 ++++++++++++++++++ 30 files changed, 689 insertions(+), 204 deletions(-) delete mode 100644 tgui/packages/tgui/interfaces/Achievements.jsx create mode 100644 tgui/packages/tgui/interfaces/Achievements.tsx diff --git a/SQL/database_changelog.md b/SQL/database_changelog.md index 8ae4c7d4264..1a4f505cb18 100644 --- a/SQL/database_changelog.md +++ b/SQL/database_changelog.md @@ -2,15 +2,28 @@ Any time you make a change to the schema files, remember to increment the databa Make sure to also update `DB_MAJOR_VERSION` and `DB_MINOR_VERSION`, which can be found in `code/__DEFINES/subsystem.dm`. -The latest database version is 5.27; The query to update the schema revision table is: +The latest database version is 5.28; The query to update the schema revision table is: ```sql -INSERT INTO `schema_revision` (`major`, `minor`) VALUES (5, 27); +INSERT INTO `schema_revision` (`major`, `minor`) VALUES (5, 28); ``` or ```sql -INSERT INTO `SS13_schema_revision` (`major`, `minor`) VALUES (5, 27); +INSERT INTO `SS13_schema_revision` (`major`, `minor`) VALUES (5, 28); +``` + +----------------------------------------------------- +Version 5.28, 1 November 2024, by Ghommie +Added `fish_progress` as the first 'progress' subtype of 'datum/award/scores' + +```sql +CREATE TABLE `fish_progress` ( + `ckey` VARCHAR(32) NOT NULL, + `progress_entry` VARCHAR(32) NOT NULL, + `datetime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`ckey`,`progress_entry`) +) ENGINE=InnoDB; ``` In any query remember to add a prefix to the table names if you use one. diff --git a/SQL/tgstation_schema.sql b/SQL/tgstation_schema.sql index 19739a306b5..ba3ff538f1c 100644 --- a/SQL/tgstation_schema.sql +++ b/SQL/tgstation_schema.sql @@ -591,6 +591,16 @@ CREATE TABLE `achievement_metadata` ( PRIMARY KEY (`achievement_key`) ) ENGINE=InnoDB; +-- Table structure for table 'x_progress' + +DROP TABLE IF EXISTS `fish_progress`; +CREATE TABLE `fish_progress` ( + `ckey` VARCHAR(32) NOT NULL, + `progress_entry` VARCHAR(32) NOT NULL, + `datetime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`ckey`,`progress_entry`) +) ENGINE=InnoDB; + -- -- Table structure for table `ticket` -- diff --git a/SQL/tgstation_schema_prefixed.sql b/SQL/tgstation_schema_prefixed.sql index fb9a6cbe10f..525b1b0aa33 100644 --- a/SQL/tgstation_schema_prefixed.sql +++ b/SQL/tgstation_schema_prefixed.sql @@ -590,6 +590,16 @@ CREATE TABLE `SS13_achievement_metadata` ( PRIMARY KEY (`achievement_key`) ) ENGINE=InnoDB; +-- Table structure for table 'SS13_x_progress' + +DROP TABLE IF EXISTS `SS13_fish_progress`; +CREATE TABLE `fish_progress` ( + `ckey` VARCHAR(32) NOT NULL, + `progress_entry` VARCHAR(32) NOT NULL, + `datetime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`ckey`,`progress_entry`) +) ENGINE=InnoDB; + -- -- Table structure for table `SS13_ticket` -- diff --git a/code/__DEFINES/achievements.dm b/code/__DEFINES/achievements.dm index a6dccb5e226..9d21bb4c31c 100644 --- a/code/__DEFINES/achievements.dm +++ b/code/__DEFINES/achievements.dm @@ -142,6 +142,9 @@ /// DB ID for the amount of achievements unlocked by the player. #define ACHIEVEMENTS_SCORE "Achievements Score" +///DB ID for all the different kinds of fish that you've caught so far. +#define FISH_SCORE "Fish Score" + // Tourist related achievements and scores //centcom grades (achievement) diff --git a/code/__DEFINES/dcs/signals/signals_subsystem.dm b/code/__DEFINES/dcs/signals/signals_subsystem.dm index 6d36cbd4428..a41ec035a33 100644 --- a/code/__DEFINES/dcs/signals/signals_subsystem.dm +++ b/code/__DEFINES/dcs/signals/signals_subsystem.dm @@ -23,3 +23,6 @@ #define COMSIG_ADDED_POINT_OF_INTEREST "added_point_of_interest" /// Sent from base of /datum/controller/subsystem/points_of_interest/proc/on_poi_element_removed : (atom/old_poi) #define COMSIG_REMOVED_POINT_OF_INTEREST "removed_point_of_interest" + +///Sent after awards are saved in the database (/datum/controller/subsystem/achievements/save_achievements_to_db) +#define COMSIG_ACHIEVEMENTS_SAVED_TO_DB "achievements_saved_to_db" diff --git a/code/__DEFINES/fish.dm b/code/__DEFINES/fish.dm index 03afd89134c..82c1117e8de 100644 --- a/code/__DEFINES/fish.dm +++ b/code/__DEFINES/fish.dm @@ -232,6 +232,11 @@ #define FISH_SOURCE_FLAG_EXPLOSIVE_MALUS (1<<0) /// The fish source is not elegible for random rewards from bluespace fishing rods #define FISH_SOURCE_FLAG_NO_BLUESPACE_ROD (1<<1) +/** + * If present, fish that can be caught from this source won't be included in the 'fish caught' score, unless + * present in other sources without this flag as well. + */ +#define FISH_SOURCE_FLAG_SKIP_CATCHABLES (1<<2) /** * A macro to ensure the wikimedia filenames of fish icons are unique, especially since there're a couple fish that have diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm index ea707e33a1f..1e4a2eb80e3 100644 --- a/code/__DEFINES/subsystems.dm +++ b/code/__DEFINES/subsystems.dm @@ -20,7 +20,7 @@ * * make sure you add an update to the schema_version stable in the db changelog */ -#define DB_MINOR_VERSION 27 +#define DB_MINOR_VERSION 28 //! ## Timing subsystem diff --git a/code/__HELPERS/maths.dm b/code/__HELPERS/maths.dm index 7e6db6fb020..040e9694429 100644 --- a/code/__HELPERS/maths.dm +++ b/code/__HELPERS/maths.dm @@ -242,6 +242,14 @@ /proc/reciprocal_add(x, y) return 1/((1/x)+y) +/// Returns a text string containing N prefixed with a series of zeros with length equal to max_zeros minus log(10, N), rounded down. +/proc/prefix_zeros_to_number(number, max_zeros) + var/zeros = "" + var/how_many_zeros = max_zeros - round(log(10, number)) + for(var/zero in 1 to how_many_zeros) + zeros += "0" + return "[zeros][number]" + /// 180s an angle /proc/reverse_angle(angle) return (angle + 180) % 360 diff --git a/code/controllers/subsystem/achievements.dm b/code/controllers/subsystem/achievements.dm index e477848d739..3dad9ef824c 100644 --- a/code/controllers/subsystem/achievements.dm +++ b/code/controllers/subsystem/achievements.dm @@ -78,7 +78,8 @@ SUBSYSTEM_DEF(achievements) cheevos_to_save += PD.achievements.get_changed_data() if(!length(cheevos_to_save)) return - SSdbcore.MassInsert(format_table_name("achievements"),cheevos_to_save,duplicate_key = TRUE) + SSdbcore.MassInsert(format_table_name("achievements"), cheevos_to_save, duplicate_key = TRUE) + SEND_SIGNAL(src, COMSIG_ACHIEVEMENTS_SAVED_TO_DB) //Update the metadata if any are behind /datum/controller/subsystem/achievements/proc/update_metadata() diff --git a/code/controllers/subsystem/dbcore.dm b/code/controllers/subsystem/dbcore.dm index b893ee70184..7d01226b2cf 100644 --- a/code/controllers/subsystem/dbcore.dm +++ b/code/controllers/subsystem/dbcore.dm @@ -189,17 +189,17 @@ SUBSYSTEM_DEF(dbcore) //Take over control of all active queries var/queries_to_check = queries_active.Copy() queries_active.Cut() - + //Start all waiting queries for(var/datum/db_query/query in queries_standby) run_query(query) queries_to_check += query queries_standby -= query - + //wait for them all to finish for(var/datum/db_query/query in queries_to_check) UNTIL(query.process() || REALTIMEOFDAY > endtime) - + //log shutdown to the db var/datum/db_query/query_round_shutdown = SSdbcore.NewQuery( "UPDATE [format_table_name("round")] SET shutdown_datetime = Now(), end_state = :end_state WHERE id = :round_id", @@ -247,7 +247,7 @@ SUBSYSTEM_DEF(dbcore) /datum/controller/subsystem/dbcore/proc/Connect() if(IsConnected()) return TRUE - + if(connection) Disconnect() //clear the current connection handle so isconnected() calls stop invoking rustg connection = null //make sure its cleared even if runtimes happened @@ -293,7 +293,7 @@ SUBSYSTEM_DEF(dbcore) log_sql("Connect() failed | [last_error]") ++failed_connections //If it failed to establish a connection more than 5 times in a row, don't bother attempting to connect for a time. - if(failed_connections > max_connection_failures) + if(failed_connections > max_connection_failures) failed_connection_timeout_count++ //basic exponential backoff algorithm failed_connection_timeout = world.time + ((2 ** failed_connection_timeout_count) SECONDS) diff --git a/code/controllers/subsystem/processing/fishing.dm b/code/controllers/subsystem/processing/fishing.dm index 0e8c126fe93..477dbf35e51 100644 --- a/code/controllers/subsystem/processing/fishing.dm +++ b/code/controllers/subsystem/processing/fishing.dm @@ -3,15 +3,37 @@ PROCESSING_SUBSYSTEM_DEF(fishing) name = "Fishing" flags = SS_BACKGROUND|SS_POST_FIRE_TIMING wait = 0.05 SECONDS // If you raise it to 0.1 SECONDS, you better also modify [datum/fish_movement/move_fish()] + ///A list of cached fish icons + var/list/cached_fish_icons + ///A list of cached fish icons turns into outlines with a queston mark in the middle, denoting fish you haven't caught yet. + var/list/cached_unknown_fish_icons + ///An assoc list of identifier strings and the path of a fish that can be gotten from fish sources. + var/list/catchable_fish ///Cached fish properties so we don't have to initalize fish every time var/list/fish_properties ///A cache of fish that can be caught by each type of fishing lure var/list/lure_catchables /datum/controller/subsystem/processing/fishing/Initialize() - ///init the properties + ..() + cached_fish_icons = list() + cached_unknown_fish_icons = list() fish_properties = list() - for(var/fish_type in subtypesof(/obj/item/fish)) + + var/icon/questionmark = icon('icons/effects/random_spawners.dmi', "questionmark") + var/list/mark_dimension = get_icon_dimensions(questionmark) + for(var/obj/item/fish/fish_type as anything in subtypesof(/obj/item/fish)) + var/list/fish_dimensions = get_icon_dimensions(fish_type::icon) + var/icon/fish_icon = icon(fish_type::icon, fish_type::icon_state, frame = 1, moving = FALSE) + cached_fish_icons[fish_type] = icon2base64(fish_icon) + var/icon/unknown_icon = icon(fish_icon) + unknown_icon.Blend("#FFFFFF", ICON_SUBTRACT) + unknown_icon.Blend("#070707", ICON_ADD) + var/width = 1 + (fish_dimensions["width"] - mark_dimension["width"]) * 0.5 + var/height = 1 + (fish_dimensions["height"] - mark_dimension["height"]) * 0.5 + unknown_icon.Blend(questionmark, ICON_OVERLAY, x = width, y = height) + cached_unknown_fish_icons[fish_type] = icon2base64(unknown_icon) + var/obj/item/fish/fish = new fish_type(null, FALSE) var/list/properties = list() fish_properties[fish_type] = properties @@ -47,6 +69,25 @@ PROCESSING_SUBSYSTEM_DEF(fishing) qdel(fish) + catchable_fish = list() + var/list/all_catchables = list() + for(var/source_type as anything in GLOB.preset_fish_sources) + var/datum/fish_source/source = GLOB.preset_fish_sources[source_type] + if(!(source.fish_source_flags & FISH_SOURCE_FLAG_SKIP_CATCHABLES)) + all_catchables |= source.fish_table + for(var/thing in all_catchables) + if(!ispath(thing, /obj/item/fish)) + continue + var/obj/item/fish/fishie = thing + var/fish_id = initial(fishie.fish_id) + if(!fish_id) + stack_trace("[fishie] doesn't have a set 'fish_id' variable despite being a catchable fish") + continue + if(catchable_fish[fish_id]) + stack_trace("[fishie] has a 'fish_id' value already assigned to [catchable_fish[fish_id]]. fish_id: [fish_id]") + continue + catchable_fish[fish_id] = fishie + ///init the list of things lures can catch lure_catchables = list() var/list/fish_types = subtypesof(/obj/item/fish) diff --git a/code/datums/achievements/_achievement_data.dm b/code/datums/achievements/_achievement_data.dm index 8b78cf7c8f3..62d6f6061b0 100644 --- a/code/datums/achievements/_achievement_data.dm +++ b/code/datums/achievements/_achievement_data.dm @@ -24,7 +24,7 @@ for(var/T in data) var/datum/award/A = SSachievements.awards[T] if(data[T] != original_cached_data[T])//If our data from before is not the same as now, save it to db. - var/deets = A.get_changed_rows(owner_ckey,data[T]) + var/deets = A.get_changed_rows(src) if(deets) . += list(deets) @@ -53,12 +53,11 @@ ///Updates local cache with db data for the given achievement type if it wasn't loaded yet. /datum/achievement_data/proc/get_data(achievement_type) - var/datum/award/A = SSachievements.awards[achievement_type] - if(!A.name) + var/datum/award/award = SSachievements.awards[achievement_type] + if(!award.name) return FALSE if(!data[achievement_type]) - data[achievement_type] = A.load(owner_ckey) - original_cached_data[achievement_type] = data[achievement_type] + award.load(src) ///Unlocks an achievement of a specific type. achievement type is a typepath to the award, user is the mob getting the award, and value is an optional value to be used for defining a score to add to the leaderboard /datum/achievement_data/proc/unlock(achievement_type, mob/user, value = 1) @@ -66,15 +65,9 @@ if(!SSachievements.achievements_enabled) return - var/datum/award/A = SSachievements.awards[achievement_type] + var/datum/award/award = SSachievements.awards[achievement_type] get_data(achievement_type) //Get the current status first if necessary - if(istype(A, /datum/award/achievement)) - if(data[achievement_type]) //You already unlocked it so don't bother running the unlock proc - return - data[achievement_type] = TRUE - A.on_unlock(user) //Only on default achievement, as scores keep going up. - else if(istype(A, /datum/award/score)) - data[achievement_type] += value + award.unlock(user, src, value) update_static_data(user) ///Getter for the status/score of an achievement @@ -99,8 +92,9 @@ . = ..() .["categories"] = GLOB.achievement_categories .["achievements"] = list() - .["highscore"] = list() - .["user_key"] = user.ckey + .["highscores"] = list() + .["progresses"] = list() + .["user_key"] = owner_ckey var/datum/asset/spritesheet/simple/assets = get_asset_datum(/datum/asset/spritesheet/simple/achievements) for(var/achievement_type in SSachievements.awards) @@ -116,14 +110,21 @@ "icon_class" = assets.icon_class_name("achievement-[award.icon_state]"), "value" = data[achievement_type], ) - award_data += award.get_ui_data(user.ckey) + award_data += award.get_ui_data(award_data, src) .["achievements"] += list(award_data) - for(var/score in SSachievements.scores) - var/datum/award/score/S = SSachievements.scores[score] - if(!S.name || !S.track_high_scores || !S.high_scores.len) + for(var/score_type in SSachievements.scores) + var/datum/award/score/score = SSachievements.scores[score_type] + if(!score.name) continue - .["highscore"] += list(list("name" = S.name,"scores" = S.high_scores)) + if(istype(score, /datum/award/score/progress)) + var/datum/award/score/progress/prog = score + var/list/prog_data = prog.get_progress(src) + if(length(prog_data)) + .["progresses"] += list(prog_data) + if(!score.track_high_scores || !length(score.high_scores)) + continue + .["highscores"] += list(list("name" = score.name, "scores" = score.high_scores)) /client/verb/checkachievements() set category = "OOC" diff --git a/code/datums/achievements/_awards.dm b/code/datums/achievements/_awards.dm index e7d18f98124..536b1fbcb99 100644 --- a/code/datums/achievements/_awards.dm +++ b/code/datums/achievements/_awards.dm @@ -14,29 +14,34 @@ //Bump this up if you're changing outdated table identifier and/or achievement type var/achievement_version = 2 - //Value returned on db connection failure, in case we want to differ 0 and nonexistent later on - var/default_value = FALSE - ///This proc loads the achievement data from the hub. -/datum/award/proc/load(key) +/datum/award/proc/load(datum/achievement_data/holder) if(!SSdbcore.Connect()) - return default_value - if(!key || !database_id || !name) - return default_value - var/raw_value = get_raw_value(key) - return parse_value(raw_value) + return default_value() + if(!holder.owner_ckey || !database_id || !name) + return default_value() + var/value = parse_value(get_raw_value(holder.owner_ckey)) + holder.original_cached_data[type] = holder.data[type] = value + return value + +//Proc that returns a value upon db connection failure, in case we need different instances too. +/datum/award/proc/default_value() + return FALSE + +/datum/award/proc/unlock(mob/user, datum/achievement_data/holder, value = 1) + return /datum/award/proc/on_achievement_data_init(datum/achievement_data/holder, database_value) holder.original_cached_data[type] = holder.data[type] = parse_value(database_value) ///This saves the changed data to the hub. -/datum/award/proc/get_changed_rows(key, value) - if(!database_id || !key || !name) +/datum/award/proc/get_changed_rows(datum/achievement_data/holder) + if(!database_id || !holder.owner_ckey || !name) return return list( - "ckey" = key, + "ckey" = holder.owner_ckey, "achievement_key" = database_id, - "value" = value, + "value" = holder.data[type], ) /datum/award/proc/get_metadata_row() @@ -65,14 +70,14 @@ //Should return sanitized value for achievement cache /datum/award/proc/parse_value(raw_value) - return default_value + return default_value() ///Can be overridden for achievement specific events /datum/award/proc/on_unlock(mob/user) return ///returns additional ui data for the Check Achievements menu -/datum/award/proc/get_ui_data() +/datum/award/proc/get_ui_data(list/award_data, datum/achievement_data/holder) return list( "score" = FALSE, "achieve_info" = null, @@ -86,11 +91,17 @@ ///How many players have earned this achievement var/times_achieved = 0 +/datum/award/achievement/unlock(mob/user, datum/achievement_data/holder, value = 1) + if(holder.data[type]) //You already unlocked it so don't bother running the unlock proc + return + holder.data[type] = TRUE + on_unlock(user) + /datum/award/achievement/get_metadata_row() . = ..() .["achievement_type"] = "achievement" -/datum/award/achievement/get_ui_data() +/datum/award/achievement/get_ui_data(list/award_data, datum/achievement_data/holder) . = ..() .["achieve_info"] = "Unlocked by [times_achieved] players so far" if(!SSachievements.most_unlocked_achievement) @@ -133,7 +144,6 @@ /datum/award/score desc = "you did it sooo many times." category = "Scores" - default_value = 0 var/track_high_scores = TRUE var/list/high_scores = list() @@ -143,11 +153,17 @@ if(track_high_scores) LoadHighScores() +/datum/award/score/default_value() + return 0 + /datum/award/score/get_metadata_row() . = ..() .["achievement_type"] = "score" -/datum/award/score/get_ui_data() +/datum/award/score/unlock(mob/user, datum/achievement_data/holder, value = 1) + holder.data[type] += value + +/datum/award/score/get_ui_data(list/award_data, datum/achievement_data/holder) . = ..() .["score"] = TRUE @@ -163,7 +179,7 @@ while(Q.NextRow()) var/key = Q.item[1] var/score = text2num(Q.item[2]) - high_scores[key] = score + high_scores += list(list("ckey" = key, "value" = score)) qdel(Q) /datum/award/score/parse_value(raw_value) @@ -176,15 +192,15 @@ icon_state = "elephant" //Obey the reference database_id = ACHIEVEMENTS_SCORE -/datum/award/score/achievements_score/get_ui_data(key) +/datum/award/score/achievements_score/get_ui_data(list/award_data, datum/achievement_data/holder) . = ..() var/datum/db_query/get_unlocked_count = SSdbcore.NewQuery( "SELECT COUNT(m.achievement_key) FROM [format_table_name("achievements")] AS a JOIN [format_table_name("achievement_metadata")] m ON a.achievement_key = m.achievement_key AND m.achievement_type = 'Achievement' WHERE a.ckey = :ckey", - list("ckey" = key) + list("ckey" = holder.owner_ckey) ) if(!get_unlocked_count.Execute(async = TRUE)) qdel(get_unlocked_count) - .["value"] = default_value + .["value"] = 0 return . if(get_unlocked_count.NextRow()) .["value"] = text2num(get_unlocked_count.item[1]) @@ -202,7 +218,7 @@ while(get_unlocked_highscore.NextRow()) var/key = get_unlocked_highscore.item[1] var/score = text2num(get_unlocked_highscore.item[2]) - high_scores[key] = score + high_scores += list(list("ckey" = key, "value" = score)) qdel(get_unlocked_highscore) /datum/award/score/achievements_score/on_achievement_data_init(datum/achievement_data/holder, database_value) @@ -217,3 +233,127 @@ holder.data[type] = text2num(get_unlocked_load.item[1]) || 0 holder.original_cached_data[type] = 0 qdel(get_unlocked_load) + +/** + * A subtype of score linked to a schema table containing objects the player has caught, made, found or otherwise achieved. + * The value of the score should equal to the length of objects that have been achieved. + * It also has an unique tab in the UI that lets you review the progress. + */ +/datum/award/score/progress + /** + * When get_changed_rows is called, this list gets filled with the entries to be mass-inserted + * in the table associated with this progress score. + */ + VAR_FINAL/list/changed_entries = list() + +/datum/award/score/progress/New() + . = ..() + if(!get_table()) + CRASH("get_table() wasn't set for [type]!") + RegisterSignal(SSachievements, COMSIG_ACHIEVEMENTS_SAVED_TO_DB, PROC_REF(insert_entries)) + +/** + * Getter proc for the table used to save the entries - ckey association. + * so we an be extra-safe that data won't be ever inserted in the wrong table. + * Remember to set this + */ +/datum/award/score/progress/proc/get_table() + return + +/datum/award/score/progress/vv_edit_var(var_name, var_value) + //These variable is associated for sql queries. We can't allow it to be edited. + if(var_name == NAMEOF(src, changed_entries)) + return FALSE + return ..() + +/datum/award/score/progress/unlock(mob/user, datum/achievement_data/holder, value) + var/list/entries = holder.data[type] + if(!value) + CRASH("empty value used as argument to progress this score award.") + if(value in entries) + return + // This ensures that the original list and the new won't be the same any longer. + // So that it'll pass the not-equal if statement and be saved in the db. + if(entries == holder.original_cached_data[type]) + entries = entries?.Copy() || list() + holder.data[type] = entries + entries |= value + +/datum/award/score/progress/load(datum/achievement_data/holder) + var/list/results = ..() + return validate_loaded_data(holder, results) + +/datum/award/score/progress/on_achievement_data_init(datum/achievement_data/holder, database_value) + var/list/results = parse_value(get_raw_value(holder.owner_ckey)) + validate_loaded_data(holder, results) + +/datum/award/score/progress/proc/validate_loaded_data(datum/achievement_data/holder, list/results) + holder.original_cached_data[type] = holder.data[type] = results + if(!length(results)) + return results + ///This list will be populated on validate_entries() + var/list/validated_results = list() + if(!validate_entries(results, validated_results)) + holder.data[type] = validated_results + return validated_results + +///Along with the changed rows for the main table, this also populates changed_entries with the entries list +/datum/award/score/progress/get_changed_rows(datum/achievement_data/holder) + if(!database_id || !holder || !name || !get_table()) + return + var/list/entries = holder.data[type] + for(var/entry in (entries - holder.original_cached_data[type])) + changed_entries += list(list("ckey" = holder.owner_ckey, "progress_entry" = entry)) + return list( + "ckey" = holder.owner_ckey, + "achievement_key" = database_id, + "value" = length(entries), + ) + +/datum/award/score/progress/get_ui_data(list/award_data, datum/achievement_data/holder) + . = ..() + award_data["value"] = length(holder.data[type]) + +///We don't care much about the default value, which is only used for high scores. Instead, we get the entries string from the db. +/datum/award/score/progress/get_raw_value(key) + var/list/entries = list() + var/datum/db_query/get_entries_load = SSdbcore.NewQuery( + "SELECT progress_entry FROM [format_table_name(get_table())] WHERE ckey = :ckey", + list("ckey" = key) + ) + if(!get_entries_load.Execute()) + qdel(get_entries_load) + return entries + while(get_entries_load.NextRow()) + entries |= get_entries_load.item[1] + qdel(get_entries_load) + return entries + +/datum/award/score/progress/parse_value(raw_value) + return islist(raw_value) ? raw_value : list() + +//Proc that returns a value upon db connection failure, in case we need to new instances too +/datum/award/score/progress/default_value() + return list() + +/** + * Validates the list of entries after it's parsed. + * If TRUE is returned (entries list is valid), 'entries' will be stored in both + * original_cached_data[type] and data[type] of the achievements holder. + * Otherwise data[type] will be the new validated_entries list, + * ensuring that the original data and the new data aren't the same, allowing the new data will be saved. + */ +/datum/award/score/progress/proc/validate_entries(list/entries, list/validated_entries) + validated_entries = unique_list(entries) + return length(validated_entries) == length(entries) + +////Returns a list of data that we can use to make an index of contents that progress this award/score. +/datum/award/score/progress/proc/get_progress(datum/achievement_data/holder) + CRASH("get_progress() undefined for [type]") + +///Called once the achievements are saved in the DB, since we also have to insert the entries in the associated table. +/datum/award/score/progress/proc/insert_entries(datum/source) + SIGNAL_HANDLER + if(!length(changed_entries)) + return + INVOKE_ASYNC(SSdbcore, TYPE_PROC_REF(/datum/controller/subsystem/dbcore, MassInsert), format_table_name(get_table()), changed_entries, duplicate_key = TRUE) diff --git a/code/datums/achievements/misc_scores.dm b/code/datums/achievements/misc_scores.dm index b91f46008de..292a74e768a 100644 --- a/code/datums/achievements/misc_scores.dm +++ b/code/datums/achievements/misc_scores.dm @@ -21,3 +21,72 @@ name = "Style Score" desc = "You might not be a robot, but you were damn close." database_id = STYLE_SCORE + +/datum/award/score/progress/fish + name = "Fish Species Caught" + desc = "How many different species of fish you've caught so far. Gotta fish 'em all." + database_id = FISH_SCORE + var/list/early_entries_to_validate = list() + +/datum/award/score/progress/fish/New() + . = ..() + RegisterSignal(SSfishing, COMSIG_SUBSYSTEM_POST_INITIALIZE, PROC_REF(validate_early_joiners)) + +/datum/award/score/progress/fish/get_table() + return "fish_progress" + +/datum/award/score/progress/fish/proc/validate_early_joiners(datum/source) + for(var/client/client as anything in GLOB.clients) + var/datum/achievement_data/holder = client.player_details.achievements + if(!holder?.initialized) + continue + var/list/entries = holder.data[/datum/award/score/progress/fish] + var/list_copied = FALSE + for(var/fish_id in entries) + if(SSfishing.catchable_fish[fish_id]) + continue + //make a new list, unbound from the cached awards data, so that the score can be updated at the end of the round. + if(!list_copied) + entries = entries.Copy() + holder.data[/datum/award/score/progress/fish] = entries + list_copied = TRUE + entries -= fish_id + +/datum/award/score/progress/fish/validate_entries(list/entries, list/validated_entries) + . = ..() + if(!SSfishing.initialized) + return + for(var/fish_id in validated_entries) + if(!(SSfishing.catchable_fish[fish_id])) + validated_entries -= fish_id + . = FALSE + +/datum/award/score/progress/fish/get_progress(datum/achievement_data/holder) + var/list/data = list( + "name" = "Fishdex", + "percent" = 0, + "value_text" = "Subsystems still initializing...", + "entries" = list(), + ) + if(!SSfishing.initialized) + return data + var/list/catched_fish = holder.data[type] + var/catched_len = length(catched_fish) + var/catchable_len = length(SSfishing.catchable_fish) + data["percent"] = catched_len/catchable_len + data["value_text"] = "[catched_len] / [catchable_len]" + var/index = 1 + var/max_zeros = round(log(10, catchable_len)) + for(var/fish_id in SSfishing.catchable_fish) + var/obj/item/fish/fish = SSfishing.catchable_fish[fish_id] + var/catched = (fish_id in catched_fish) + var/entry_name = "◦[prefix_zeros_to_number(index, max_zeros)]◦ [catched ? full_capitalize(initial(fish.name)) : "??????" ]" + var/list/icon_dimensions = get_icon_dimensions(initial(fish.icon)) + data["entries"] += list(list( + "name" = entry_name, + "icon" = catched ? SSfishing.cached_fish_icons[fish] : SSfishing.cached_unknown_fish_icons[fish], + "height" = icon_dimensions["height"] * 2, + "width" = icon_dimensions["width"] * 2, + )) + index++ + return data diff --git a/code/modules/fishing/fish/_fish.dm b/code/modules/fishing/fish/_fish.dm index 9143ebcf339..60384400e69 100644 --- a/code/modules/fishing/fish/_fish.dm +++ b/code/modules/fishing/fish/_fish.dm @@ -164,6 +164,13 @@ */ var/bites_amount = 0 + /** + * An identifier for this fish used to track progress for fish caught between rounds in + * a way that's resilient to repathing (and removing paths). Only catchable fish need it. + * Once set, the value shouldn't be changed, so don't make typos. + */ + var/fish_id + /obj/item/fish/Initialize(mapload, apply_qualities = TRUE) . = ..() base_icon_state = icon_state diff --git a/code/modules/fishing/fish/types/air_space.dm b/code/modules/fishing/fish/types/air_space.dm index b0c8208fe81..73c7b82c29d 100644 --- a/code/modules/fishing/fish/types/air_space.dm +++ b/code/modules/fishing/fish/types/air_space.dm @@ -1,5 +1,6 @@ /obj/item/fish/sand_surfer name = "sand surfer" + fish_id = "sand_surfer" desc = "A bronze alien \"fish\" living and swimming underneath faraway sandy places." icon_state = "sand_surfer" sprite_height = 6 @@ -19,6 +20,7 @@ /obj/item/fish/sand_crab name = "burrower crab" + fish_id = "sand_crab" desc = "A sand-dwelling crustacean. It looks like a crab and tastes like a crab, but waddles like a fish." icon_state = "crab" dedicated_in_aquarium_icon_state = "crab_small" @@ -48,6 +50,7 @@ /obj/item/fish/bumpy name = "bump-fish" + fish_id = "bumpy" desc = "An misshapen fish-thing all covered in stubby little tendrils" icon_state = "bumpy" sprite_height = 4 @@ -67,6 +70,7 @@ /obj/item/fish/starfish name = "cosmostarfish" + fish_id = "cosmostarfish" desc = "A peculiar, gravity-defying, echinoderm-looking critter from hyperspace." icon_state = "starfish" icon_state_dead = "starfish_dead" @@ -102,6 +106,7 @@ /obj/item/fish/baby_carp name = "baby space carp" + fish_id = "baby_carp" desc = "A juvenile spawn of the dreaded space carp. Don't let the innocent looks fool you, they're aggressive little bastards." icon_state = "baby_carp" sprite_height = 3 diff --git a/code/modules/fishing/fish/types/anadromous.dm b/code/modules/fishing/fish/types/anadromous.dm index 5afb2cb48ce..7f9e6b4d2e2 100644 --- a/code/modules/fishing/fish/types/anadromous.dm +++ b/code/modules/fishing/fish/types/anadromous.dm @@ -1,5 +1,6 @@ /obj/item/fish/sockeye_salmon name = "sockeye salmon" + fish_id = "sockeye_salmon" desc = "A fairly common and iconic salmon endemic of the Pacific Ocean. At some point imported into outer space, where we're now." icon_state = "sockeye" sprite_width = 6 @@ -18,6 +19,7 @@ /obj/item/fish/arctic_char name = "arctic char" + fish_id = "arctic_char" desc = "A cold-water anadromous fish widespread around the Northern Hemisphere of Earth, yet it has somehow found a way here." icon_state = "arctic_char" sprite_width = 7 @@ -32,6 +34,7 @@ /obj/item/fish/pike name = "pike" + fish_id = "pike" desc = "A long-bodied predator with a snout that almost looks like a beak. Definitely not a weapon to swing around." icon = 'icons/obj/aquarium/wide.dmi' icon_state = "pike" diff --git a/code/modules/fishing/fish/types/freshwater.dm b/code/modules/fishing/fish/types/freshwater.dm index 75d4891b4f0..e1c1ca5db2f 100644 --- a/code/modules/fishing/fish/types/freshwater.dm +++ b/code/modules/fishing/fish/types/freshwater.dm @@ -1,5 +1,6 @@ /obj/item/fish/goldfish name = "goldfish" + fish_id = "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" @@ -36,6 +37,7 @@ /obj/item/fish/goldfish/three_eyes name = "three-eyed goldfish" + fish_id = "three_eyes" desc = "A goldfish with an extra half a pair of eyes. You wonder what it's been feeding on lately..." icon_state = "three_eyes" stable_population = 4 @@ -70,6 +72,7 @@ /obj/item/fish/angelfish name = "angelfish" + fish_id = "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" sprite_width = 4 @@ -83,6 +86,7 @@ /obj/item/fish/guppy name = "guppy" + fish_id = "guppy" desc = "Guppy is also known as rainbow fish because of the brightly colored body and fins." icon_state = "guppy" sprite_width = 5 @@ -97,6 +101,7 @@ /obj/item/fish/plasmatetra name = "plasma tetra" + fish_id = "plasmatetra" desc = "Due to their small size, tetras are prey to many predators in their watery world, including eels, crustaceans, and invertebrates." icon_state = "plastetra" sprite_width = 4 @@ -109,6 +114,7 @@ /obj/item/fish/catfish name = "catfish" + fish_id = "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" sprite_width = 8 @@ -129,6 +135,7 @@ /obj/item/fish/zipzap name = "anxious zipzap" + fish_id = "zipzap" desc = "A fish overflowing with crippling anxiety and electric potential. Worried about the walls of its tank closing in constantly. Both literally and as a general metaphorical unease about life's direction." icon_state = "zipzap" icon_state_dead = "zipzap_dead" @@ -207,6 +214,7 @@ /obj/item/fish/perch name = "perch" + fish_id = "perch" desc = "An all around popular panfish, game fish and unfortunate prey to other, bigger predators." icon_state = "perch" dedicated_in_aquarium_icon_state = "fish_greyscale" diff --git a/code/modules/fishing/fish/types/holographic.dm b/code/modules/fishing/fish/types/holographic.dm index 64de7d866d6..4dc304cb0ca 100644 --- a/code/modules/fishing/fish/types/holographic.dm +++ b/code/modules/fishing/fish/types/holographic.dm @@ -1,6 +1,7 @@ /obj/item/fish/holo name = "holographic goldfish" + fish_id = "hologoldfish" desc = "A holographic representation of a common goldfish, slowly flickering out, removed from its holo-habitat." icon_state = /obj/item/fish/goldfish::icon_state fish_flags = parent_type::fish_flags & ~(FISH_FLAG_SHOW_IN_CATALOG|FISH_FLAG_EXPERIMENT_SCANNABLE) @@ -37,6 +38,7 @@ /obj/item/fish/holo/crab name = "holographic crab" + fish_id = "holocrab" desc = "A holographic represantion of a soul-crushingly soulless crab, unlike the cuter ones occasionally roaming around. It stares at you, with empty, beady eyes." icon_state = "crab" dedicated_in_aquarium_icon_state = null @@ -49,6 +51,7 @@ /obj/item/fish/holo/puffer name = "holographic pufferfish" + fish_id = "holopufferfish" desc ="A holographic representation of 100% safe-to-eat pufferfish... that is, if holographic fishes were even edible." icon_state = /obj/item/fish/pufferfish::icon_state dedicated_in_aquarium_icon_state = /obj/item/fish/pufferfish::dedicated_in_aquarium_icon_state @@ -61,6 +64,7 @@ /obj/item/fish/holo/angel name = "holographic angelfish" + fish_id = "holoangelfish" desc = "A holographic representation of a angelfish. I got nothing snarky to say about this one." icon_state = /obj/item/fish/angelfish::icon_state dedicated_in_aquarium_icon_state = /obj/item/fish/angelfish::dedicated_in_aquarium_icon_state @@ -73,6 +77,7 @@ /obj/item/fish/holo/clown name = "holographic clownfish" + fish_id = "holoclownfish" icon_state = "holo_clownfish" desc = "A holographic representation of a clownfish, or at least how they used to look like five centuries ago." dedicated_in_aquarium_icon_state = null @@ -86,6 +91,7 @@ /obj/item/fish/holo/checkered name = "unrendered holographic fish" + fish_id = "checkered" desc = "A checkered silhoutte of searing purple and pitch black presents itself before your eyes, like a tear in fabric of reality. It hurts to watch." icon_state = "checkered" //it's a meta joke, buddy. dedicated_in_aquarium_icon_state = null @@ -98,6 +104,7 @@ /obj/item/fish/holo/halffish name = "holographic half-fish" + fish_id = "halffish" desc = "A holographic representation of... a fish reduced to all bones, except for its head. Isn't it supposed to be dead? Ehr, holo-dead?" icon_state = "half_fish" dedicated_in_aquarium_icon_state = null diff --git a/code/modules/fishing/fish/types/mining.dm b/code/modules/fishing/fish/types/mining.dm index f3f4137fae3..41e240889da 100644 --- a/code/modules/fishing/fish/types/mining.dm +++ b/code/modules/fishing/fish/types/mining.dm @@ -1,6 +1,7 @@ /// Commonly found on the mining fishing spots. Can be grown into lobstrosities /obj/item/fish/chasm_crab name = "chasm chrab" + fish_id = "chasm_crab" desc = "The young of the lobstrosity mature in pools below the earth, eating what falls in until large enough to clamber out. Those found near the station are well-fed." icon_state = "chrab" sprite_height = 9 @@ -103,6 +104,7 @@ /obj/item/fish/chasm_crab/ice name = "arctic chrab" + fish_id = "arctic_crab" desc = "A subspecies of chasm chrabs that has adapted to the cold climate and lack of abysmal holes of the icemoon." icon_state = "arctic_chrab" required_temperature_min = ICEBOX_MIN_TEMPERATURE-20 @@ -114,6 +116,7 @@ /obj/item/fish/boned name = "unmarine bonemass" + fish_id = "bonemass" desc = "What one could mistake for fish remains, is in reality a species that chose to discard its weak flesh a long time ago. A living fossil, in its most literal sense." icon_state = "bonemass" sprite_width = 10 @@ -143,6 +146,7 @@ /obj/item/fish/lavaloop name = "lavaloop fish" + fish_id = "lavaloop" desc = "Due to its curvature, it can be used as make-shift boomerang." icon_state = "lava_loop" sprite_width = 3 @@ -159,6 +163,7 @@ /datum/fish_trait/carnivore, /datum/fish_trait/heavy, ) + compatible_types = list(/obj/item/fish/lavaloop/plasma_river) hitsound = null throwforce = 5 beauty = FISH_BEAUTY_GOOD @@ -200,6 +205,8 @@ return (target.mob_size >= MOB_SIZE_LARGE) /obj/item/fish/lavaloop/plasma_river + fish_id = "plasma_lavaloop" + compatible_types = list(/obj/item/fish/lavaloop) maximum_bonus = 30 /obj/item/fish/lavaloop/plasma_river/explode_on_user(mob/living/user) diff --git a/code/modules/fishing/fish/types/ruins.dm b/code/modules/fishing/fish/types/ruins.dm index ccf6560139c..b9166f4897a 100644 --- a/code/modules/fishing/fish/types/ruins.dm +++ b/code/modules/fishing/fish/types/ruins.dm @@ -1,6 +1,7 @@ ///From oil puddles from the elephant graveyard. Also an evolution of the "unmarine bonemass" /obj/item/fish/mastodon name = "unmarine mastodon" + fish_id = "mastodon" desc = "A monster of exposed muscles and innards, wrapped in a fish-like skeleton. You don't remember ever seeing it on the catalog." icon = 'icons/obj/aquarium/wide.dmi' icon_state = "mastodon" @@ -36,6 +37,7 @@ ///From the cursed spring /obj/item/fish/soul name = "soulfish" + fish_id = "soulfish" desc = "A distant yet vaguely close critter, like a long lost relative. You feel your soul rejuvenated just from looking at it... Also, what the fuck is this shit?!" icon_state = "soulfish" sprite_width = 7 @@ -69,6 +71,7 @@ ///From the cursed spring /obj/item/fish/skin_crab name = "skin crab" + fish_id = "skin_crab" desc = "\"And on the eighth day, a demential mockery of both humanity and crabity was made.\" Fascinating." icon_state = "skin_crab" sprite_width = 7 diff --git a/code/modules/fishing/fish/types/saltwater.dm b/code/modules/fishing/fish/types/saltwater.dm index 74f1d1d32b9..a28ad497f53 100644 --- a/code/modules/fishing/fish/types/saltwater.dm +++ b/code/modules/fishing/fish/types/saltwater.dm @@ -1,5 +1,6 @@ /obj/item/fish/clownfish name = "clownfish" + fish_id = "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" required_fluid_type = AQUARIUM_FLUID_SALTWATER @@ -19,6 +20,7 @@ /obj/item/fish/clownfish/lube name = "lubefish" + fish_id = "lube" desc = "A clownfish exposed to cherry-flavored lube for far too long. First discovered the days following a cargo incident around the seas of Europa, when thousands of thousands of thousands..." icon_state = "lubefish" random_case_rarity = FISH_RARITY_VERY_RARE @@ -31,6 +33,7 @@ /obj/item/fish/cardinal name = "cardinalfish" + fish_id = "cardinal" desc = "Cardinalfish are often found near sea urchins, where the fish hide when threatened." icon_state = "cardinalfish" sprite_width = 6 @@ -45,6 +48,7 @@ /obj/item/fish/greenchromis name = "green chromis" + fish_id = "greenchromis" desc = "The Chromis can vary in color from blue to green depending on the lighting and distance from the lights." icon_state = "greenchromis" sprite_width = 5 @@ -60,6 +64,7 @@ /obj/item/fish/firefish name = "firefish goby" + fish_id = "firefish" desc = "To communicate in the wild, the firefish uses its dorsal fin to alert others of potential danger." icon_state = "firefish" sprite_width = 5 @@ -75,6 +80,7 @@ /obj/item/fish/pufferfish name = "pufferfish" + fish_id = "pufferfish" desc = "They say that one pufferfish contains enough toxins to kill 30 people, although in the last few decades they've been genetically engineered en masse to be less poisonous." icon_state = "pufferfish" required_fluid_type = AQUARIUM_FLUID_SALTWATER @@ -91,6 +97,7 @@ /obj/item/fish/lanternfish name = "lanternfish" + fish_id = "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 @@ -107,6 +114,7 @@ /obj/item/fish/stingray name = "stingray" + fish_id = "stingray" desc = "A type of ray, most known for its venomous stinger. Despite that, They're normally docile, if not a bit easily frightened." icon_state = "stingray" stable_population = 4 @@ -121,6 +129,7 @@ /obj/item/fish/swordfish name = "swordfish" + fish_id = "swordfish" desc = "A large billfish, most famous for its elongated bill, while also fairly popular for cooking, and as a fearsome weapon in the hands of a veteran spess-fisherman." icon = 'icons/obj/aquarium/wide.dmi' icon_state = "swordfish" @@ -217,6 +226,7 @@ /obj/item/fish/squid name = "squid" + fish_id = "squid" desc = "An elongated mollusk with eight tentacles, natural camouflage and ink clouds to spray at predators. One of the most intelligent, well-equipped invertebrates out there." icon_state = "squid" sprite_width = 4 @@ -238,6 +248,7 @@ /obj/item/fish/monkfish name = "monkfish" + fish_id = "monkfish" desc = "A member of the Lophiid family of anglerfish. It goes by several different names, however none of them will make it look any prettier, nor be any less delicious." icon_state = "monkfish" required_fluid_type = AQUARIUM_FLUID_SALTWATER @@ -264,6 +275,7 @@ /obj/item/fish/plaice name = "plaice" + fish_id = "plaice" desc = "Perhaps the most prominent flatfish in the space-market. Nature really pulled out the rolling pin on this one." icon_state = "plaice" sprite_height = 7 diff --git a/code/modules/fishing/fish/types/station.dm b/code/modules/fishing/fish/types/station.dm index 94923f7dc1a..22e4c201a44 100644 --- a/code/modules/fishing/fish/types/station.dm +++ b/code/modules/fishing/fish/types/station.dm @@ -1,5 +1,6 @@ /obj/item/fish/ratfish name = "ratfish" + fish_id = "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" sprite_width = 7 @@ -41,6 +42,7 @@ /obj/item/fish/sludgefish name = "sludgefish" + fish_id = "sludgefish" desc = "A misshapen, fragile, loosely fish-like living goop, the only thing that'd ever thrive in the acidic and claustrophobic cavities of the station's organic waste disposal system." icon_state = "sludgefish" sprite_width = 7 @@ -68,7 +70,8 @@ fish_traits = list(/datum/fish_trait/parthenogenesis) /obj/item/fish/slimefish - name = "acquatic slime" + name = "aquatic slime" + fish_id = "slimefish" desc = "Kids, this is what happens when a slime overcomes its hydrophobic nature. It goes glug glug." icon_state = "slimefish" icon_state_dead = "slimefish_dead" @@ -103,6 +106,7 @@ /obj/item/fish/fryish name = "fryish" + fish_id = "fryish" desc = "A youngling of the Fritterish family of delicious extremophile, piscine lifeforms. Just don't tell 'Mankind for Ethical Animal Treatment' you ate it." icon_state = "fryish" sprite_width = 3 @@ -176,6 +180,7 @@ /obj/item/fish/fryish/fritterish name = "fritterish" + fish_id = "fritterish" desc = "A deliciously extremophile alien fish. This one looks like a taiyaki." icon_state = "fritterish" average_size = 50 @@ -230,6 +235,7 @@ /obj/item/fish/fryish/nessie name = "nessie-fish" + fish_id = "nessie" desc = "A deliciously extremophile alien fish. This one is so big, you could write legends about it." icon = 'icons/obj/aquarium/wide.dmi' icon_state = "nessiefish" diff --git a/code/modules/fishing/fish/types/syndicate.dm b/code/modules/fishing/fish/types/syndicate.dm index 8732bb8f0bd..1a88f2600a6 100644 --- a/code/modules/fishing/fish/types/syndicate.dm +++ b/code/modules/fishing/fish/types/syndicate.dm @@ -1,6 +1,7 @@ ///Contains fish that can be found in the syndicate fishing portal setting as well as the ominous fish case. /obj/item/fish/emulsijack name = "toxic emulsijack" + fish_id = "emulsijack" desc = "Ah, the terrifying emulsijack. Created in a laboratory, the only real use of this slimey, scaleless fish is for completely ruining a tank." icon_state = "emulsijack" random_case_rarity = FISH_RARITY_GOOD_LUCK_FINDING_THIS @@ -18,6 +19,7 @@ /obj/item/fish/donkfish name = "donk co. company patent donkfish" + fish_id = "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" random_case_rarity = FISH_RARITY_VERY_RARE @@ -32,6 +34,7 @@ /obj/item/fish/jumpercable name = "monocloning jumpercable" + fish_id = "jumpercable" desc = "A surprisingly useful if nasty looking creation from the syndicate fish labs. Drop one in a tank, and \ watch it self-feed and multiply. Generates more and more power as a growing swarm!" icon_state = "jumpercable" @@ -54,6 +57,7 @@ /obj/item/fish/chainsawfish name = "chainsawfish" + fish_id = "chainsawfish" desc = "A very, very angry bioweapon, whose sole purpose is to rip and tear." icon = 'icons/obj/aquarium/wide.dmi' icon_state = "chainsawfish" @@ -175,6 +179,7 @@ /obj/item/fish/pike/armored name = "armored pike" + fish_id = "armored_pike" desc = "A long-bodied, metal-clad predator with a snout that almost looks like an halberd. Definitely a weapon to swing around." icon_state = "armored_pike" inhand_icon_state = "armored_pike" diff --git a/code/modules/fishing/fish/types/tiziran.dm b/code/modules/fishing/fish/types/tiziran.dm index bd0216f9e1a..5f90cedc63d 100644 --- a/code/modules/fishing/fish/types/tiziran.dm +++ b/code/modules/fishing/fish/types/tiziran.dm @@ -2,6 +2,7 @@ /obj/item/fish/dwarf_moonfish name = "dwarf moonfish" + fish_id = "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" sprite_height = 6 @@ -17,6 +18,7 @@ /obj/item/fish/gunner_jellyfish name = "gunner jellyfish" + fish_id = "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" sprite_height = 4 @@ -41,6 +43,7 @@ /obj/item/fish/needlefish name = "needlefish" + fish_id = "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" sprite_height = 3 @@ -61,6 +64,7 @@ /obj/item/fish/armorfish name = "armorfish" + fish_id = "armorfish" desc = "A small shellfish native to Tizira's oceans, known for its exceptionally hard shell. Consumed similarly to prawns." icon_state = "armorfish" sprite_height = 5 diff --git a/code/modules/fishing/fishing_minigame.dm b/code/modules/fishing/fishing_minigame.dm index 8d4b312c4f6..a0bdbfcc9f9 100644 --- a/code/modules/fishing/fishing_minigame.dm +++ b/code/modules/fishing/fishing_minigame.dm @@ -351,6 +351,11 @@ GLOBAL_LIST_EMPTY(fishing_challenges_by_user) if(win) if(reward_path != FISHING_DUD) playsound(location, 'sound/effects/bigsplash.ogg', 100) + if(ispath(reward_path, /obj/item/fish)) + var/obj/item/fish/fish_reward = reward_path + var/fish_id = initial(fish_reward.fish_id) + if(fish_id) + user.client?.give_award(/datum/award/score/progress/fish, user, initial(fish_reward.fish_id)) SEND_SIGNAL(user, COMSIG_MOB_COMPLETE_FISHING, src, win) if(!QDELETED(src)) qdel(src) diff --git a/code/modules/fishing/sources/_fish_source.dm b/code/modules/fishing/sources/_fish_source.dm index 2ad6a2bc5bb..71780e76f11 100644 --- a/code/modules/fishing/sources/_fish_source.dm +++ b/code/modules/fishing/sources/_fish_source.dm @@ -98,7 +98,6 @@ GLOBAL_LIST_INIT(specific_fish_icons, generate_specific_fish_icons()) /obj/structure/closet/crate/necropolis/tendril, )) - ///List of multipliers used to make fishes more common compared to everything else depending on bait quality, indexed from best to worst. var/static/weight_result_multiplier = list( TRAIT_GREAT_QUALITY_BAIT = 9, diff --git a/code/modules/unit_tests/fish_unit_tests.dm b/code/modules/unit_tests/fish_unit_tests.dm index e21b3fec4c0..94337ae2cc2 100644 --- a/code/modules/unit_tests/fish_unit_tests.dm +++ b/code/modules/unit_tests/fish_unit_tests.dm @@ -421,8 +421,10 @@ /datum/fish_source/unit_test_profound_fisher fish_table = list(/obj/item/fish/testdummy = 1) fish_counts = list(/obj/item/fish/testdummy = 2) + fish_source_flags = parent_type::fish_source_flags | FISH_SOURCE_FLAG_SKIP_CATCHABLES /datum/fish_source/unit_test_all_fish + fish_source_flags = parent_type::fish_source_flags | FISH_SOURCE_FLAG_SKIP_CATCHABLES /datum/fish_source/unit_test_all_fish/New() for(var/fish_type as anything in subtypesof(/obj/item/fish)) diff --git a/tgui/packages/tgui/interfaces/Achievements.jsx b/tgui/packages/tgui/interfaces/Achievements.jsx deleted file mode 100644 index ccb7c2807f6..00000000000 --- a/tgui/packages/tgui/interfaces/Achievements.jsx +++ /dev/null @@ -1,147 +0,0 @@ -import { useState } from 'react'; -import { Box, Flex, Icon, Table, Tabs, Tooltip } from 'tgui-core/components'; - -import { useBackend } from '../backend'; -import { Window } from '../layouts'; - -export const Achievements = (props) => { - const { data } = useBackend(); - const { categories } = data; - const [selectedCategory, setSelectedCategory] = useState(categories[0]); - const achievements = data.achievements.filter( - (x) => x.category === selectedCategory, - ); - return ( - - - - {categories.map((category) => ( - setSelectedCategory(category)} - > - {category} - - ))} - setSelectedCategory('High Scores')} - > - High Scores - - - {(selectedCategory === 'High Scores' && ) || ( - - )} - - - ); -}; - -const AchievementTable = (props) => { - const { achievements } = props; - return ( - - {achievements.map((achievement) => ( - - ))} -
- ); -}; - -const Achievement = (props) => { - const { achievement } = props; - const { - name, - desc, - icon_class, - value, - score, - achieve_info, - achieve_tooltip, - } = achievement; - return ( - - - - - -

{name}

- {desc} - {(score && ( - 0 ? 'good' : 'bad'}> - {value > 0 ? `Earned ${value} times` : 'Locked'} - - )) || ( - - {value ? 'Unlocked' : 'Locked'} - - )} - {!!achieve_info && ( - - - {achieve_info} - - - )} -
-
- ); -}; - -const HighScoreTable = (props) => { - const { data } = useBackend(); - const { highscore: highscores, user_ckey } = data; - const [highScoreIndex, setHighScoreIndex] = useState(0); - const highscore = highscores[highScoreIndex]; - if (!highscore) { - return null; - } - const scores = Object.keys(highscore.scores).map((key) => ({ - ckey: key, - value: highscore.scores[key], - })); - return ( - - - - {highscores.map((highscore, i) => ( - setHighScoreIndex(i)} - > - {highscore.name} - - ))} - - - - - - # - Key - Score - - {scores.map((score, i) => ( - - - {i + 1} - - - {i === 0 && } - {score.ckey} - {i === 0 && } - - {score.value} - - ))} -
-
-
- ); -}; diff --git a/tgui/packages/tgui/interfaces/Achievements.tsx b/tgui/packages/tgui/interfaces/Achievements.tsx new file mode 100644 index 00000000000..fd98e61e986 --- /dev/null +++ b/tgui/packages/tgui/interfaces/Achievements.tsx @@ -0,0 +1,255 @@ +import { useState } from 'react'; +import { + Box, + Flex, + Icon, + Image, + ProgressBar, + Table, + Tabs, + Tooltip, +} from 'tgui-core/components'; +import { BooleanLike } from 'tgui-core/react'; + +import { useBackend } from '../backend'; +import { Window } from '../layouts'; + +type Data = { + categories: string[]; + achievements: Achievement[]; + highscores: Highscore[]; + progresses: Progress[]; + user_key: string; +}; + +type Achievement = { + name: string; + desc: string; + category: string; + icon_class: string; + value: number; + score: BooleanLike; + achieve_info: string; + achieve_tooltip: string; +}; + +type Highscore = { + name: string; + scores: Score[]; +}; + +type Score = { + ckey: string; + value: number; +}; + +type Progress = { + name: string; + value_text: string; + percent: number; + entries: ProgEntry[]; +}; + +type ProgEntry = { + name: string; + icon: string; + height: number; + width: number; +}; + +export const Achievements = (props) => { + const { data } = useBackend(); + const { categories } = data; + const [selectedCategory, setSelectedCategory] = useState(categories[0]); + return ( + + + + {categories.map((category) => ( + setSelectedCategory(category)} + > + {category} + + ))} + setSelectedCategory('High Scores')} + > + High Scores + + setSelectedCategory('Progress')} + > + Progress + + + {(selectedCategory === 'High Scores' && ) || + (selectedCategory === 'Progress' && ) || ( + + )} + + + ); +}; + +const AchievementTable = (props) => { + const { data } = useBackend(); + const { achievements } = data; + const { category } = props; + const filtered_achievements = achievements.filter( + (x) => x.category === category, + ); + return ( + + {filtered_achievements.map((achievement) => ( + + + + + +

{achievement.name}

+ {achievement.desc} + {(achievement.score && ( + 0 ? 'good' : 'bad'}> + {achievement.value > 0 + ? `Earned ${achievement.value} times` + : 'Locked'} + + )) || ( + + {achievement.value ? 'Unlocked' : 'Locked'} + + )} + {!!achievement.achieve_info && ( + + + {achievement.achieve_info} + + + )} +
+
+ ))} +
+ ); +}; + +const ProgressTable = () => { + const { data } = useBackend(); + const { progresses } = data; + const [progressIndex, setProgressIndex] = useState(0); + if (!progresses || progresses.length === 0) { + return null; + } + const progress: Progress = progresses[progressIndex]; + return ( + + + + {progresses.map((progress, i) => ( + setProgressIndex(i)} + > + {progress.name} + + ))} + + + + + + {progress.percent >= 0.97 && ( + + )} + {progress.value_text} + {progress.percent >= 0.98 && ( + + )} + + + + {progress.entries.map((entry, i) => ( + + + + + + + {entry.name} + + + + ))} +
+
+
+ ); +}; + +const HighScoreTable = () => { + const { data } = useBackend(); + const { highscores, user_key } = data; + const [highScoreIndex, setHighScoreIndex] = useState(0); + if (!highscores || highscores.length === 0) { + return null; + } + const highscore: Highscore = highscores[highScoreIndex]; + return ( + + + + {highscores.map((highscore, i) => ( + setHighScoreIndex(i)} + > + {highscore.name} + + ))} + + + + + + # + Key + Score + + {highscore.scores.map((score, i) => ( + + + {i + 1} + + + {i === 0 && } + {score.ckey} + {i === 0 && } + + {score.value} + + ))} +
+
+
+ ); +};