diff --git a/code/__DEFINES/say.dm b/code/__DEFINES/say.dm index dbc4021b72b7d..03aa4f85c07da 100644 --- a/code/__DEFINES/say.dm +++ b/code/__DEFINES/say.dm @@ -63,6 +63,7 @@ #define SPAN_MEGAPHONE "megaphone" #define SPAN_CLOWN "clowntext" #define SPAN_SINGING "singing" +#define SPAN_TAPE_RECORDER "tape_recorder" //bitflag #defines for return value of the radio() proc. #define ITALICS (1<<0) diff --git a/code/datums/looping_sounds/item_sounds.dm b/code/datums/looping_sounds/item_sounds.dm index 5b02eef9c4ce6..181e1d5a12b70 100644 --- a/code/datums/looping_sounds/item_sounds.dm +++ b/code/datums/looping_sounds/item_sounds.dm @@ -51,3 +51,8 @@ mid_sounds = list('sound/items/weeoo1.ogg' = 1) mid_length = 15 volume = 20 + +/datum/looping_sound/tape_recorder_hiss + mid_sounds = list('sound/items/taperecorder/taperecorder_hiss_mid.ogg') + start_sound = list('sound/items/taperecorder/taperecorder_hiss_start.ogg') + volume = 10 diff --git a/code/game/objects/items/devices/taperecorder.dm b/code/game/objects/items/devices/taperecorder.dm index ab5d8bad62876..98d6381ede1fb 100644 --- a/code/game/objects/items/devices/taperecorder.dm +++ b/code/game/objects/items/devices/taperecorder.dm @@ -11,53 +11,105 @@ slot_flags = ITEM_SLOT_BELT custom_materials = list(/datum/material/iron=60, /datum/material/glass=30) force = 2 - throwforce = 0 + throwforce = 2 + speech_span = SPAN_TAPE_RECORDER drop_sound = 'sound/items/handling/taperecorder_drop.ogg' pickup_sound = 'sound/items/handling/taperecorder_pickup.ogg' - var/recording = 0 - var/playing = 0 + var/recording = FALSE + var/playing = FALSE var/playsleepseconds = 0 var/obj/item/tape/mytape var/starting_tape_type = /obj/item/tape/random - var/open_panel = 0 - var/canprint = 1 + var/open_panel = FALSE + var/canprint = TRUE + var/list/icons_available = list() + var/radial_icon_file = 'icons/obj/radial_taperecorder.dmi' + ///Whether we've warned during this recording session that the tape is almost up. + var/time_warned = FALSE + ///Seconds under which to warn that the tape is almost up. + var/time_left_warning = 60 SECONDS + ///Sound loop that plays when recording or playing back. + var/datum/looping_sound/tape_recorder_hiss/soundloop /obj/item/taperecorder/Initialize(mapload) . = ..() if(starting_tape_type) mytape = new starting_tape_type(src) + soundloop = new(src, FALSE) update_icon() become_hearing_sensitive() +/obj/item/taperecorder/proc/readout() + if(mytape) + if(playing) + return "PLAYING" + else + var/time = mytape.used_capacity / 10 //deciseconds / 10 = seconds + var/mins = round(time / 60) + var/secs = time - mins * 60 + return "[mins]m [secs]s" + return "NO TAPE INSERTED" + +/obj/item/taperecorder/proc/update_available_icons() + icons_available = list() + + if(!playing && !recording) + icons_available += list("Record" = image(radial_icon_file,"record")) + icons_available += list("Play" = image(radial_icon_file,"play")) + if(canprint && mytape?.storedinfo.len) + icons_available += list("Print Transcript" = image(radial_icon_file,"print")) + if(playing || recording) + icons_available += list("Stop" = image(radial_icon_file,"stop")) + if(mytape) + icons_available += list("Eject" = image(radial_icon_file,"eject")) + +/obj/item/taperecorder/proc/update_sound() + if(!playing && !recording) + soundloop.stop() + else + soundloop.start() + /obj/item/taperecorder/Destroy() + QDEL_NULL(soundloop) QDEL_NULL(mytape) return ..() /obj/item/taperecorder/examine(mob/user) . = ..() - . += "The wire panel is [open_panel ? "opened" : "closed"]." - + if(in_range(src, user) || isobserver(user)) + . += "The wire panel is [open_panel ? "opened" : "closed"]. The display reads:" + . += "[readout()]" /obj/item/taperecorder/attackby(obj/item/I, mob/user, params) if(!mytape && istype(I, /obj/item/tape)) if(!user.transferItemToLoc(I,src)) return mytape = I - to_chat(user, "You insert [I] into [src].") - update_icon() + balloon_alert(user, "inserted [mytape]") + playsound(src, 'sound/items/taperecorder/taperecorder_close.ogg', 50, FALSE) + update_appearance() /obj/item/taperecorder/proc/eject(mob/user) - if(mytape) - to_chat(user, "You remove [mytape] from [src].") - stop() - user.put_in_hands(mytape) - mytape = null - update_icon() + if(!mytape) + balloon_alert(user, "no tape!") + return + if(playing) + balloon_alert(user, "stop the tape first!") + return + if(recording) + balloon_alert(user, "stop the recording first!") + return + playsound(src, 'sound/items/taperecorder/taperecorder_open.ogg', 50, FALSE) + balloon_alert(user, "ejected [mytape]") + stop() + user.put_in_hands(mytape) + mytape = null + update_icon() /obj/item/taperecorder/fire_act(exposed_temperature, exposed_volume) - mytape.ruin() //Fires destroy the tape + mytape.unspool() //Fires unspool the tape, which makes sense if you don't think about it ..() //ATTACK HAND IGNORING PARENT RETURN VALUE @@ -82,8 +134,10 @@ set category = "Object" if(!can_use(usr)) + balloon_alert(usr, "can't use!") return if(!mytape) + balloon_alert(usr, "no tape!") return eject(usr) @@ -107,37 +161,48 @@ . = ..() if(mytape && recording) mytape.timestamp += mytape.used_capacity - mytape.storedinfo += "\[[time2text(mytape.used_capacity * 10,"mm:ss")]\] [message]" + mytape.storedinfo += "\[[time2text(mytape.used_capacity,"mm:ss")]\] [message]" /obj/item/taperecorder/verb/record() set name = "Start Recording" set category = "Object" if(!can_use(usr)) + balloon_alert(usr, "can't use!") return - if(!mytape || mytape.ruined) + if(!mytape || mytape.unspooled) + balloon_alert(usr, "no spooled tape!") return if(recording) + balloon_alert(usr, "stop recording first!") return if(playing) + balloon_alert(usr, "already playing!") return + playsound(src, 'sound/items/taperecorder/taperecorder_play.ogg', 50, FALSE) + if(mytape.used_capacity < mytape.max_capacity) - to_chat(usr, "Recording started.") - recording = 1 + recording = TRUE + balloon_alert(usr, "started recording") + update_sound() update_icon() - mytape.timestamp += mytape.used_capacity - mytape.storedinfo += "\[[time2text(mytape.used_capacity * 10,"mm:ss")]\] Recording started." var/used = mytape.used_capacity //to stop runtimes when you eject the tape var/max = mytape.max_capacity while(recording && used < max) - mytape.used_capacity++ - used++ - sleep(10) - recording = 0 - update_icon() + mytape.used_capacity += 1 SECONDS + used += 1 SECONDS + if(max - used < time_left_warning && !time_warned) + time_warned = TRUE + balloon_alert(usr, "[(max - used) / 10] second\s left") + sleep(1 SECONDS) + if(used >= max) + balloon_alert(usr, "tape full!") + sleep(1 SECONDS) //prevent balloon alerts layering over the top of each other + stop() else - to_chat(usr, "The tape is full.") + balloon_alert(usr, "tape full!") + playsound(src, 'sound/items/taperecorder/taperecorder_stop.ogg', 50, FALSE) /obj/item/taperecorder/verb/stop() @@ -145,70 +210,93 @@ set category = "Object" if(!can_use(usr)) + balloon_alert(usr, "can't use!") return if(recording) - recording = 0 - mytape.timestamp += mytape.used_capacity - mytape.storedinfo += "\[[time2text(mytape.used_capacity * 10,"mm:ss")]\] Recording stopped." - to_chat(usr, "Recording stopped.") - return + playsound(src, 'sound/items/taperecorder/taperecorder_stop.ogg', 50, FALSE) + balloon_alert(usr, "stopped recording") + recording = FALSE else if(playing) - playing = 0 - var/turf/T = get_turf(src) - T.visible_message("Tape Recorder: Playback stopped.") + playsound(src, 'sound/items/taperecorder/taperecorder_stop.ogg', 50, FALSE) + balloon_alert(usr, "stopped playing") + playing = FALSE + time_warned = FALSE update_icon() - + update_sound() /obj/item/taperecorder/verb/play() set name = "Play Tape" set category = "Object" if(!can_use(usr)) + balloon_alert(usr, "can't use!") return - if(!mytape || mytape.ruined) + if(!mytape || mytape.unspooled) + balloon_alert(usr, "no spooled tape!") return if(recording) + balloon_alert(usr, "stop recording first!") return if(playing) + balloon_alert(usr, "already playing!") + return + if(mytape.storedinfo?.len <= 0) + balloon_alert(usr, "[mytape] is empty!") return - playing = 1 + playing = TRUE update_icon() - to_chat(usr, "Playing started.") + update_sound() + balloon_alert(usr, "started playing") + playsound(src, 'sound/items/taperecorder/taperecorder_play.ogg', 50, FALSE) var/used = mytape.used_capacity //to stop runtimes when you eject the tape var/max = mytape.max_capacity - for(var/i = 1, used < max, sleep(10 * playsleepseconds)) + for(var/i = 1, used <= max, sleep(playsleepseconds)) if(!mytape) break - if(playing == 0) + if(playing == FALSE) break if(mytape.storedinfo.len < i) + balloon_alert(usr, "recording ended") + sleep(1 SECONDS) break - say(mytape.storedinfo[i]) + say("[mytape.storedinfo[i]]") if(mytape.storedinfo.len < i + 1) playsleepseconds = 1 - sleep(10) - say("End of recording.") + sleep(1 SECONDS) else playsleepseconds = mytape.timestamp[i + 1] - mytape.timestamp[i] - if(playsleepseconds > 14) - sleep(10) - say("Skipping [playsleepseconds] seconds of silence") - playsleepseconds = 1 + if(playsleepseconds > 14 SECONDS) + sleep(1 SECONDS) + say("Skipping [playsleepseconds/10] seconds of silence.") + playsleepseconds = 1 SECONDS i++ - playing = 0 - update_icon() + stop() /obj/item/taperecorder/attack_self(mob/user) - if(!mytape || mytape.ruined) + if(!mytape) + balloon_alert(user, "it's empty!") return - if(recording) - stop() - else - record() + + update_available_icons() + if(icons_available) + var/selection = show_radial_menu(user, src, icons_available, radius = 38, require_near = TRUE, tooltips = TRUE) + if(!selection) + return + switch(selection) + if("Stop") + stop() + if("Record") + record() + if("Play") + play() + if("Print Transcript") + print_transcript() + if("Eject") + eject(user) /obj/item/taperecorder/verb/print_transcript() @@ -217,15 +305,22 @@ var/list/transcribed_info = mytape.storedinfo if(!length(transcribed_info)) - return - if(!mytape) + balloon_alert(usr, "tape is empty!") return if(!canprint) - to_chat(usr, "The recorder can't print that fast!") - return - if(recording || playing) + balloon_alert(usr, "can't print that fast!") return if(!can_use(usr)) + balloon_alert(usr, "can't use!") + return + if(!mytape || mytape.unspooled) + balloon_alert(usr, "no spooled tape!") + return + if(recording) + balloon_alert(usr, "stop recording first!") + return + if(playing) + balloon_alert(usr, "already playing!") return var/transcribed_text = "Transcript:

