Merge branch 'Skyrat-SS13:master' into master

This commit is contained in:
ReturnToZender
2023-01-11 18:59:15 -06:00
committed by GitHub
77 changed files with 1509 additions and 1038 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -785,7 +785,8 @@
/datum/crafting_recipe/paperframes
name = "Paper Frames"
result = /obj/item/stack/sheet/paperframes/five
result = /obj/item/stack/sheet/paperframes
result_amount = 5
time = 1 SECONDS
reqs = list(/obj/item/stack/sheet/mineral/wood = 5, /obj/item/paper = 20)
category = CAT_STRUCTURE
@@ -814,7 +815,8 @@
/datum/crafting_recipe/blackcarpet
name = "Black Carpet"
reqs = list(/obj/item/stack/tile/carpet = 50, /obj/item/toy/crayon/black = 1)
result = /obj/item/stack/tile/carpet/black/fifty
result = /obj/item/stack/tile/carpet/black
result_amount = 50
category = CAT_TILES
/datum/crafting_recipe/curtain
+1 -1
View File
@@ -219,7 +219,7 @@
// wield update status
wielded = FALSE
UnregisterSignal(user, COMSIG_MOB_SWAP_HANDS)
UnregisterSignal(user, COMSIG_MOB_SWAPPING_HANDS)
SEND_SIGNAL(parent, COMSIG_TWOHANDED_UNWIELD, user)
REMOVE_TRAIT(parent, TRAIT_WIELDED, REF(src))
unwield_callback?.Invoke(parent, user)
@@ -465,6 +465,11 @@
icon_file = 'icons/obj/sofa.dmi'
json_config = 'code/datums/greyscale/json_configs/bench_right.json'
/datum/greyscale_config/bench_solo
name = "Bench Right"
icon_file = 'icons/obj/sofa.dmi'
json_config = 'code/datums/greyscale/json_configs/bench_solo.json'
/datum/greyscale_config/bench_corner
name = "Bench Corner"
icon_file = 'icons/obj/sofa.dmi'
@@ -0,0 +1,15 @@
{
"bench_solo": [
{
"type": "icon_state",
"icon_state": "bench_solo_base",
"blend_mode": "overlay"
},
{
"type": "icon_state",
"icon_state": "bench_solo_cover",
"blend_mode": "overlay",
"color_ids": [ 1 ]
}
]
}
+5 -3
View File
@@ -1,6 +1,7 @@
/**
* A savefile implementation that handles all data using json.
* Also saves it using JSON too, fancy.
* If you pass in a null path, it simply acts as a memory tree instead, and cannot be saved.
*/
/datum/json_savefile
var/path = ""
@@ -13,7 +14,7 @@ GENERAL_PROTECT_DATUM(/datum/json_savefile)
/datum/json_savefile/New(path)
src.path = path
tree = list()
if(fexists(path))
if(path && fexists(path))
load()
/**
@@ -43,7 +44,7 @@ GENERAL_PROTECT_DATUM(/datum/json_savefile)
tree?.Cut()
/datum/json_savefile/proc/load()
if(!fexists(path))
if(!path || !fexists(path))
return FALSE
try
tree = json_decode(rustg_file_read(path))
@@ -53,7 +54,8 @@ GENERAL_PROTECT_DATUM(/datum/json_savefile)
return FALSE
/datum/json_savefile/proc/save()
rustg_file_write(json_encode(tree), path)
if(path)
rustg_file_write(json_encode(tree), path)
/// Traverses the entire dir tree of the given savefile and dynamically assembles the tree from it
/datum/json_savefile/proc/import_byond_savefile(savefile/savefile)
@@ -203,7 +203,6 @@
return
//install board
if(install_board(target_board, user, FALSE))
user.Beam(src, icon_state = "rped_upgrade", time = 5)
replacer.play_rped_sound()
//attack this frame again with the rped so it can install stock parts since its now in state 3
attackby(replacer, user, params)
@@ -141,3 +141,9 @@
/obj/structure/sign/plaques/kiddie/gameoflife
name = "\improper Conway's The Game Of Life plaque"
desc = "A plaque detailing the historical significance of The Game Of Life in the field of computer science, and that the mural underfoot is a representation of the game in action."
/obj/structure/sign/plaques/tram
name = "\improper tram information plate"
desc = "A plate showing details from the manufacturer about this Nakamura Engineering SkyyTram Mk IV, serial number LT301TG2563. We are not responsible for any injuries or fatalites caused by usage of the tram. For technical assistance in the event of malfunction please contact: (the info seems to be scratched up to the point of being unreadable.)"
icon_state = "commission_tram"
custom_materials = list(/datum/material/titanium = 2000)
+21 -15
View File
@@ -2,20 +2,26 @@ GLOBAL_LIST_EMPTY(preferences_datums)
/datum/preferences
var/client/parent
//doohickeys for savefiles
/// The path to the general savefile for this datum
var/path
var/default_slot = 1 //Holder so it doesn't default to slot 1, rather the last one used
var/max_save_slots = 30 //SKYRAT EDIT CHANGE
/// Whether or not we allow saving/loading. Used for guests, if they're enabled
var/load_and_save = TRUE
/// Ensures that we always load the last used save, QOL
var/default_slot = 1
/// The maximum number of slots we're allowed to contain
var/max_save_slots = 30 //SKYRAT EDIT - ORIGINAL 3
//non-preference stuff
var/muted = 0
/// Bitflags for communications that are muted
var/muted = NONE
/// Last IP that this client has connected from
var/last_ip
/// Last CID that this client has connected from
var/last_id
//game-preferences
var/lastchangelog = "" //Saved changlog filesize to detect if there was a change
/// Cached changelog size, to detect new changelogs since last join
var/lastchangelog = ""
//Antag preferences
/// List of ROLE_X that the client wants to be eligible for
var/list/be_special = list() //Special role selection
/// Custom keybindings. Map of keybind names to keyboard inputs.
@@ -99,13 +105,13 @@ GLOBAL_LIST_EMPTY(preferences_datums)
middleware += new middleware_type(src)
if(IS_CLIENT_OR_MOCK(parent))
if(!is_guest_key(parent.key))
load_path(parent.ckey)
if(!fexists(path))
try_savefile_type_migration()
unlock_content = !!parent.IsByondMember()
if(unlock_content)
max_save_slots = 40 //SKYRAT EDIT CHANGE
load_and_save = !is_guest_key(parent.key)
load_path(parent.ckey)
if(load_and_save && !fexists(path))
try_savefile_type_migration()
unlock_content = !!parent.IsByondMember()
if(unlock_content)
max_save_slots = 40 //SKYRAT EDIT - ORIGINAL 8
else
CRASH("attempted to create a preferences datum without a client or mock!")
load_savefile()
+4 -6
View File
@@ -141,14 +141,14 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
to_chat(parent, span_danger("[conflicted.category]: [conflicted.full_name] needs updating"))
/datum/preferences/proc/load_path(ckey, filename="preferences.json")
if(!ckey)
if(!ckey || !load_and_save)
return
path = "data/player_saves/[ckey[1]]/[ckey]/[filename]"
/datum/preferences/proc/load_savefile()
if(!path)
if(load_and_save && !path)
CRASH("Attempted to load savefile without first loading a path!")
savefile = new /datum/json_savefile(path)
savefile = new /datum/json_savefile(load_and_save ? path : null)
/datum/preferences/proc/load_preferences()
if(!savefile)
@@ -159,7 +159,7 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
return FALSE
var/needs_update = save_data_needs_update(savefile.get_entry())
if(needs_update == -2) //fatal, can't load any data
if(load_and_save && (needs_update == -2)) //fatal, can't load any data
var/bacpath = "[path].updatebac" //todo: if the savefile version is higher then the server, check the backup, and give the player a prompt to load the backup
if (fexists(bacpath))
fdel(bacpath) //only keep 1 version of backup
@@ -264,7 +264,6 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
/datum/preferences/proc/load_character(slot)
SHOULD_NOT_SLEEP(TRUE)
if(!slot)
slot = default_slot
slot = sanitize_integer(slot, 1, max_save_slots, initial(default_slot))
@@ -320,7 +319,6 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
/datum/preferences/proc/save_character()
SHOULD_NOT_SLEEP(TRUE)
if(!path)
return FALSE
var/tree_key = "character[default_slot]"
@@ -821,11 +821,26 @@ GLOBAL_LIST_EMPTY(lifts)
/obj/structure/industrial_lift/tram/white
icon_state = "titanium_white"
/obj/structure/industrial_lift/tram/subfloor
name = "tram subfloor"
desc = "A sturdy looking thermoplastic subfloor the tram is built on."
icon_state = "tram_subfloor"
/datum/armor/structure_industrial_lift
melee = 50
fire = 80
acid = 50
/obj/structure/industrial_lift/tram/accessible
icon_state = "titanium_accessible_north"
/obj/structure/industrial_lift/tram/accessible/north
icon_state = "titanium_accessible_north"
/obj/structure/industrial_lift/tram/accessible/south
icon_state = "titanium_accessible_south"
/obj/structure/industrial_lift/tram/AddItemOnLift(datum/source, atom/movable/AM)
. = ..()
if(travelling)
@@ -5,6 +5,7 @@
base_icon_state = "tram_"
icon_screen = "tram_Central Wing_idle"
icon_keyboard = null
layer = SIGN_LAYER
circuit = /obj/item/circuitboard/computer/tram_controls
flags_1 = NODECONSTRUCT_1 | SUPERMATTER_IGNORES_1
resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF
@@ -197,3 +198,5 @@
/obj/item/circuit_component/tram_controls/proc/on_tram_travel(datum/source, obj/effect/landmark/tram/from_where, obj/effect/landmark/tram/to_where)
SIGNAL_HANDLER
location.set_output(to_where.name)
MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/computer/tram_controls, 0)
@@ -31,15 +31,26 @@
greyscale_colors = "#00CCFF"
/obj/structure/chair/sofa/bench/tram/left
name = "tram seating"
desc = "Not the most comfortable, but easy to keep clean!"
icon_state = "bench_left"
greyscale_config = /datum/greyscale_config/bench_left
greyscale_colors = "#00CCFF"
/obj/structure/chair/sofa/bench/tram/right
name = "tram seating"
desc = "Not the most comfortable, but easy to keep clean!"
icon_state = "bench_right"
greyscale_config = /datum/greyscale_config/bench_right
greyscale_colors = "#00CCFF"
/obj/structure/chair/sofa/bench/tram/solo
name = "tram seating"
desc = "Not the most comfortable, but easy to keep clean!"
icon_state = "bench_solo"
greyscale_config = /datum/greyscale_config/bench_solo
greyscale_colors = "#00CCFF"
/turf/open/floor/glass/reinforced/tram
name = "tram bridge"
desc = "It shakes a bit when you step, but lets you cross between sides quickly!"
@@ -49,6 +60,8 @@
icon = 'icons/obj/doors/tramdoor.dmi'
var/associated_lift = MAIN_STATION_TRAM
var/datum/weakref/tram_ref
/// Directions the tram door can be forced open in an emergency
var/space_dir = null
name = "tram door"
desc = "Probably won't crush you if you try to rush them as they close. But we know you live on that danger, try and beat the tram!"
@@ -72,6 +85,17 @@
INVOKE_ASYNC(src, PROC_REF(open))
find_tram()
/obj/machinery/door/window/tram/examine(mob/user)
. = ..()
. += span_notice("It has labels indicating that it has an emergency mechanism to open from the inside using <b>just your hands</b> in the event of an emergency.")
/obj/machinery/door/window/tram/try_safety_unlock(mob/user)
if(!hasPower())
to_chat(user, span_notice("You begin pulling the tram emergency exit handle..."))
if(do_after(user, 15 SECONDS, target = src))
try_to_crowbar(null, user, TRUE)
return TRUE
/obj/machinery/door/window/tram/open_and_close()
var/datum/lift_master/tram/tram_part = tram_ref?.resolve()
if(!open())
-3
View File
@@ -35,9 +35,6 @@
/// Supervisors, who this person answers to directly
var/supervisors = ""
/// Selection screen color
var/selection_color = "#ffffff"
/// What kind of mob type joining players with this job as their assigned role are spawned as.
var/spawn_type = /mob/living/carbon/human
-1
View File
@@ -6,7 +6,6 @@
total_positions = 1
spawn_positions = 1
supervisors = "your laws"
selection_color = "#ccffcc"
spawn_type = /mob/living/silicon/ai
req_admin_notify = TRUE
minimal_player_age = 30
-1
View File
@@ -10,7 +10,6 @@ Assistant
total_positions = 5
spawn_positions = 5
supervisors = "absolutely everyone"
selection_color = "#dddddd"
exp_granted_type = EXP_TYPE_CREW
outfit = /datum/outfit/job/assistant
plasmaman_outfit = /datum/outfit/plasmaman
@@ -6,7 +6,6 @@
total_positions = 3
spawn_positions = 2
supervisors = SUPERVISOR_CE
selection_color = "#fff5cc"
exp_requirements = 60
exp_required_type = EXP_TYPE_CREW
exp_granted_type = EXP_TYPE_CREW
-1
View File
@@ -6,7 +6,6 @@
total_positions = 1
spawn_positions = 1
supervisors = SUPERVISOR_HOP
selection_color = "#bbe291"
exp_granted_type = EXP_TYPE_CREW
config_tag = "BARTENDER"
-1
View File
@@ -6,7 +6,6 @@
total_positions = 3
spawn_positions = 2
supervisors = SUPERVISOR_HOP
selection_color = "#bbe291"
exp_granted_type = EXP_TYPE_CREW
config_tag = "BOTANIST"
-1
View File
@@ -9,7 +9,6 @@
total_positions = 1
spawn_positions = 1
supervisors = "Nanotrasen officials and Space Law"
selection_color = "#ccccff"
req_admin_notify = 1
minimal_player_age = 14
exp_requirements = 180
@@ -8,7 +8,6 @@
total_positions = 3
spawn_positions = 2
supervisors = SUPERVISOR_QM
selection_color = "#dcba97"
exp_granted_type = EXP_TYPE_CREW
config_tag = "CARGO_TECHNICIAN"
-1
View File
@@ -7,7 +7,6 @@
total_positions = 1
spawn_positions = 1
supervisors = SUPERVISOR_HOP
selection_color = "#bbe291"
exp_granted_type = EXP_TYPE_CREW
config_tag = "CHAPLAIN"
-1
View File
@@ -7,7 +7,6 @@
total_positions = 2
spawn_positions = 2
supervisors = SUPERVISOR_CMO
selection_color = "#ffeef0"
exp_requirements = 60
exp_required_type = EXP_TYPE_CREW
exp_granted_type = EXP_TYPE_CREW
@@ -9,7 +9,6 @@
total_positions = 1
spawn_positions = 1
supervisors = SUPERVISOR_CAPTAIN
selection_color = "#ffeeaa"
req_admin_notify = 1
minimal_player_age = 7
exp_requirements = 180
@@ -9,7 +9,6 @@
total_positions = 1
spawn_positions = 1
supervisors = SUPERVISOR_CAPTAIN
selection_color = "#ffddf0"
req_admin_notify = 1
minimal_player_age = 7
exp_requirements = 180
-1
View File
@@ -6,7 +6,6 @@
total_positions = 1
spawn_positions = 1
supervisors = SUPERVISOR_HOP
selection_color = "#bbe291"
exp_granted_type = EXP_TYPE_CREW
config_tag = "CLOWN"
-1
View File
@@ -6,7 +6,6 @@
total_positions = 2
spawn_positions = 1
supervisors = SUPERVISOR_HOP
selection_color = "#bbe291"
exp_granted_type = EXP_TYPE_CREW
config_tag = "COOK"
var/cooks = 0 //Counts cooks amount
-1
View File
@@ -7,7 +7,6 @@
total_positions = 1
spawn_positions = 1
supervisors = SUPERVISOR_HOP
selection_color = "#bbe291"
config_tag = "CURATOR"
exp_granted_type = EXP_TYPE_CREW
-1
View File
@@ -6,7 +6,6 @@
total_positions = 3 // SKYRAT EDIT: Original value (0)
spawn_positions = 3 // SKYRAT EDIT: Original value (1)
supervisors = "your laws and the AI" //Nodrak
selection_color = "#ddffdd"
spawn_type = /mob/living/silicon/robot
minimal_player_age = 21
exp_requirements = 120
-1
View File
@@ -8,7 +8,6 @@
total_positions = 1
spawn_positions = 1
supervisors = SUPERVISOR_HOS
selection_color = "#ffeeee"
minimal_player_age = 7
exp_requirements = 300
exp_required_type = EXP_TYPE_CREW
@@ -6,7 +6,6 @@
total_positions = 2
spawn_positions = 2
supervisors = SUPERVISOR_RD
selection_color = "#ffeeff"
exp_requirements = 60
exp_required_type = EXP_TYPE_CREW
exp_granted_type = EXP_TYPE_CREW
@@ -9,7 +9,6 @@
total_positions = 1
spawn_positions = 1
supervisors = SUPERVISOR_HOP
selection_color = "#ddddff"
req_admin_notify = 1
minimal_player_age = 10
exp_requirements = 180
@@ -9,7 +9,6 @@
total_positions = 1
spawn_positions = 1
supervisors = SUPERVISOR_CAPTAIN
selection_color = "#ffdddd"
req_admin_notify = 1
minimal_player_age = 14
exp_requirements = 300
-1
View File
@@ -6,7 +6,6 @@
total_positions = 2
spawn_positions = 1
supervisors = SUPERVISOR_HOP
selection_color = "#bbe291"
exp_granted_type = EXP_TYPE_CREW
config_tag = "JANITOR"
-1
View File
@@ -7,7 +7,6 @@
total_positions = 2
spawn_positions = 2
supervisors = SUPERVISOR_HOP
selection_color = "#bbe291"
exp_granted_type = EXP_TYPE_CREW
config_tag = "LAWYER"
@@ -7,7 +7,6 @@
total_positions = 5
spawn_positions = 3
supervisors = SUPERVISOR_CMO
selection_color = "#ffeef0"
exp_granted_type = EXP_TYPE_CREW
config_tag = "MEDICAL_DOCTOR"
-1
View File
@@ -6,7 +6,6 @@
total_positions = 1
spawn_positions = 1
supervisors = SUPERVISOR_HOP
selection_color = "#bbe291"
exp_granted_type = EXP_TYPE_CREW
config_tag = "MIME"
-1
View File
@@ -7,7 +7,6 @@
total_positions = 2
spawn_positions = 2
supervisors = SUPERVISOR_CMO
selection_color = "#ffeef0"
exp_granted_type = EXP_TYPE_CREW
config_tag = "PARAMEDIC"
-1
View File
@@ -6,7 +6,6 @@
total_positions = 12 // SKYRAT EDIT: Original value (0)
spawn_positions = 2
supervisors = "the security team"
selection_color = "#ffe1c3"
exp_granted_type = EXP_TYPE_CREW
paycheck = PAYCHECK_LOWER
config_tag = "PRISONER"
@@ -7,7 +7,6 @@
total_positions = 1
spawn_positions = 1
supervisors = "the Head of Personnel and the Chief Medical Officer"
selection_color = "#bbe291"
exp_granted_type = EXP_TYPE_CREW
config_tag = "PSYCHOLOGIST"
@@ -9,7 +9,6 @@
spawn_positions = 1
minimal_player_age = 7
supervisors = SUPERVISOR_CAPTAIN
selection_color = "#d7b088"
exp_required_type_department = EXP_TYPE_SUPPLY
exp_granted_type = EXP_TYPE_CREW
config_tag = "QUARTERMASTER"
@@ -10,7 +10,6 @@
total_positions = 1
spawn_positions = 1
supervisors = SUPERVISOR_CAPTAIN
selection_color = "#ffddff"
req_admin_notify = 1
minimal_player_age = 7
exp_required_type_department = EXP_TYPE_SCIENCE
@@ -6,7 +6,6 @@
total_positions = 2
spawn_positions = 2
supervisors = SUPERVISOR_RD
selection_color = "#ffeeff"
exp_requirements = 60
exp_required_type = EXP_TYPE_CREW
exp_granted_type = EXP_TYPE_CREW
-1
View File
@@ -6,7 +6,6 @@
total_positions = 5
spawn_positions = 3
supervisors = SUPERVISOR_RD
selection_color = "#ffeeff"
exp_requirements = 60
exp_required_type = EXP_TYPE_CREW
exp_granted_type = EXP_TYPE_CREW
@@ -8,7 +8,6 @@
total_positions = 8 //Handled in /datum/controller/occupations/proc/setup_officer_positions() //SKYRAT EDIT: SET TO 8, WAS 5
spawn_positions = 8 //Handled in /datum/controller/occupations/proc/setup_officer_positions() //SKYRAT EDIT: SEE ABOVE
supervisors = "the Head of Security, and the head of your assigned department (if applicable)"
selection_color = "#ffeeee"
minimal_player_age = 7
exp_requirements = 300
exp_required_type = EXP_TYPE_CREW
@@ -7,7 +7,6 @@
total_positions = 3
spawn_positions = 3
supervisors = SUPERVISOR_QM
selection_color = "#dcba97"
exp_granted_type = EXP_TYPE_CREW
config_tag = "SHAFT_MINER"
@@ -7,7 +7,6 @@
total_positions = 5
spawn_positions = 5
supervisors = SUPERVISOR_CE
selection_color = "#fff5cc"
exp_requirements = 60
exp_required_type = EXP_TYPE_CREW
exp_granted_type = EXP_TYPE_CREW
@@ -7,7 +7,6 @@
total_positions = 1
spawn_positions = 1
supervisors = SUPERVISOR_CMO
selection_color = "#ffeef0"
exp_requirements = 60
exp_required_type = EXP_TYPE_CREW
exp_granted_type = EXP_TYPE_CREW
-1
View File
@@ -9,7 +9,6 @@
total_positions = 1
spawn_positions = 1
supervisors = SUPERVISOR_HOS
selection_color = "#ffeeee"
minimal_player_age = 7
exp_requirements = 300
exp_required_type = EXP_TYPE_CREW
@@ -103,6 +103,10 @@
name = "Afro (Large)"
icon_state = "hair_bigafro"
/datum/sprite_accessory/hair/allthefuzz
name = "All The Fuzz"
icon_state = "hair_allthefuzz"
/datum/sprite_accessory/hair/antenna
name = "Ahoge"
icon_state = "hair_antenna"
@@ -128,7 +132,7 @@
icon_state = "hair_bedheadv3"
/datum/sprite_accessory/hair/bedheadv4
name = "BedHead 4X"
name = "Bedhead 4x"
icon_state = "hair_bedheadv4"
/datum/sprite_accessory/hair/bedheadlong
@@ -139,8 +143,8 @@
name = "Floorlength Bedhead"
icon_state = "hair_floorlength_bedhead"
/datum/sprite_accessory/hair/BadlyCut
name = "Marginally shorter yet Long Bedhead"
/datum/sprite_accessory/hair/badlycut
name = "Shorter Long Bedhead"
icon_state = "hair_verybadlycut"
/datum/sprite_accessory/hair/beehive
@@ -527,6 +531,10 @@
name = "Over Eye"
icon_state = "hair_shortovereye"
/datum/sprite_accessory/hair/hair_overeyetwo
name = "Over Eye 2"
icon_state = "hair_overeyetwo"
/datum/sprite_accessory/hair/oxton
name = "Oxton"
icon_state = "hair_oxton"
+2 -3
View File
@@ -55,9 +55,6 @@ If you create T5+ please take a pass at mech_fabricator.dm. The parts being good
if(!ismachinery(attacked_object) && !istype(attacked_object, /obj/structure/frame/machine))
return ..()
if(adjacent)
return ..()
if(ismachinery(attacked_object))
var/obj/machinery/attacked_machinery = attacked_object
@@ -70,6 +67,8 @@ If you create T5+ please take a pass at mech_fabricator.dm. The parts being good
return
var/obj/structure/frame/machine/attacked_frame = attacked_object
if(!adjacent && !works_from_distance)
return
// no point attacking the frame with the rped if the frame doesn't have wiring or it doesn't have components & rped has no circuitboard to offer as an component.
if(attacked_frame.state == 1 || (!attacked_frame.components && !has_an_circuitboard()))
return
+38
View File
@@ -433,3 +433,41 @@
- rscadd: RPED can now pick up & install machine circuit boards in machine frames
- rscadd: RPED can pick up computer circuit boards for storage convenience but doesn't
install them
2023-01-12:
GoldenAlpharex:
- code_imp: Made the worn_icons unit test check the worn GAGS config rather than
the normal one.
LT3:
- imageadd: Tram controls are now side mounted, freeing up space. Even better, you
now have two of them!
- imageadd: Added a new accessible space on the tram for wheelchairs
- bugfix: Removed duplicate tram plate/platforms
- code_imp: Tram has its own floor subtype for the walls and windows
- rscadd: Emergency exit feature added for people who are silly enough to not carry
a crowbar
MiiyaBot:
- rscadd: Two new hairstyles! ("All the Fuzz" & "Over Eye (Izutsumi)")
- spellcheck: Renamed "Marginally shorter yet long bedhead" to "Shorter Long Bedhead"
- spellcheck: Renamed "BedHead 4X" to "Bedhead 4x"
- bugfix: Slight changes to "Jade", "Shorter Bedhead", "Bedhead 4x"
RatFromTheJungle:
- bugfix: the detective revolver is once more, visible at all times
ShizCalev:
- bugfix: Fixed Black Carpet and Paper Frames crafting recipes only giving you 1
item in return.
SkyratBot:
- server: Preferences now support guests
- bugfix: fixes twohanded items not letting you switch hands
- bugfix: Chemists can now properly open and close their desk windoor on Deltastation
- bugfix: normal RPED from behaving like an BRPED when installing stock parts in
an incomplete machine frame
dawsonkeyes:
- qol: marked one is now harder and makes clang noises and talks to signal attacks
- bugfix: removed useless berserker armor vars
stanalbatross, thedonkified:
- rscadd: added the ability to make up to 10 individual highlights in tgchat, customise
their colours, and toggle if they highlight the whole message or not. Ported
from CM.
- rscadd: you can now use regex for text highlighting. The syntax is /[regex expression]/,
eg /dog[0-9]/. There are no quantifiers, but you can use capture groups. The
regex must read a minimum of two characters.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 325 KiB

After

Width:  |  Height:  |  Size: 331 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

@@ -7,7 +7,6 @@
total_positions = 1
spawn_positions = 1
supervisors = "Central Command and the Nanotrasen Consultant"
selection_color = "#c6ffe0"
minimal_player_age = 7
exp_requirements = 2400
exp_required_type = EXP_TYPE_CREW
@@ -1,5 +1,4 @@
#define MAX_BERSERK_CHARGE 200
#define BERSERK_HALF_CHARGE 100
#define BERSERK_MAX_CHARGE 100
#define PROJECTILE_HIT_MULTIPLIER 1.5
#define DAMAGE_TO_CHARGE_SCALE 1
#define CHARGE_DRAINED_PER_SECOND 3
@@ -77,8 +76,6 @@
armor_type = /datum/armor/berserker_gatsu
resistance_flags = INDESTRUCTIBLE
actions_types = list(/datum/action/item_action/berserk_mode)
///tracks whether or not the armor's charge is equal to or greater than 100% so it does not do the bubble alert twice
var/charged = FALSE
/datum/armor/berserker_gatsu
melee = 40
@@ -96,20 +93,18 @@
/obj/item/clothing/head/hooded/berserker/gatsu/examine()
. = ..()
. += span_warning("Berserk mode is usable at 100% charge but can gain up to 200% charge for extended duration.") //woag!!!
. += span_warning("Berserk mode is usable at 100% charge and requires the helmet to be closed in order to remain active.") //woag!!!
/obj/item/clothing/head/hooded/berserker/gatsu/process(delta_time)
if(berserk_active)
berserk_charge = clamp(berserk_charge - CHARGE_DRAINED_PER_SECOND * delta_time, 0, MAX_BERSERK_CHARGE)
berserk_charge = clamp(berserk_charge - CHARGE_DRAINED_PER_SECOND * delta_time, 0, BERSERK_MAX_CHARGE)
if(!berserk_charge)
if(ishuman(loc))
end_berserk(loc)
charged = FALSE
/obj/item/clothing/head/hooded/berserker/gatsu/dropped(mob/user)
. = ..()
end_berserk(user)
charged = FALSE
/obj/item/clothing/head/hooded/berserker/gatsu/hit_reaction(mob/living/carbon/human/owner, atom/movable/hitby, attack_text = "the attack", final_block_chance = 0, damage = 0, attack_type = MELEE_ATTACK)
if(berserk_active)
@@ -117,10 +112,9 @@
var/berserk_value = damage * DAMAGE_TO_CHARGE_SCALE
if(attack_type == PROJECTILE_ATTACK)
berserk_value *= PROJECTILE_HIT_MULTIPLIER
berserk_charge = clamp(round(berserk_charge + berserk_value), 0, MAX_BERSERK_CHARGE)
if(berserk_charge >= BERSERK_HALF_CHARGE & charged == FALSE)
berserk_charge = clamp(round(berserk_charge + berserk_value), 0, BERSERK_MAX_CHARGE)
if(berserk_charge >= BERSERK_MAX_CHARGE)
balloon_alert(owner, "berserk charged")
charged = TRUE
/obj/item/clothing/head/hooded/berserker/gatsu/IsReflect()
if(berserk_active)
@@ -234,8 +228,7 @@
new /obj/item/clothing/neck/warrior_cape(src)
new /obj/item/crusher_trophy/gladiator(src)
#undef MAX_BERSERK_CHARGE
#undef BERSERK_HALF_CHARGE
#undef BERSERK_MAX_CHARGE
#undef PROJECTILE_HIT_MULTIPLIER
#undef DAMAGE_TO_CHARGE_SCALE
#undef CHARGE_DRAINED_PER_SECOND
@@ -50,7 +50,7 @@
/// Are we doing the spin attack?
var/spinning = FALSE
/// Range of spin attack
var/spinning_range = 4
var/spinning_range = 6
/// Are we doing the charge attack
var/charging = FALSE
/// If we are charging, this is a counter for how many tiles we have ran past
@@ -60,12 +60,16 @@
/// We get stunned whenever we ram into a closed turf
var/stunned = FALSE
/// Move_to_delay but only while we are charging
var/move_to_delay_charge = 1.5
/// Chance to block damage entirely on phase 1
var/phase_1_block_chance = 50
var/move_to_delay_charge = 0.6
/// Chance to block damage entirely on phases 1 and 4
var/block_chance = 50
/// This mob will not attack mobs randomly if not in anger, the time doubles as a check for anger
var/anger_timer_id = null
/mob/living/simple_animal/hostile/megafauna/gladiator/Initialize(mapload)
. = ..()
ADD_TRAIT(src, TRAIT_NO_FLOATING_ANIM, INNATE_TRAIT)
/mob/living/simple_animal/hostile/megafauna/gladiator/Destroy()
get_calm()
return ..()
@@ -103,14 +107,17 @@
. = ..()
. += span_boldwarning("They are currently in Phase [phase].")
/// Gets him mad at you if you're a species he's not racist towards, and provides the 25% chance to block attacks in the first phase
/// Gets him mad at you if you're a species he's not racist towards, and provides the 50% to block attacks in the first and fourth phases
/mob/living/simple_animal/hostile/megafauna/gladiator/adjustHealth(amount, updating_health, forced)
get_angry()
if(spinning)
visible_message(span_danger("[src] brushes off all incoming attacks with his spinning blade!"))
balloon_alert_to_viewers("damage blocked!")
return FALSE
else if(prob(phase_1_block_chance) && (phase == 1) && !stunned)
visible_message(span_danger("[src] blocks all incoming damage with his arm!"))
else if(prob(block_chance) && (phase == 1) && !stunned)
balloon_alert_to_viewers("damage blocked!")
return FALSE
else if(prob(block_chance) && (phase == 4) && !stunned)
balloon_alert_to_viewers("damage blocked!")
return FALSE
. = ..()
update_phase()
@@ -199,6 +206,7 @@
if(anger_timer_id)
deltimer(anger_timer_id)
anger_timer_id = addtimer(CALLBACK(src, PROC_REF(get_calm)), MARKED_ONE_ANGER_DURATION, TIMER_STOPPABLE)
balloon_alert_to_viewers("angered!")
/// Makes the Marked One a sleepy boy that don't wanna hurt nobody. He starts like this and progresses to his hostile state after seeing an ash walker or being punched in the noggin.
/mob/living/simple_animal/hostile/megafauna/gladiator/proc/get_calm()
@@ -268,7 +276,7 @@
if(phase == MARKED_ONE_FIRST_PHASE)
phase = MARKED_ONE_SECOND_PHASE
INVOKE_ASYNC(src, PROC_REF(charge), target, 21)
playsound(src, 'sound/effects/clockcult_gateway_disrupted.ogg', 200, 1, 2)
ranged_cooldown += 8 SECONDS //this needs to be here lest another ranged attack override the charge while it's prepping
icon_state = "marked2"
rapid_melee = 2
move_to_delay = 2
@@ -278,21 +286,21 @@
if(phase == MARKED_ONE_SECOND_PHASE)
phase = MARKED_ONE_THIRD_PHASE
INVOKE_ASYNC(src, PROC_REF(charge), target, 21)
playsound(src, 'sound/effects/clockcult_gateway_charging.ogg', 200, 1, 2)
ranged_cooldown += 8 SECONDS
rapid_melee = 4
melee_damage_upper = 25
melee_damage_lower = 25
move_to_delay = 1.7
move_to_delay = 1.5
if(0 to SHOWDOWN_PERCENT)
if (phase == MARKED_ONE_THIRD_PHASE)
phase = MARKED_ONE_FINAL_PHASE
INVOKE_ASYNC(src, PROC_REF(charge), target, 21)
playsound(src, 'sound/effects/clockcult_gateway_active.ogg', 200, 1, 2)
ranged_cooldown += 8 SECONDS
icon_state = "marked3"
rapid_melee = 1
melee_damage_upper = 50
melee_damage_lower = 50
move_to_delay = 2.5
move_to_delay = 1.2
if(charging)
move_to_delay = move_to_delay_charge
@@ -301,7 +309,18 @@
var/turf/our_turf = get_turf(src)
if(!istype(our_turf))
return
visible_message(span_userdanger("[src] lifts his ancient blade, and prepares to spin!"))
var/static/list/spin_messages = list(
"Duck!",
"I'll break your legs!",
"Plain, dead, simple!",
"SWING AND A MISS!",
"Only one of us makes it outta here!",
"JUMP-ROPE!!",
"Slice and dice, right?!",
"Come on, HIT ME!",
"CLANG!!",
)
say(message = pick(spin_messages))
spinning = TRUE
animate(src, color = "#ff6666", 10)
SLEEP_CHECK_DEATH(5, src)
@@ -322,7 +341,7 @@
QDEL_IN(smonk, 0.5 SECONDS)
for(var/mob/living/slapped in targeted)
if(!faction_check(faction, slapped.faction) && !(slapped in hit_things))
playsound(src, 'sound/weapons/slash.ogg', 75, 0)
playsound(src, 'modular_skyrat/modules/gladiator/Clang_cut.ogg', 75, 0)
if(slapped.apply_damage(40, BRUTE, BODY_ZONE_CHEST, slapped.run_armor_check(BODY_ZONE_CHEST), wound_bonus = CANT_WOUND))
visible_message(span_danger("[src] slashes through [slapped] with his spinning blade!"))
else
@@ -333,13 +352,22 @@
break
sleep(0.75) //addtimer(CALLBACK(src, PROC_REF(convince_zonespace_to_let_me_use_sleeps)), 2 WEEKS)
animate(src, color = initial(color), 3)
sleep(3)
sleep(1)
spinning = FALSE
/// The Marked One's charge has an instant travel time, but takes a moment to power-up, allowing you to get behind cover to stun him if he hits a wall. Only ever called when a phase change occurs, as it hardstuns if it lands
/mob/living/simple_animal/hostile/megafauna/gladiator/proc/charge(atom/target, range = 1)
face_atom(target)
visible_message(span_userdanger("[src] lifts his arm, and prepares to charge!"))
var/static/list/charge_messages = list(
"Heads up!",
"Coming through!",
"This ends only one way!",
"Hold still!",
"GET OVER HERE!",
"Looking for this?!",
"COME ON!!",
)
say(message = pick(charge_messages))
animate(src, color = "#ff6666", 3)
SLEEP_CHECK_DEATH(4, src)
face_atom(target)
@@ -353,6 +381,7 @@
charging = FALSE
minimum_distance = initial(minimum_distance)
chargetiles = 0
playsound(src, 'modular_skyrat/modules/gladiator/Clang_cut.ogg', 75, 0)
animate(src, color = initial(color), 5)
update_phase()
sleep(CEILING(MARKED_ONE_STUN_DURATION * modifier, 1))
@@ -396,30 +425,25 @@
for(var/turf/stomp_turf in all_turfs)
if(get_dist(origin, stomp_turf) > sound_range)
continue
new /obj/effect/temp_visual/small_smoke/halfsecond(stomp_turf)
for(var/mob/living/target in stomp_turf)
if(target == src || target.throwing)
continue
to_chat(target, span_userdanger("[src]'s ground slam shockwave sends you flying!"))
var/turf/thrownat = get_ranged_target_turf_direct(src, target, throw_range, rand(-10, 10))
target.throw_at(thrownat, 8, 2, null, TRUE, force = MOVE_FORCE_OVERPOWERING, gentle = TRUE)
target.apply_damage(20, BRUTE, wound_bonus=CANT_WOUND)
shake_camera(target, 2, 1)
all_turfs -= stomp_turf
sleep(delay)
/// Large radius but slow-to-move radiating ground slam
/mob/living/simple_animal/hostile/megafauna/gladiator/proc/swordslam()
ground_pound(5, 1 SECONDS, 8)
ground_pound(5, 0.4 SECONDS, 8)
/// Sort range slam with faster shockwave travel
/mob/living/simple_animal/hostile/megafauna/gladiator/proc/stomp()
ground_pound(2, 0.5 SECONDS, 3)
ground_pound(2, 0.2 SECONDS, 3)
/// Used to determine what attacks the Marked One actually uses. This works by making him a ranged mob without a projectile. Shitcode? Maybe! But it woooorks.
/mob/living/simple_animal/hostile/megafauna/gladiator/OpenFire()
@@ -432,7 +456,6 @@
if(MARKED_ONE_FIRST_PHASE)
if(prob(10) && (get_dist(src, target) <= spinning_range))
INVOKE_ASYNC(src, PROC_REF(spinattack))
INVOKE_ASYNC(src, PROC_REF(stomp))
ranged_cooldown += 5.5 SECONDS
else
if(prob(50))
@@ -446,7 +469,6 @@
if(prob(80))
if(prob(50) && (get_dist(src, target) <= spinning_range))
INVOKE_ASYNC(src, PROC_REF(spinattack))
INVOKE_ASYNC(src, PROC_REF(stomp))
ranged_cooldown += 5 SECONDS
else
INVOKE_ASYNC(src, PROC_REF(swordslam))
@@ -463,8 +485,7 @@
if(prob(50))
if(prob(30) && (get_dist(src, target) <= spinning_range))
INVOKE_ASYNC(src, PROC_REF(spinattack))
INVOKE_ASYNC(src, PROC_REF(stomp))
ranged_cooldown += 4.5 SECONDS
ranged_cooldown += 3 SECONDS
else
INVOKE_ASYNC(src, PROC_REF(swordslam))
ranged_cooldown += 2 SECONDS
@@ -485,6 +506,7 @@
ranged_cooldown += 2 SECONDS
else
INVOKE_ASYNC(src, PROC_REF(swordslam))
INVOKE_ASYNC(src, PROC_REF(stomp))
ranged_cooldown += 2 SECONDS
else
INVOKE_ASYNC(src, PROC_REF(bone_knife_throw), target)
@@ -159,7 +159,6 @@
total_positions = 2
spawn_positions = 2
supervisors = SUPERVISOR_RD
selection_color = "#ffeeee"
exp_granted_type = EXP_TYPE_CREW
config_tag = "SCIENCE_GUARD"
@@ -261,7 +260,6 @@
total_positions = 2
spawn_positions = 2
supervisors = SUPERVISOR_CMO
selection_color = "#ffeeee"
exp_granted_type = EXP_TYPE_CREW
config_tag = "ORDERLY"
@@ -359,7 +357,6 @@
total_positions = 2
spawn_positions = 2
supervisors = SUPERVISOR_CE
selection_color = "#ffeeee"
exp_granted_type = EXP_TYPE_CREW
config_tag = "ENGINEERING_GUARD"
@@ -462,7 +459,6 @@
total_positions = 2
spawn_positions = 2
supervisors = SUPERVISOR_QM
selection_color = "#ffeeee"
exp_granted_type = EXP_TYPE_CREW
config_tag = "CUSTOMS_AGENT"
@@ -559,7 +555,6 @@
total_positions = 2
spawn_positions = 2
supervisors = SUPERVISOR_HOP
selection_color = "#ffeeee"
exp_granted_type = EXP_TYPE_CREW
config_tag = "BOUNCER"
@@ -6,7 +6,6 @@
total_positions = 1
spawn_positions = 1
supervisors = "Central Command"
selection_color = "#c6ffe0"
minimal_player_age = 14
exp_requirements = 600
exp_required_type = EXP_TYPE_CREW
@@ -6,7 +6,6 @@
total_positions = 2
spawn_positions = 2
supervisors = SUPERVISOR_HOP
selection_color = "#bbe291"
exp_granted_type = EXP_TYPE_CREW
config_tag = "BARBER"
@@ -7,7 +7,6 @@
total_positions = 1
spawn_positions = 1
supervisors = SUPERVISOR_HOS
selection_color = "#ffeeee"
minimal_player_age = 7
exp_requirements = 150
exp_required_type = EXP_TYPE_CREW
@@ -7,7 +7,6 @@
total_positions = 1
spawn_positions = 1
supervisors = SUPERVISOR_HOS
selection_color = "#ffeeee"
minimal_player_age = 7
exp_requirements = 120
exp_required_type = EXP_TYPE_CREW
+12 -6
View File
@@ -6,7 +6,7 @@
import DOMPurify from 'dompurify';
import { storage } from 'common/storage';
import { loadSettings, updateSettings } from '../settings/actions';
import { loadSettings, updateSettings, addHighlightSetting, removeHighlightSetting, updateHighlightSetting } from '../settings/actions';
import { selectSettings } from '../settings/selectors';
import { addChatPage, changeChatPage, changeScrollTracking, loadChat, rebuildChat, removeChatPage, saveChatToDisk, toggleAcceptedType, updateMessageCount } from './actions';
import { MAX_PERSISTED_MESSAGES, MESSAGE_SAVE_INTERVAL } from './constants';
@@ -113,15 +113,21 @@ export const chatMiddleware = (store) => {
chatRenderer.rebuildChat();
return next(action);
}
if (type === updateSettings.type || type === loadSettings.type) {
if (
type === updateSettings.type ||
type === loadSettings.type ||
type === addHighlightSetting.type ||
type === removeHighlightSetting.type ||
type === updateHighlightSetting.type
) {
next(action);
const settings = selectSettings(store.getState());
chatRenderer.setHighlight(
settings.highlightText,
settings.highlightColor,
settings.matchWord,
settings.matchCase
settings.highlightSettings,
settings.highlightSettingById
);
return;
}
if (type === 'roundrestart') {
+83 -29
View File
@@ -180,30 +180,79 @@ class ChatRenderer {
}
}
setHighlight(text, color, matchWord, matchCase) {
if (!text || !color) {
this.highlightRegex = null;
this.highlightColor = null;
setHighlight(highlightSettings, highlightSettingById) {
this.highlightParsers = null;
if (!highlightSettings) {
return;
}
const lines = String(text)
.split(',')
// eslint-disable-next-line no-useless-escape
.map((str) => str.trim().replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'))
// Must be longer than one character
.filter((str) => str && str.length > 1);
// Nothing to match, reset highlighting
if (lines.length === 0) {
this.highlightRegex = null;
this.highlightColor = null;
return;
}
const pattern = `${matchWord ? '\\b' : ''}(${lines.join('|')})${
matchWord ? '\\b' : ''
}`;
const flags = 'g' + (matchCase ? '' : 'i');
this.highlightRegex = new RegExp(pattern, flags);
this.highlightColor = color;
highlightSettings.map((id) => {
const setting = highlightSettingById[id];
const text = setting.highlightText;
const highlightColor = setting.highlightColor;
const highlightWholeMessage = setting.highlightWholeMessage;
const matchWord = setting.matchWord;
const matchCase = setting.matchCase;
const allowedRegex = /^[a-z0-9_\-$/^[\s\]\\]+$/gi;
const lines = String(text)
.split(',')
.map((str) => str.trim())
.filter(
(str) =>
// Must be longer than one character
str &&
str.length > 1 &&
// Must be alphanumeric (with some punctuation)
allowedRegex.test(str) &&
// Reset lastIndex so it does not mess up the next word
((allowedRegex.lastIndex = 0) || true)
);
let highlightWords;
let highlightRegex;
// Nothing to match, reset highlighting
if (lines.length === 0) {
return;
}
let regexExpressions = [];
// Organize each highlight entry into regex expressions and words
for (let line of lines) {
// Regex expression syntax is /[exp]/
if (line.charAt(0) === '/' && line.charAt(line.length - 1) === '/') {
const expr = line.substring(1, line.length - 1);
// Check if this is more than one character
if (/^(\[.*\]|\\.|.)$/.test(expr)) {
continue;
}
regexExpressions.push(expr);
} else {
// Lazy init
if (!highlightWords) {
highlightWords = [];
}
highlightWords.push(line);
}
}
const regexStr = regexExpressions.join('|');
const flags = 'g' + (matchCase ? '' : 'i');
// setting regex overrides matchword
if (regexStr) {
highlightRegex = new RegExp('(' + regexStr + ')', flags);
} else {
const pattern = `${matchWord ? '\\b' : ''}(${lines.join('|')})${
matchWord ? '\\b' : ''
}`;
highlightRegex = new RegExp(pattern, flags);
}
// Lazy init
if (!this.highlightParsers) {
this.highlightParsers = [];
}
this.highlightParsers.push({
highlightWords,
highlightRegex,
highlightColor,
highlightWholeMessage,
});
});
}
scrollToBottom() {
@@ -352,13 +401,18 @@ class ChatRenderer {
}
// Highlight text
if (!message.avoidHighlighting && this.highlightRegex) {
const highlighted = highlightNode(node, this.highlightRegex, (text) =>
createHighlightNode(text, this.highlightColor)
);
if (highlighted) {
node.className += ' ChatMessage--highlighted';
}
if (!message.avoidHighlighting && this.highlightParsers) {
this.highlightParsers.map((parser) => {
const highlighted = highlightNode(
node,
parser.highlightRegex,
parser.highlightWords,
(text) => createHighlightNode(text, parser.highlightColor)
);
if (highlighted && parser.highlightWholeMessage) {
node.className += ' ChatMessage--highlighted';
}
});
}
// Linkify text
const linkifyNodes = node.querySelectorAll('.linkify');
@@ -7,43 +7,120 @@
/**
* Replaces text matching a regular expression with a custom node.
*/
export const replaceInTextNode = (regex, createNode) => (node) => {
const regexParseNode = (params) => {
const { node, regex, createNode, captureAdjust } = params;
const text = node.textContent;
const textLength = text.length;
let nodes;
let new_node;
let match;
let lastIndex = 0;
let fragment;
let n = 0;
let count = 0;
// eslint-disable-next-line no-cond-assign
while ((match = regex.exec(text))) {
n += 1;
// Safety check to prevent permanent
// client crashing
if (++count > 9999) {
return {};
}
// Lazy init fragment
if (!fragment) {
fragment = document.createDocumentFragment();
}
const matchText = match[0];
// Lazy init nodes
if (!nodes) {
nodes = [];
}
const matchText = captureAdjust ? captureAdjust(match[0]) : match[0];
const matchLength = matchText.length;
const matchIndex = match.index;
// If matchText is set to be a substring nested within the original
// matched text make sure to properly offset the index
const matchIndex = match.index + match[0].indexOf(matchText);
// Insert previous unmatched chunk
if (lastIndex < matchIndex) {
fragment.appendChild(
document.createTextNode(text.substring(lastIndex, matchIndex))
);
new_node = document.createTextNode(text.substring(lastIndex, matchIndex));
nodes.push(new_node);
fragment.appendChild(new_node);
}
lastIndex = matchIndex + matchLength;
// Create a wrapper node
fragment.appendChild(createNode(matchText));
new_node = createNode(matchText);
nodes.push(new_node);
fragment.appendChild(new_node);
}
if (fragment) {
// Insert the remaining unmatched chunk
if (lastIndex < textLength) {
fragment.appendChild(
document.createTextNode(text.substring(lastIndex, textLength))
);
new_node = document.createTextNode(text.substring(lastIndex, textLength));
nodes.push(new_node);
fragment.appendChild(new_node);
}
// Commit the fragment
node.parentNode.replaceChild(fragment, node);
}
return {
nodes: nodes,
n: n,
};
};
/**
* Replace text of a node with custom nades if they match
* a regex expression or are in a word list
*/
export const replaceInTextNode = (regex, words, createNode) => (node) => {
let nodes;
let result;
let n = 0;
if (regex) {
result = regexParseNode({
node: node,
regex: regex,
createNode: createNode,
});
nodes = result.nodes;
n += result.n;
}
if (words) {
let i = 0;
let wordRegexStr = '(';
for (let word of words) {
// Capture if the word is at the beginning, end, middle,
// or by itself in a message
wordRegexStr += `^${word}\\W|\\W${word}\\W|\\W${word}$|^${word}$`;
// Make sure the last character for the expression is NOT '|'
if (++i !== words.length) {
wordRegexStr += '|';
}
}
wordRegexStr += ')';
const wordRegex = new RegExp(wordRegexStr, 'gi');
if (regex && nodes) {
for (let a_node of nodes) {
result = regexParseNode({
node: a_node,
regex: wordRegex,
createNode: createNode,
captureAdjust: (str) => str.replace(/^\W|\W$/g, ''),
});
n += result.n;
}
} else {
result = regexParseNode({
node: node,
regex: wordRegex,
createNode: createNode,
captureAdjust: (str) => str.replace(/^\W|\W$/g, ''),
});
n += result.n;
}
}
return n;
};
@@ -71,6 +148,7 @@ const createHighlightNode = (text) => {
export const highlightNode = (
node,
regex,
words,
createNode = createHighlightNode
) => {
if (!createNode) {
@@ -82,9 +160,9 @@ export const highlightNode = (
const node = childNodes[i];
// Is a text node
if (node.nodeType === 3) {
n += replaceInTextNode(regex, createNode)(node);
n += replaceInTextNode(regex, words, createNode)(node);
} else {
n += highlightNode(node, regex, createNode);
n += highlightNode(node, regex, words, createNode);
}
}
return n;
@@ -118,7 +196,7 @@ export const linkifyNode = (node) => {
return n;
};
const linkifyTextNode = replaceInTextNode(URL_REGEX, (text) => {
const linkifyTextNode = replaceInTextNode(URL_REGEX, null, (text) => {
const node = document.createElement('a');
node.href = text;
node.textContent = text;
@@ -11,9 +11,9 @@ import { Box, Button, ColorBox, Divider, Dropdown, Flex, Input, LabeledList, Num
import { ChatPageSettings } from '../chat';
import { rebuildChat, saveChatToDisk } from '../chat/actions';
import { THEMES } from '../themes';
import { changeSettingsTab, updateSettings } from './actions';
import { FONTS, SETTINGS_TABS } from './constants';
import { selectActiveTab, selectSettings } from './selectors';
import { changeSettingsTab, updateSettings, addHighlightSetting, removeHighlightSetting, updateHighlightSetting } from './actions';
import { SETTINGS_TABS, FONTS, MAX_HIGHLIGHT_SETTINGS } from './constants';
import { selectActiveTab, selectSettings, selectHighlightSettings, selectHighlightSettingById } from './selectors';
export const SettingsPanel = (props, context) => {
const activeTab = useSelector(context, selectActiveTab);
@@ -43,22 +43,17 @@ export const SettingsPanel = (props, context) => {
<Stack.Item grow={1} basis={0}>
{activeTab === 'general' && <SettingsGeneral />}
{activeTab === 'chatPage' && <ChatPageSettings />}
{activeTab === 'textHighlight' && <TextHighlightSettings />}
</Stack.Item>
</Stack>
);
};
export const SettingsGeneral = (props, context) => {
const {
theme,
fontFamily,
fontSize,
lineHeight,
highlightText,
highlightColor,
matchWord,
matchCase,
} = useSelector(context, selectSettings);
const { theme, fontFamily, fontSize, lineHeight } = useSelector(
context,
selectSettings
);
const dispatch = useDispatch(context);
const [freeFont, setFreeFont] = useLocalState(context, 'freeFont', false);
return (
@@ -157,62 +152,41 @@ export const SettingsGeneral = (props, context) => {
</LabeledList.Item>
</LabeledList>
<Divider />
<Box>
<Flex mb={1} color="label" align="baseline">
<Flex.Item grow={1}>Highlight text (comma separated):</Flex.Item>
<Flex.Item shrink={0}>
<ColorBox mr={1} color={highlightColor} />
<Input
width="5em"
monospace
placeholder="#ffffff"
value={highlightColor}
onInput={(e, value) =>
dispatch(
updateSettings({
highlightColor: value,
})
)
}
<Button icon="save" onClick={() => dispatch(saveChatToDisk())}>
Save chat log
</Button>
</Section>
);
};
const TextHighlightSettings = (props, context) => {
const highlightSettings = useSelector(context, selectHighlightSettings);
const dispatch = useDispatch(context);
return (
<Section fill scrollable height="200px">
<Section p={0}>
<Flex direction="column">
{highlightSettings.map((id, i) => (
<TextHighlightSetting
key={i}
id={id}
mb={i + 1 === highlightSettings.length ? 0 : '10px'}
/>
</Flex.Item>
))}
{highlightSettings.length < MAX_HIGHLIGHT_SETTINGS && (
<Flex.Item>
<Button
color="transparent"
icon="plus"
content="Add Highlight Setting"
onClick={() => {
dispatch(addHighlightSetting());
}}
/>
</Flex.Item>
)}
</Flex>
<TextArea
height="3em"
value={highlightText}
onChange={(e, value) =>
dispatch(
updateSettings({
highlightText: value,
})
)
}
/>
<Button.Checkbox
checked={matchWord}
tooltipPosition="bottom-start"
tooltip="Not compatible with punctuation."
onClick={() =>
dispatch(
updateSettings({
matchWord: !matchWord,
})
)
}>
Match word
</Button.Checkbox>
<Button.Checkbox
checked={matchCase}
onClick={() =>
dispatch(
updateSettings({
matchCase: !matchCase,
})
)
}>
Match case
</Button.Checkbox>
</Box>
</Section>
<Divider />
<Box>
<Button icon="check" onClick={() => dispatch(rebuildChat())}>
@@ -222,10 +196,116 @@ export const SettingsGeneral = (props, context) => {
Can freeze the chat for a while.
</Box>
</Box>
<Divider />
<Button icon="save" onClick={() => dispatch(saveChatToDisk())}>
Save chat log
</Button>
</Section>
);
};
const TextHighlightSetting = (props, context) => {
const { id, ...rest } = props;
const highlightSettingById = useSelector(context, selectHighlightSettingById);
const dispatch = useDispatch(context);
const {
highlightColor,
highlightText,
highlightWholeMessage,
matchWord,
matchCase,
} = highlightSettingById[id];
return (
<Flex.Item {...rest}>
<Flex mb={1} color="label" align="baseline">
<Flex.Item grow>
<Button
content="Delete"
color="transparent"
icon="times"
onClick={() =>
dispatch(
removeHighlightSetting({
id: id,
})
)
}
/>
</Flex.Item>
<Flex.Item>
<Button.Checkbox
checked={highlightWholeMessage}
content="Whole Message"
tooltip="If this option is selected, the entire message will be highlighted in yellow."
mr="5px"
onClick={() =>
dispatch(
updateHighlightSetting({
id: id,
highlightWholeMessage: !highlightWholeMessage,
})
)
}
/>
</Flex.Item>
<Flex.Item>
<Button.Checkbox
content="Exact"
checked={matchWord}
tooltipPosition="bottom-start"
tooltip="If this option is selected, only exact matches (no extra letters before or after) will trigger. Not compatible with punctuation. Overriden if regex is used."
onClick={() =>
dispatch(
updateHighlightSetting({
id: id,
matchWord: !matchWord,
})
)
}
/>
</Flex.Item>
<Flex.Item>
<Button.Checkbox
content="Case"
tooltip="If this option is selected, the highlight will be case-sensitive."
checked={matchCase}
onClick={() =>
dispatch(
updateHighlightSetting({
id: id,
matchCase: !matchCase,
})
)
}
/>
</Flex.Item>
<Flex.Item shrink={0}>
<ColorBox mr={1} color={highlightColor} />
<Input
width="5em"
monospace
placeholder="#ffffff"
value={highlightColor}
onInput={(e, value) =>
dispatch(
updateHighlightSetting({
id: id,
highlightColor: value,
})
)
}
/>
</Flex.Item>
</Flex>
<TextArea
height="3em"
value={highlightText}
placeholder="Put words to highlight here. Separate terms with commas, i.e. (term1, term2, term3)"
onChange={(e, value) =>
dispatch(
updateHighlightSetting({
id: id,
highlightText: value,
})
)
}
/>
</Flex.Item>
);
};
@@ -5,9 +5,22 @@
*/
import { createAction } from 'common/redux';
import { createHighlightSetting } from './model';
export const updateSettings = createAction('settings/update');
export const loadSettings = createAction('settings/load');
export const changeSettingsTab = createAction('settings/changeTab');
export const toggleSettings = createAction('settings/toggle');
export const openChatSettings = createAction('settings/openChatTab');
export const addHighlightSetting = createAction(
'settings/addHighlightSetting',
() => ({
payload: createHighlightSetting(),
})
);
export const removeHighlightSetting = createAction(
'settings/removeHighlightSetting'
);
export const updateHighlightSetting = createAction(
'settings/updateHighlightSetting'
);
@@ -9,6 +9,11 @@ export const SETTINGS_TABS = [
id: 'general',
name: 'General',
},
{
id: 'textHighlight',
name: 'Text Highlights',
},
{
id: 'chatPage',
name: 'Chat Tabs',
@@ -30,3 +35,5 @@ export const FONTS = [
'Courier New',
'Lucida Console',
];
export const MAX_HIGHLIGHT_SETTINGS = 10;
@@ -6,7 +6,7 @@
import { storage } from 'common/storage';
import { setClientTheme } from '../themes';
import { loadSettings, updateSettings } from './actions';
import { loadSettings, updateSettings, addHighlightSetting, removeHighlightSetting, updateHighlightSetting } from './actions';
import { selectSettings } from './selectors';
import { FONTS_DISABLED } from './constants';
@@ -32,7 +32,13 @@ export const settingsMiddleware = (store) => {
store.dispatch(loadSettings(settings));
});
}
if (type === updateSettings.type || type === loadSettings.type) {
if (
type === updateSettings.type ||
type === loadSettings.type ||
type === addHighlightSetting.type ||
type === removeHighlightSetting.type ||
type === updateHighlightSetting.type
) {
// Set client theme
const theme = payload?.theme;
if (theme) {
@@ -0,0 +1,20 @@
/**
* @file
*/
import { createUuid } from 'common/uuid';
export const createHighlightSetting = (obj) => ({
id: createUuid(),
highlightText: '',
highlightColor: '#ffdd44',
highlightWholeMessage: true,
matchWord: false,
matchCase: false,
...obj,
});
export const createDefaultHighlightSetting = (obj) =>
createHighlightSetting({
id: 'default',
...obj,
});
+104 -5
View File
@@ -4,8 +4,11 @@
* @license MIT
*/
import { changeSettingsTab, loadSettings, openChatSettings, toggleSettings, updateSettings } from './actions';
import { FONTS, SETTINGS_TABS } from './constants';
import { changeSettingsTab, loadSettings, openChatSettings, toggleSettings, updateSettings, addHighlightSetting, removeHighlightSetting, updateHighlightSetting } from './actions';
import { createDefaultHighlightSetting } from './model';
import { SETTINGS_TABS, FONTS, MAX_HIGHLIGHT_SETTINGS } from './constants';
const defaultHighlightSetting = createDefaultHighlightSetting();
const initialState = {
version: 1,
@@ -14,10 +17,14 @@ const initialState = {
lineHeight: 1.2,
theme: 'light',
adminMusicVolume: 0.5,
// Keep these two state vars for compatibility with other servers
highlightText: '',
highlightColor: '#ffdd44',
matchWord: false,
matchCase: false,
// END compatibility state vars
highlightSettings: [defaultHighlightSetting.id],
highlightSettingById: {
[defaultHighlightSetting.id]: defaultHighlightSetting,
},
view: {
visible: false,
activeTab: SETTINGS_TABS[0].id,
@@ -37,11 +44,34 @@ export const settingsReducer = (state = initialState, action) => {
if (!payload?.version) {
return state;
}
delete payload.view;
return {
const nextState = {
...state,
...payload,
};
// Lazy init the list for compatibility reasons
if (!nextState.highlightSettings) {
nextState.highlightSettings = [defaultHighlightSetting.id];
nextState.highlightSettingById[defaultHighlightSetting.id] =
defaultHighlightSetting;
}
// Compensating for mishandling of default highlight settings
else if (!nextState.highlightSettingById[defaultHighlightSetting.id]) {
nextState.highlightSettings = [
defaultHighlightSetting.id,
...nextState.highlightSettings,
];
nextState.highlightSettingById[defaultHighlightSetting.id] =
defaultHighlightSetting;
}
// Update the highlight settings for default highlight
// settings compatibility
const highlightSetting =
nextState.highlightSettingById[defaultHighlightSetting.id];
highlightSetting.highlightColor = nextState.highlightColor;
highlightSetting.highlightText = nextState.highlightText;
return nextState;
}
if (type === toggleSettings.type) {
return {
@@ -72,5 +102,74 @@ export const settingsReducer = (state = initialState, action) => {
},
};
}
if (type === addHighlightSetting.type) {
const highlightSetting = payload;
if (state.highlightSettings.length >= MAX_HIGHLIGHT_SETTINGS) {
return state;
}
return {
...state,
highlightSettings: [...state.highlightSettings, highlightSetting.id],
highlightSettingById: {
...state.highlightSettingById,
[highlightSetting.id]: highlightSetting,
},
};
}
if (type === removeHighlightSetting.type) {
const { id } = payload;
const nextState = {
...state,
highlightSettings: [...state.highlightSettings],
highlightSettingById: {
...state.highlightSettingById,
},
};
if (id === defaultHighlightSetting.id) {
nextState.highlightSettings[defaultHighlightSetting.id] =
defaultHighlightSetting;
} else {
delete nextState.highlightSettingById[id];
nextState.highlightSettings = nextState.highlightSettings.filter(
(sid) => sid !== id
);
if (!nextState.highlightSettings.length) {
nextState.highlightSettings.push(defaultHighlightSetting.id);
nextState.highlightSettingById[defaultHighlightSetting.id] =
defaultHighlightSetting;
}
}
return nextState;
}
if (type === updateHighlightSetting.type) {
const { id, ...settings } = payload;
const nextState = {
...state,
highlightSettings: [...state.highlightSettings],
highlightSettingById: {
...state.highlightSettingById,
},
};
// Transfer this data from the default highlight setting
// so they carry over to other servers
if (id === defaultHighlightSetting.id) {
if (settings.highlightText) {
nextState.highlightText = settings.highlightText;
}
if (settings.highlightColor) {
nextState.highlightColor = settings.highlightColor;
}
}
if (nextState.highlightSettingById[id]) {
nextState.highlightSettingById[id] = {
...nextState.highlightSettingById[id],
...settings,
};
}
return nextState;
}
return state;
};
@@ -6,3 +6,7 @@
export const selectSettings = (state) => state.settings;
export const selectActiveTab = (state) => state.settings.view.activeTab;
export const selectHighlightSettings = (state) =>
state.settings.highlightSettings;
export const selectHighlightSettingById = (state) =>
state.settings.highlightSettingById;