Merge branch 'Skyrat-SS13:master' into master
@@ -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
|
||||
|
||||
@@ -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 ]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 325 KiB After Width: | Height: | Size: 331 KiB |
|
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
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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;
|
||||
|
||||