" @@ -240,7 +335,7 @@ // Very unexpected. Better abort non-gracefully. if(excerpt_length > MAX_PAPER_LENGTH) - say("Error: Data corruption detected. Cannot print.") + balloon_alert(usr, "data corrupted, can't print!") CRASH("Transcript entry has more than [MAX_PAPER_LENGTH] chars: [excerpt_length] chars") // If we're going to overflow the paper's length, print the current transcribed text out first and reset to prevent us @@ -259,7 +354,10 @@ transcript_paper.add_raw_text(transcribed_text) transcript_paper.name = "[paper_name] page [page_count]" transcript_paper.update_appearance() - to_chat(usr, "Transcript printed, [page_count] pages.") + balloon_alert(usr, "transcript printed\n[page_count] page\s") + playsound(src, 'sound/items/taperecorder/taperecorder_print.ogg', 50, FALSE) + + // Can't put the entire stack into their hands if there's multple pages, but hey we can at least put one page in. usr.put_in_hands(transcript_paper) canprint = FALSE addtimer(VARSET_CALLBACK(src, canprint, TRUE), 30 SECONDS) @@ -272,7 +370,7 @@ /obj/item/tape name = "tape" - desc = "A magnetic tape that can hold up to ten minutes of content." + desc = "A magnetic tape that can hold up to ten minutes of content on either side." icon_state = "tape_white" icon = 'icons/obj/device.dmi' item_state = "analyzer" @@ -282,46 +380,123 @@ custom_materials = list(/datum/material/iron=20, /datum/material/glass=5) force = 1 throwforce = 0 - var/max_capacity = 600 - var/used_capacity = 0 + obj_flags = UNIQUE_RENAME //my mixtape + drop_sound = 'sound/items/handling/tape_drop.ogg' + pickup_sound = 'sound/items/handling/tape_pickup.ogg' + ///Because we can't expect God to do all the work. + var/initial_icon_state + var/max_capacity = 10 MINUTES + var/used_capacity = 0 SECONDS + ///Numbered list of chat messages the recorder has heard with spans and prepended timestamps. Used for playback and transcription. var/list/storedinfo = list() + ///Numbered list of seconds the messages in the previous list appear at on the tape. Used by playback to get the timing right. var/list/timestamp = list() - var/ruined = 0 + var/used_capacity_otherside = 0 SECONDS //Separate my side + var/list/storedinfo_otherside = list() + var/list/timestamp_otherside = list() + var/unspooled = FALSE + var/list/icons_available = list() + var/radial_icon_file = 'icons/obj/radial_tape.dmi' /obj/item/tape/fire_act(exposed_temperature, exposed_volume) - ruin() + unspool() ..() -/obj/item/tape/attack_self(mob/user) - if(!ruined) - to_chat(user, "You pull out all the tape!") - ruin() +/obj/item/tape/Initialize(mapload) + . = ..() + initial_icon_state = icon_state //random tapes will set this after choosing their icon + var/mycolor = random_short_color() + name += " ([mycolor])" //multiple tapes can get confusing fast + if(icon_state == "tape_greyscale") + add_atom_colour("#[mycolor]", FIXED_COLOUR_PRIORITY) + if(prob(50)) + tapeflip() + +/obj/item/tape/proc/update_available_icons() + icons_available = list() + if(!unspooled) + icons_available += list("Unwind tape" = image(radial_icon_file,"tape_unwind")) + icons_available += list("Flip tape" = image(radial_icon_file,"tape_flip")) +/obj/item/tape/attack_self(mob/user) + update_available_icons() + if(icons_available) + var/selection = show_radial_menu(user, src, icons_available, radius = 38, require_near = TRUE, tooltips = TRUE) + if(!selection) + return + switch(selection) + if("Flip tape") + if(loc != user) + return + tapeflip() + balloon_alert(user, "flipped tape") + playsound(src, 'sound/items/taperecorder/tape_flip.ogg', 70, FALSE) + if("Unwind tape") + if(loc != user) + return + unspool() + balloon_alert(user, "unspooled tape") + +/obj/item/tape/throw_impact(atom/hit_atom, datum/thrownthing/throwingdatum) + if(prob(50)) + tapeflip() + . = ..() -/obj/item/tape/proc/ruin() - //Lets not add infinite amounts of overlays when our fireact is called - //repeatedly - if(!ruined) +/obj/item/tape/proc/unspool() + //Let's not add infinite amounts of overlays when our fire_act is called repeatedly + if(!unspooled) add_overlay("ribbonoverlay") - ruined = 1 - + unspooled = TRUE -/obj/item/tape/proc/fix() +/obj/item/tape/proc/respool() cut_overlay("ribbonoverlay") - ruined = 0 - - -/obj/item/tape/attackby(obj/item/I, mob/user, params) - if(ruined && I.tool_behaviour == TOOL_SCREWDRIVER || istype(I, /obj/item/pen)) - to_chat(user, "You start winding the tape back in...") - if(I.use_tool(src, user, 120)) - to_chat(user, "You wound the tape back in.") - fix() + unspooled = FALSE + +/obj/item/tape/proc/tapeflip() + //first we save a copy of our current side + var/list/storedinfo_currentside = storedinfo.Copy() + var/list/timestamp_currentside = timestamp.Copy() + var/used_capacity_currentside = used_capacity + //then we overwite our current side with our other side + storedinfo = storedinfo_otherside.Copy() + timestamp = timestamp_otherside.Copy() + used_capacity = used_capacity_otherside + //then we overwrite our other side with the saved side + storedinfo_otherside = storedinfo_currentside.Copy() + timestamp_otherside = timestamp_currentside.Copy() + used_capacity_otherside = used_capacity_currentside + + if(icon_state == initial_icon_state) + icon_state = "[initial_icon_state]_reverse" + else if(icon_state == "[initial_icon_state]_reverse") //so flipping doesn't overwrite an unexpected icon_state (e.g. an admin's) + icon_state = initial_icon_state + +/obj/item/tape/attackby(obj/item/tool, mob/user, params) + if(unspooled && (istype(tool, /obj/item/pen))) + balloon_alert(user, "respooling tape...") + if(!tool.use_tool(src, user, 12 SECONDS)) + balloon_alert(user, "respooling failed!") + return FALSE + balloon_alert(user, "tape respooled") + respool() + +/obj/item/tape/screwdriver_act(mob/living/user, obj/item/tool) + if(!unspooled) + return FALSE + balloon_alert(user, "respooling tape...") + if(!tool.use_tool(src, user, 12 SECONDS)) + balloon_alert(user, "respooling failed!") + return FALSE + balloon_alert(user, "tape respooled") + respool() //Random colour tapes /obj/item/tape/random icon_state = "random_tape" /obj/item/tape/random/Initialize(mapload) + icon_state = "tape_[pick("white", "blue", "red", "yellow", "purple", "tape_greyscale")]" . = ..() - icon_state = "tape_[pick("white", "blue", "red", "yellow", "purple")]" + +/obj/item/tape/dyed + icon_state = "tape_greyscale" diff --git a/icons/obj/device.dmi b/icons/obj/device.dmi index e6379f882ff31..d7467f636522b 100644 Binary files a/icons/obj/device.dmi and b/icons/obj/device.dmi differ diff --git a/icons/obj/radial_tape.dmi b/icons/obj/radial_tape.dmi new file mode 100644 index 0000000000000..ae5be96dd8a94 Binary files /dev/null and b/icons/obj/radial_tape.dmi differ diff --git a/icons/obj/radial_taperecorder.dmi b/icons/obj/radial_taperecorder.dmi new file mode 100644 index 0000000000000..29d97b0aba05f Binary files /dev/null and b/icons/obj/radial_taperecorder.dmi differ diff --git a/interface/stylesheet.dm b/interface/stylesheet.dm index dd4a8786dc2f2..8619c149d008c 100644 --- a/interface/stylesheet.dm +++ b/interface/stylesheet.dm @@ -232,4 +232,5 @@ h1.alert, h2.alert {color: #000000;} .monkeyhive {color: #774704;} .monkeylead {color: #774704; font-size: 2;} +.tape_recorder {color: #ff0000; font-family: 'Courier New', cursive, sans-serif;} "} diff --git a/sound/items/taperecorder/tape_flip.ogg b/sound/items/taperecorder/tape_flip.ogg new file mode 100644 index 0000000000000..fae3e07373c5b Binary files /dev/null and b/sound/items/taperecorder/tape_flip.ogg differ diff --git a/sound/items/taperecorder/taperecorder_close.ogg b/sound/items/taperecorder/taperecorder_close.ogg new file mode 100644 index 0000000000000..ab9f521c5f9fc Binary files /dev/null and b/sound/items/taperecorder/taperecorder_close.ogg differ diff --git a/sound/items/taperecorder/taperecorder_hiss_mid.ogg b/sound/items/taperecorder/taperecorder_hiss_mid.ogg new file mode 100644 index 0000000000000..50ef4f2171b19 Binary files /dev/null and b/sound/items/taperecorder/taperecorder_hiss_mid.ogg differ diff --git a/sound/items/taperecorder/taperecorder_hiss_start.ogg b/sound/items/taperecorder/taperecorder_hiss_start.ogg new file mode 100644 index 0000000000000..fa57041a72281 Binary files /dev/null and b/sound/items/taperecorder/taperecorder_hiss_start.ogg differ diff --git a/sound/items/taperecorder/taperecorder_open.ogg b/sound/items/taperecorder/taperecorder_open.ogg new file mode 100644 index 0000000000000..7b7110fa58ba5 Binary files /dev/null and b/sound/items/taperecorder/taperecorder_open.ogg differ diff --git a/sound/items/taperecorder/taperecorder_play.ogg b/sound/items/taperecorder/taperecorder_play.ogg new file mode 100644 index 0000000000000..1bf4d7a3bd63a Binary files /dev/null and b/sound/items/taperecorder/taperecorder_play.ogg differ diff --git a/sound/items/taperecorder/taperecorder_print.ogg b/sound/items/taperecorder/taperecorder_print.ogg new file mode 100644 index 0000000000000..7912d08dc9827 Binary files /dev/null and b/sound/items/taperecorder/taperecorder_print.ogg differ diff --git a/sound/items/taperecorder/taperecorder_stop.ogg b/sound/items/taperecorder/taperecorder_stop.ogg new file mode 100644 index 0000000000000..a3b0f659928f3 Binary files /dev/null and b/sound/items/taperecorder/taperecorder_stop.ogg differ diff --git a/tgui/packages/tgui-panel/styles/goon/chat-dark.scss b/tgui/packages/tgui-panel/styles/goon/chat-dark.scss index 7f0900ee516e1..e30a3478814e5 100644 --- a/tgui/packages/tgui-panel/styles/goon/chat-dark.scss +++ b/tgui/packages/tgui-panel/styles/goon/chat-dark.scss @@ -969,6 +969,11 @@ em { font-family: 'Courier New', cursive, sans-serif; } +.tape_recorder { + color: #ff0000; + font-family: 'Courier New', cursive, sans-serif; +} + .command_headset { font-weight: bold; font-size: 24px; diff --git a/tgui/packages/tgui-panel/styles/goon/chat-light.scss b/tgui/packages/tgui-panel/styles/goon/chat-light.scss index 44e3ea8702b07..0f9ef2f2be605 100644 --- a/tgui/packages/tgui-panel/styles/goon/chat-light.scss +++ b/tgui/packages/tgui-panel/styles/goon/chat-light.scss @@ -974,6 +974,11 @@ h2.alert { font-family: 'Courier New', cursive, sans-serif; } +.tape_recorder { + color: #800000; + font-family: 'Courier New', cursive, sans-serif; +} + .command_headset { font-weight: bold; font-size: 24px;