mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-26 17:41:05 +00:00
[MIRROR] Paintings improvements. [MDB IGNORE] (#9977)
* Paintings improvements. (#63170) About The Pull Request Paintings can now do stroke painting. Added painting management panel for admins. Paintings now display author's character name, year of painting, medium and patron when hung on wall. You can become new patron by paying more than the previous one. Added painter's palettes to library vendor. (Sprites by @ Mickyan ) Backend changes: Images are now stored in /data/paintings/images/*.png instead of /data/paintings/[category]/*.png Old categories are now just tags Screens & Video Changelog cl add: You can now become patron of your favorite painting by buying sponsorship from Nanotrasen Trust Foundation. add: Painter's palettes are now available at library vendor. qol: Can use strokes in paintings now /cl * Paintings improvements. Co-authored-by: AnturK <AnturK@users.noreply.github.com>
This commit is contained in:
@@ -1,3 +1,92 @@
|
||||
#define PAINTINGS_DATA_FORMAT_VERSION 1
|
||||
/*
|
||||
{
|
||||
"version":1
|
||||
"paintings":[
|
||||
{
|
||||
"md5":"2e117d9d372fb6823bd81d3542a419d6", //unique identifier
|
||||
"creator_ckey" : "example",
|
||||
"creator_name" : "example",
|
||||
"creation_date" : "YYYY-MM-DD hh:mm:ss",
|
||||
"creation_round_id": 222,
|
||||
"title": "example title",
|
||||
"tags": ["library","library_private"],
|
||||
"patron_ckey" : "example",
|
||||
"patron_name" : "example",
|
||||
"credit_value" : 999,
|
||||
"width" : 24,
|
||||
"height" : 24,
|
||||
"medium" : "Oil on canvas"
|
||||
},
|
||||
]
|
||||
}
|
||||
*/
|
||||
|
||||
/datum/painting
|
||||
/// md5 of the png file, also the filename.
|
||||
var/md5
|
||||
/// Title
|
||||
var/title
|
||||
/// Author's ckey
|
||||
var/creator_ckey
|
||||
/// Author's name
|
||||
var/creator_name
|
||||
/// Timestamp when painting was made (finalized ?)
|
||||
var/creation_date
|
||||
/// Round if when the painting was made
|
||||
var/creation_round_id
|
||||
/// List of this painting string tags if any
|
||||
var/list/tags
|
||||
/// Patron ckey
|
||||
var/patron_ckey
|
||||
/// Patron name
|
||||
var/patron_name
|
||||
/// Amount paid by last patron for this painting
|
||||
var/credit_value = 0
|
||||
/// painting width
|
||||
var/width
|
||||
/// painting height
|
||||
var/height
|
||||
/// short painting medium description
|
||||
var/medium
|
||||
/// Was the painting loaded from json or created this round
|
||||
var/loaded_from_json = FALSE
|
||||
|
||||
/datum/painting/proc/load_from_json(list/json_data)
|
||||
md5 = json_data["md5"]
|
||||
title = json_data["title"]
|
||||
creator_ckey = json_data["creator_ckey"]
|
||||
creator_name = json_data["creator_name"]
|
||||
creation_date = json_data["creation_date"]
|
||||
creation_round_id = json_data["creation_round_id"]
|
||||
tags = json_data["tags"]
|
||||
patron_ckey = json_data["patron_ckey"]
|
||||
credit_value = json_data["credit_value"]
|
||||
width = json_data["width"]
|
||||
height = json_data["height"]
|
||||
medium = json_data["medium"]
|
||||
loaded_from_json = TRUE
|
||||
|
||||
/datum/painting/proc/to_json()
|
||||
var/list/new_data = list()
|
||||
new_data["md5"] = md5
|
||||
new_data["title"] = title
|
||||
new_data["creator_ckey"] = creator_ckey
|
||||
new_data["creator_name"] = creator_name
|
||||
new_data["creation_date"] = creation_date
|
||||
new_data["creation_round_id"] = creation_round_id
|
||||
new_data["tags"] = tags
|
||||
new_data["patron_ckey"] = patron_ckey
|
||||
new_data["patron_name"] = patron_name
|
||||
new_data["credit_value"] = credit_value
|
||||
new_data["width"] = width
|
||||
new_data["height"] = height
|
||||
new_data["medium"] = medium
|
||||
return new_data
|
||||
|
||||
/// Only returns paintings with 23x23 or 24x24 sizes fitting AI display icon.
|
||||
#define PAINTINGS_FILTER_AI_PORTRAIT 1
|
||||
|
||||
SUBSYSTEM_DEF(persistent_paintings)
|
||||
name = "Persistent Paintings"
|
||||
init_order = INIT_ORDER_PERSISTENT_PAINTINGS
|
||||
@@ -6,24 +95,111 @@ SUBSYSTEM_DEF(persistent_paintings)
|
||||
/// A list of painting frames that this controls
|
||||
var/list/obj/structure/sign/painting/painting_frames = list()
|
||||
|
||||
/// A map of identifiers (such as library) to paintings from paintings.json
|
||||
/// A list of /datum/paintings saved or ready to be saved this round.
|
||||
var/list/paintings = list()
|
||||
|
||||
/datum/controller/subsystem/persistent_paintings/Initialize(start_timeofday)
|
||||
var/json_file = file("data/paintings.json")
|
||||
if(fexists(json_file))
|
||||
paintings = json_decode(file2text(json_file))
|
||||
var/list/raw_data = update_format(json_decode(file2text(json_file)))
|
||||
for(var/list/painting_data as anything in raw_data["paintings"])
|
||||
var/datum/painting/loaded_painting = new
|
||||
loaded_painting.load_from_json(painting_data)
|
||||
paintings += loaded_painting
|
||||
|
||||
for(var/obj/structure/sign/painting/painting_frame as anything in painting_frames)
|
||||
painting_frame.load_persistent()
|
||||
|
||||
return ..()
|
||||
|
||||
/// Generates painting data ready to be consumed by ui
|
||||
/datum/controller/subsystem/persistent_paintings/proc/painting_ui_data(filter=NONE,admin=FALSE)
|
||||
. = list()
|
||||
for(var/datum/painting/painting as anything in paintings)
|
||||
if(filter & PAINTINGS_FILTER_AI_PORTRAIT && ((painting.width != 24 && painting.width != 23) || (painting.height != 24 && painting.height != 23)))
|
||||
continue
|
||||
if(admin)
|
||||
var/list/pdata = painting.to_json()
|
||||
pdata["ref"] = REF(painting)
|
||||
. += list(pdata)
|
||||
else
|
||||
. += list(list("title" = painting.title,"md5" = painting.md5,"ref" = REF(painting)))
|
||||
|
||||
/// Returns paintings with given tag.
|
||||
/datum/controller/subsystem/persistent_paintings/proc/get_paintings_with_tag(tag_name)
|
||||
. = list()
|
||||
for(var/datum/painting/painting as anything in paintings)
|
||||
if(!painting.tags || !(tag_name in painting.tags))
|
||||
continue
|
||||
. += painting
|
||||
|
||||
/// Updates paintings data format to latest if necessary
|
||||
/datum/controller/subsystem/persistent_paintings/proc/update_format(current_data)
|
||||
if(current_data["version"] && current_data["version"] == PAINTINGS_DATA_FORMAT_VERSION)
|
||||
return current_data
|
||||
|
||||
var/current_format = current_data["version"] || 0
|
||||
switch(current_format)
|
||||
if(0)
|
||||
fcopy("data/paintings.json","data/paintings_migration_backup_0.json") //Better safe than losing all metadata
|
||||
var/list/result = list()
|
||||
result["version"] = 1
|
||||
var/list/data = list()
|
||||
// Squash categories into tags
|
||||
for(var/category in current_data)
|
||||
for(var/old_data in current_data[category])
|
||||
var/duplicate_found = FALSE
|
||||
for(var/list/entry in data)
|
||||
if(entry["md5"] == old_data["md5"])
|
||||
entry["tags"] |= category
|
||||
duplicate_found = TRUE
|
||||
break
|
||||
if(duplicate_found)
|
||||
continue
|
||||
var/old_png_path = "data/paintings/[category]/[old_data["md5"]].png"
|
||||
var/new_png_path = "data/paintings/images/[old_data["md5"]].png"
|
||||
fcopy(old_png_path,new_png_path)
|
||||
fdel(old_png_path)
|
||||
var/icon/painting_icon = new(new_png_path)
|
||||
var/width = painting_icon.Width()
|
||||
var/height = painting_icon.Height()
|
||||
var/list/new_data = list()
|
||||
new_data["md5"] = old_data["md5"]
|
||||
new_data["title"] = old_data["title"] || "Untitled Artwork"
|
||||
new_data["creator_ckey"] = old_data["ckey"] || ""
|
||||
new_data["creator_name"] = "Anonymous"
|
||||
new_data["creation_date"] = time2text(world.realtime) // Could use creation/modified file helpers in rustg
|
||||
new_data["creation_round_id"] = GLOB.round_id
|
||||
new_data["tags"] = list(category,"Migrated from version 0")
|
||||
new_data["patron_ckey"] = ""
|
||||
new_data["patron_name"] = ""
|
||||
new_data["credit_value"] = 0
|
||||
new_data["width"] = width
|
||||
new_data["height"] = height
|
||||
new_data["medium"] = "Spraypaint on canvas" //Let's go with most common tool.
|
||||
data += list(new_data)
|
||||
result["paintings"] = data
|
||||
//We're going to save this immidiately this is non-recoverable operation
|
||||
var/json_file = file("data/paintings.json")
|
||||
fdel(json_file)
|
||||
WRITE_FILE(json_file, json_encode(result))
|
||||
return update_format(result)
|
||||
|
||||
/// Saves all persistent paintings
|
||||
/datum/controller/subsystem/persistent_paintings/proc/save_paintings()
|
||||
// Collect new painting data
|
||||
for(var/obj/structure/sign/painting/painting_frame as anything in painting_frames)
|
||||
painting_frame.save_persistent()
|
||||
|
||||
save_to_file()
|
||||
|
||||
/// Saves all currently tracked painting data to file
|
||||
/datum/controller/subsystem/persistent_paintings/proc/save_to_file()
|
||||
var/json_file = file("data/paintings.json")
|
||||
fdel(json_file)
|
||||
WRITE_FILE(json_file, json_encode(paintings))
|
||||
var/list/all_data = list("version" = PAINTINGS_DATA_FORMAT_VERSION)
|
||||
var/list/painting_data = list()
|
||||
for(var/datum/painting/painting as anything in paintings)
|
||||
painting_data += list(painting.to_json())
|
||||
all_data["paintings"] = painting_data
|
||||
WRITE_FILE(json_file, json_encode(all_data))
|
||||
|
||||
@@ -83,6 +83,7 @@ GLOBAL_PROTECT(admin_verbs_admin)
|
||||
/datum/admins/proc/view_all_circuits,
|
||||
/datum/admins/proc/view_all_sdql_spells,
|
||||
/datum/admins/proc/known_alts_panel,
|
||||
/datum/admins/proc/paintings_manager,
|
||||
)
|
||||
GLOBAL_LIST_INIT(admin_verbs_ban, list(/client/proc/unban_panel, /client/proc/ban_panel, /client/proc/stickybanpanel))
|
||||
GLOBAL_PROTECT(admin_verbs_ban)
|
||||
|
||||
96
code/modules/admin/painting_manager.dm
Normal file
96
code/modules/admin/painting_manager.dm
Normal file
@@ -0,0 +1,96 @@
|
||||
/datum/admins/proc/paintings_manager()
|
||||
set name = "Paintings Manager"
|
||||
set category = "Admin"
|
||||
|
||||
if(!check_rights(R_ADMIN))
|
||||
return
|
||||
var/datum/paintings_manager/ui = new(usr)
|
||||
ui.ui_interact(usr)
|
||||
|
||||
/// Painting Admin Management Panel
|
||||
/datum/paintings_manager
|
||||
|
||||
/datum/paintings_manager/ui_state(mob/user)
|
||||
return GLOB.admin_state
|
||||
|
||||
/datum/paintings_manager/ui_close(mob/user)
|
||||
qdel(src)
|
||||
|
||||
/datum/paintings_manager/ui_interact(mob/user, datum/tgui/ui)
|
||||
ui = SStgui.try_update_ui(user, src, ui)
|
||||
if(!ui)
|
||||
ui = new(user, src, "PaintingAdminPanel")
|
||||
ui.open()
|
||||
|
||||
/datum/paintings_manager/ui_assets(mob/user)
|
||||
return list(
|
||||
get_asset_datum(/datum/asset/simple/portraits)
|
||||
)
|
||||
|
||||
/datum/paintings_manager/ui_data(mob/user)
|
||||
. = list()
|
||||
.["paintings"] = SSpersistent_paintings.painting_ui_data(filter = NONE, admin = TRUE)
|
||||
|
||||
/datum/paintings_manager/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
|
||||
if(..())
|
||||
return
|
||||
if (!check_rights(R_ADMIN))
|
||||
return
|
||||
var/mob/user = usr
|
||||
var/datum/painting/chosen_painting = locate(params["ref"]) in SSpersistent_paintings.paintings
|
||||
if(!chosen_painting)
|
||||
return
|
||||
switch(action)
|
||||
if("delete")
|
||||
//Delete the png file
|
||||
var/png = "data/paintings/images/[chosen_painting.md5].png"
|
||||
fdel(png)
|
||||
//Remove entry from paintings list
|
||||
SSpersistent_paintings.paintings -= chosen_painting
|
||||
SSpersistent_paintings.save_to_file() // Save now so we don't have broken variations if this round crashes
|
||||
//Delete any painting instances in the current round
|
||||
for(var/obj/structure/sign/painting/painting as anything in SSpersistent_paintings.painting_frames)
|
||||
if(painting.current_canvas && painting.current_canvas.painting_metadata == chosen_painting)
|
||||
QDEL_NULL(painting.current_canvas)
|
||||
painting.update_appearance()
|
||||
log_admin("[key_name(user)] has deleted a persistent painting made by [chosen_painting.creator_ckey].")
|
||||
message_admins(span_notice("[key_name_admin(user)] has deleted persistent painting made by [chosen_painting.creator_ckey]."))
|
||||
return TRUE
|
||||
if("rename")
|
||||
//Modify the metadata
|
||||
var/old_title = chosen_painting.title
|
||||
var/new_title = stripped_input(user, "New painting title?", "Painting rename", chosen_painting.title)
|
||||
if(!new_title)
|
||||
return
|
||||
chosen_painting.title = new_title
|
||||
log_admin("[key_name(user)] has renamed a persistent painting made by [chosen_painting.creator_ckey] with id [chosen_painting.md5] from [old_title] to [chosen_painting.title].")
|
||||
return TRUE
|
||||
if("rename_author")
|
||||
var/old_name = chosen_painting.creator_name
|
||||
var/new_name = stripped_input(user, "New painting author name?", "Painting rename", chosen_painting.creator_name)
|
||||
if(!new_name)
|
||||
return
|
||||
chosen_painting.creator_name = new_name
|
||||
log_admin("[key_name(user)] has renamed a persistent painting author made by [chosen_painting.creator_name] with id [chosen_painting.md5] from [old_name] to [chosen_painting.creator_name].")
|
||||
return TRUE
|
||||
if("dumpit")
|
||||
//Modify the metadata
|
||||
chosen_painting.patron_name = ""
|
||||
chosen_painting.patron_ckey = ""
|
||||
chosen_painting.credit_value = 0
|
||||
log_admin("[key_name(user)] has reset patronage data on a persistent painting made by [chosen_painting.creator_ckey] with id [chosen_painting.md5].")
|
||||
return TRUE
|
||||
if("remove_tag")
|
||||
if(chosen_painting.tags)
|
||||
chosen_painting.tags -= params["tag"]
|
||||
log_admin("[key_name(user)] has removed tag [params["tag"]] from persistent painting made by [chosen_painting.creator_ckey] with id [chosen_painting.md5].")
|
||||
return TRUE
|
||||
if("add_tag")
|
||||
var/tag_name = stripped_input(user, "New tag name?", "???")
|
||||
if(!tag_name)
|
||||
return
|
||||
if(!chosen_painting.tags)
|
||||
chosen_painting.tags = list()
|
||||
chosen_painting.tags |= tag_name
|
||||
log_admin("[key_name(user)] has added tag [tag_name] to persistent painting made by [chosen_painting.creator_ckey] with id [chosen_painting.md5].")
|
||||
return TRUE
|
||||
@@ -45,16 +45,18 @@
|
||||
var/width = 11
|
||||
var/height = 11
|
||||
var/list/grid
|
||||
var/canvas_color = "#ffffff" //empty canvas color
|
||||
/// empty canvas color
|
||||
var/canvas_color = "#ffffff"
|
||||
/// Is it clean canvas or was there something painted on it at some point, used to decide when to show wip splotch overlay
|
||||
var/used = FALSE
|
||||
var/painting_name = "Untitled Artwork" //Painting name, this is set after framing.
|
||||
var/finalized = FALSE //Blocks edits
|
||||
var/author_ckey
|
||||
var/icon_generated = FALSE
|
||||
var/icon/generated_icon
|
||||
///boolean that blocks persistence from saving it. enabled from printing copies, because we do not want to save copies.
|
||||
var/no_save = FALSE
|
||||
|
||||
var/datum/painting/painting_metadata
|
||||
|
||||
// Painting overlay offset when framed
|
||||
var/framed_offset_x = 11
|
||||
var/framed_offset_y = 10
|
||||
@@ -66,6 +68,12 @@
|
||||
. = ..()
|
||||
reset_grid()
|
||||
|
||||
painting_metadata = new
|
||||
painting_metadata.title = "Untitled Artwork"
|
||||
painting_metadata.creation_round_id = GLOB.round_id
|
||||
painting_metadata.width = width
|
||||
painting_metadata.height = height
|
||||
|
||||
/obj/item/canvas/proc/reset_grid()
|
||||
grid = new/list(width,height)
|
||||
for(var/x in 1 to width)
|
||||
@@ -86,7 +94,6 @@
|
||||
ui = SStgui.try_update_ui(user, src, ui)
|
||||
if(!ui)
|
||||
ui = new(user, src, "Canvas", name)
|
||||
ui.set_autoupdate(FALSE)
|
||||
ui.open()
|
||||
|
||||
/obj/item/canvas/attackby(obj/item/I, mob/living/user, params)
|
||||
@@ -98,8 +105,15 @@
|
||||
/obj/item/canvas/ui_data(mob/user)
|
||||
. = ..()
|
||||
.["grid"] = grid
|
||||
.["name"] = painting_name
|
||||
.["name"] = painting_metadata.title
|
||||
.["author"] = painting_metadata.creator_name
|
||||
.["patron"] = painting_metadata.patron_name
|
||||
.["medium"] = painting_metadata.medium
|
||||
.["date"] = painting_metadata.creation_date
|
||||
.["finalized"] = finalized
|
||||
.["editable"] = !finalized //Ideally you should be able to draw moustaches on existing paintings in the gallery but that's not implemented yet
|
||||
.["show_plaque"] = istype(loc,/obj/structure/sign/painting)
|
||||
.["paint_tool_color"] = get_paint_tool_color(user.get_active_held_item())
|
||||
|
||||
/obj/item/canvas/examine(mob/user)
|
||||
. = ..()
|
||||
@@ -107,32 +121,79 @@
|
||||
|
||||
/obj/item/canvas/ui_act(action, params)
|
||||
. = ..()
|
||||
if(. || finalized)
|
||||
if(.)
|
||||
return
|
||||
var/mob/user = usr
|
||||
switch(action)
|
||||
if("paint")
|
||||
if(finalized)
|
||||
return TRUE
|
||||
var/obj/item/I = user.get_active_held_item()
|
||||
var/color = get_paint_tool_color(I)
|
||||
if(!color)
|
||||
var/tool_color = get_paint_tool_color(I)
|
||||
if(!tool_color)
|
||||
return FALSE
|
||||
var/x = text2num(params["x"])
|
||||
var/y = text2num(params["y"])
|
||||
grid[x][y] = color
|
||||
var/list/data = params["data"]
|
||||
//could maybe validate continuity but eh
|
||||
for(var/point in data)
|
||||
var/x = text2num(point["x"])
|
||||
var/y = text2num(point["y"])
|
||||
grid[x][y] = tool_color
|
||||
var/medium = get_paint_tool_medium(I)
|
||||
if(medium && painting_metadata.medium && painting_metadata.medium != medium)
|
||||
painting_metadata.medium = "Mixed medium"
|
||||
else
|
||||
painting_metadata.medium = medium
|
||||
used = TRUE
|
||||
update_appearance()
|
||||
. = TRUE
|
||||
if("finalize")
|
||||
. = TRUE
|
||||
if(!finalized)
|
||||
finalize(user)
|
||||
if("patronage")
|
||||
. = TRUE
|
||||
patron(user)
|
||||
|
||||
/obj/item/canvas/proc/finalize(mob/user)
|
||||
if(painting_metadata.loaded_from_json || finalized)
|
||||
return
|
||||
finalized = TRUE
|
||||
author_ckey = user.ckey
|
||||
painting_metadata.creator_ckey = user.ckey
|
||||
painting_metadata.creator_name = user.real_name
|
||||
painting_metadata.creation_date = time2text(world.realtime)
|
||||
painting_metadata.creation_round_id = GLOB.round_id
|
||||
generate_proper_overlay()
|
||||
try_rename(user)
|
||||
|
||||
/obj/item/canvas/proc/patron(mob/user)
|
||||
if(!finalized || !painting_metadata.loaded_from_json || !isliving(user))
|
||||
return
|
||||
var/mob/living/living_user = user
|
||||
var/obj/item/card/id/id_card = living_user.get_idcard(TRUE)
|
||||
if(!id_card)
|
||||
to_chat(user,span_notice("You don't even have a id and you want to be an art patron?"))
|
||||
return
|
||||
if(!id_card.registered_account || !id_card.registered_account.account_job)
|
||||
to_chat(user,span_notice("No valid non-departamental account found."))
|
||||
return
|
||||
var/datum/bank_account/account = id_card.registered_account
|
||||
if(account.account_balance < painting_metadata.credit_value)
|
||||
to_chat(user,span_notice("You can't afford this."))
|
||||
return
|
||||
var/sniped_amount = painting_metadata.credit_value
|
||||
var/offer_amount = input(user,"How much do you want to offer ? Minimum : [painting_metadata.credit_value]","Patronage Amount", painting_metadata.credit_value + 1) as num|null
|
||||
if(account.account_balance < offer_amount)
|
||||
to_chat(user,span_notice("You can't afford this."))
|
||||
return
|
||||
if(!offer_amount || sniped_amount != painting_metadata.credit_value || offer_amount < painting_metadata.credit_value+1 || !user.canUseTopic(src))
|
||||
return
|
||||
if(!account.adjust_money(-offer_amount))
|
||||
to_chat(user,span_warning("Transaction failure. Please try again."))
|
||||
return
|
||||
painting_metadata.patron_ckey = user.ckey
|
||||
painting_metadata.patron_name = user.real_name
|
||||
painting_metadata.credit_value = offer_amount
|
||||
to_chat(user,span_notice("Nanotrasen Trust Foundation thanks you for your contribution. You're now offical patron of this painting."))
|
||||
|
||||
/obj/item/canvas/update_overlays()
|
||||
. = ..()
|
||||
if(icon_generated)
|
||||
@@ -153,9 +214,11 @@
|
||||
if(icon_generated)
|
||||
return
|
||||
var/png_filename = "data/paintings/temp_painting.png"
|
||||
var/result = rustg_dmi_create_png(png_filename,"[width]","[height]",get_data_string())
|
||||
var/image_data = get_data_string()
|
||||
var/result = rustg_dmi_create_png(png_filename, "[width]", "[height]", image_data)
|
||||
if(result)
|
||||
CRASH("Error generating painting png : [result]")
|
||||
painting_metadata.md5 = md5(lowertext(image_data))
|
||||
generated_icon = new(png_filename)
|
||||
icon_generated = TRUE
|
||||
update_appearance()
|
||||
@@ -168,14 +231,17 @@
|
||||
return data.Join("")
|
||||
|
||||
//Todo make this element ?
|
||||
/obj/item/canvas/proc/get_paint_tool_color(obj/item/I)
|
||||
if(!I)
|
||||
/obj/item/canvas/proc/get_paint_tool_color(obj/item/painting_implement)
|
||||
if(!painting_implement)
|
||||
return
|
||||
if(istype(I, /obj/item/toy/crayon))
|
||||
var/obj/item/toy/crayon/crayon = I
|
||||
if(istype(painting_implement, /obj/item/paint_palette))
|
||||
var/obj/item/paint_palette/palette = painting_implement
|
||||
return palette.current_color
|
||||
if(istype(painting_implement, /obj/item/toy/crayon))
|
||||
var/obj/item/toy/crayon/crayon = painting_implement
|
||||
return crayon.paint_color
|
||||
else if(istype(I, /obj/item/pen))
|
||||
var/obj/item/pen/P = I
|
||||
else if(istype(painting_implement, /obj/item/pen))
|
||||
var/obj/item/pen/P = painting_implement
|
||||
switch(P.colour)
|
||||
if("black")
|
||||
return "#000000"
|
||||
@@ -184,15 +250,38 @@
|
||||
if("red")
|
||||
return "#ff0000"
|
||||
return P.colour
|
||||
else if(istype(I, /obj/item/soap) || istype(I, /obj/item/reagent_containers/glass/rag))
|
||||
else if(istype(painting_implement, /obj/item/soap) || istype(painting_implement, /obj/item/reagent_containers/glass/rag))
|
||||
return canvas_color
|
||||
|
||||
/// Generates medium description
|
||||
/obj/item/canvas/proc/get_paint_tool_medium(obj/item/painting_implement)
|
||||
if(!painting_implement)
|
||||
return
|
||||
if(istype(painting_implement, /obj/item/paint_palette))
|
||||
return "Oil on canvas"
|
||||
else if(istype(painting_implement, /obj/item/toy/crayon/spraycan))
|
||||
return "Spraycan on canvas"
|
||||
else if(istype(painting_implement, /obj/item/toy/crayon))
|
||||
return "Crayon on canvas"
|
||||
else if(istype(painting_implement, /obj/item/pen))
|
||||
return "Ink on canvas"
|
||||
else if(istype(painting_implement, /obj/item/soap) || istype(painting_implement, /obj/item/reagent_containers/glass/rag))
|
||||
return //These are just for cleaning, ignore them
|
||||
else
|
||||
return "Unknown medium"
|
||||
|
||||
/obj/item/canvas/proc/try_rename(mob/user)
|
||||
if(painting_metadata.loaded_from_json) // No renaming old paintings
|
||||
return
|
||||
var/new_name = stripped_input(user,"What do you want to name the painting?")
|
||||
if(new_name != painting_name && new_name && user.canUseTopic(src,BE_CLOSE))
|
||||
painting_name = new_name
|
||||
if(new_name != painting_metadata.title && new_name && user.canUseTopic(src, BE_CLOSE))
|
||||
painting_metadata.title = new_name
|
||||
var/sign_choice = tgui_alert(user, "Do you want to sign it or remain anonymous?", "Sign painting?", list("Yes", "No"))
|
||||
if(sign_choice != "Yes")
|
||||
painting_metadata.creator_name = "Anonymous"
|
||||
SStgui.update_uis(src)
|
||||
|
||||
|
||||
/obj/item/canvas/nineteen_nineteen
|
||||
name = "canvas (19x19)"
|
||||
icon_state = "19x19"
|
||||
@@ -271,7 +360,7 @@
|
||||
/obj/structure/sign/painting/attackby(obj/item/I, mob/user, params)
|
||||
if(!current_canvas && istype(I, /obj/item/canvas))
|
||||
frame_canvas(user,I)
|
||||
else if(current_canvas && current_canvas.painting_name == initial(current_canvas.painting_name) && istype(I,/obj/item/pen))
|
||||
else if(current_canvas && current_canvas.painting_metadata.title == initial(current_canvas.painting_metadata.title) && istype(I,/obj/item/pen))
|
||||
try_rename(user)
|
||||
else
|
||||
return ..()
|
||||
@@ -302,11 +391,11 @@
|
||||
update_appearance()
|
||||
|
||||
/obj/structure/sign/painting/proc/try_rename(mob/user)
|
||||
if(current_canvas.painting_name == initial(current_canvas.painting_name))
|
||||
if(current_canvas.painting_metadata.title == initial(current_canvas.painting_metadata.title))
|
||||
current_canvas.try_rename(user)
|
||||
|
||||
/obj/structure/sign/painting/update_name(updates)
|
||||
name = current_canvas ? "painting - [current_canvas.painting_name]" : initial(name)
|
||||
name = current_canvas ? "painting - [current_canvas.painting_metadata.title]" : initial(name)
|
||||
return ..()
|
||||
|
||||
/obj/structure/sign/painting/update_desc(updates)
|
||||
@@ -337,24 +426,13 @@
|
||||
* Deleting paintings leaves their json, so this proc will remove the json and try again if it finds one of those.
|
||||
*/
|
||||
/obj/structure/sign/painting/proc/load_persistent()
|
||||
if(!persistence_id || !SSpersistent_paintings.paintings[persistence_id])
|
||||
if(!persistence_id)
|
||||
return
|
||||
var/list/painting_category = SSpersistent_paintings.paintings[persistence_id]
|
||||
var/list/painting
|
||||
while(!painting)
|
||||
if(!length(SSpersistent_paintings.paintings[persistence_id]))
|
||||
var/list/valid_paintings = SSpersistent_paintings.get_paintings_with_tag(persistence_id)
|
||||
if(!length(valid_paintings))
|
||||
return //aborts loading anything this category has no usable paintings
|
||||
var/list/chosen = pick(painting_category)
|
||||
if(!fexists("data/paintings/[persistence_id]/[chosen["md5"]].png")) //shitmin deleted this art, lets remove json entry to avoid errors
|
||||
painting_category -= list(chosen)
|
||||
continue //and try again
|
||||
painting = chosen
|
||||
var/title = painting["title"]
|
||||
var/author = painting["ckey"]
|
||||
var/png = "data/paintings/[persistence_id]/[painting["md5"]].png"
|
||||
if(!title)
|
||||
title = "Untitled Artwork" //legacy artwork allowed null names which was bad for the json, lets fix that
|
||||
painting["title"] = title
|
||||
var/datum/painting/painting = pick(valid_paintings)
|
||||
var/png = "data/paintings/images/[painting.md5].png"
|
||||
var/icon/I = new(png)
|
||||
var/obj/item/canvas/new_canvas
|
||||
var/w = I.Width()
|
||||
@@ -364,39 +442,43 @@
|
||||
if(initial(new_canvas.width) == w && initial(new_canvas.height) == h)
|
||||
new_canvas = new T(src)
|
||||
break
|
||||
if(!istype(new_canvas))
|
||||
CRASH("Found painting size with no matching canvas type")
|
||||
new_canvas.painting_metadata = painting
|
||||
new_canvas.fill_grid_from_icon(I)
|
||||
new_canvas.generated_icon = I
|
||||
new_canvas.icon_generated = TRUE
|
||||
new_canvas.finalized = TRUE
|
||||
new_canvas.painting_name = title
|
||||
new_canvas.author_ckey = author
|
||||
new_canvas.name = "painting - [title]"
|
||||
new_canvas.name = "painting - [painting.title]"
|
||||
current_canvas = new_canvas
|
||||
current_canvas.update_appearance()
|
||||
update_appearance()
|
||||
|
||||
/obj/structure/sign/painting/proc/save_persistent()
|
||||
if(!persistence_id || !current_canvas || current_canvas.no_save)
|
||||
if(!persistence_id || !current_canvas || current_canvas.no_save || current_canvas.painting_metadata.loaded_from_json)
|
||||
return
|
||||
if(SANITIZE_FILENAME(persistence_id) != persistence_id)
|
||||
stack_trace("Invalid persistence_id - [persistence_id]")
|
||||
return
|
||||
if(!current_canvas.painting_name)
|
||||
current_canvas.painting_name = "Untitled Artwork"
|
||||
var/data = current_canvas.get_data_string()
|
||||
var/md5 = md5(lowertext(data))
|
||||
var/list/current = SSpersistent_paintings.paintings[persistence_id]
|
||||
if(!current)
|
||||
current = list()
|
||||
for(var/list/entry in current)
|
||||
if(entry["md5"] == md5)
|
||||
for(var/datum/painting/entry in SSpersistent_paintings.paintings)
|
||||
if(entry.md5 == md5) // No duplicates
|
||||
return
|
||||
var/png_directory = "data/paintings/[persistence_id]/"
|
||||
current_canvas.painting_metadata.md5 = md5
|
||||
if(!current_canvas.painting_metadata.tags)
|
||||
current_canvas.painting_metadata.tags = list(persistence_id)
|
||||
else
|
||||
current_canvas.painting_metadata.tags |= persistence_id
|
||||
var/png_directory = "data/paintings/images/"
|
||||
var/png_path = png_directory + "[md5].png"
|
||||
var/result = rustg_dmi_create_png(png_path,"[current_canvas.width]","[current_canvas.height]",data)
|
||||
if(result)
|
||||
CRASH("Error saving persistent painting: [result]")
|
||||
current += list(list("title" = current_canvas.painting_name , "md5" = md5, "ckey" = current_canvas.author_ckey))
|
||||
SSpersistent_paintings.paintings[persistence_id] = current
|
||||
SSpersistent_paintings.paintings += current_canvas.painting_metadata
|
||||
|
||||
/obj/item/canvas/proc/fill_grid_from_icon(icon/I)
|
||||
var/h = I.Height() + 1
|
||||
@@ -423,31 +505,20 @@
|
||||
desc_with_canvas = "A painting hung away from lesser minds."
|
||||
persistence_id = "library_private"
|
||||
|
||||
/obj/structure/sign/painting/vv_get_dropdown()
|
||||
. = ..()
|
||||
VV_DROPDOWN_OPTION(VV_HK_REMOVE_PAINTING, "Remove Persistent Painting")
|
||||
/// Simple painting utility.
|
||||
/obj/item/paint_palette
|
||||
name = "paint palette"
|
||||
desc = "paintbrush included"
|
||||
icon = 'icons/obj/artstuff.dmi'
|
||||
icon_state = "palette"
|
||||
lefthand_file = 'icons/mob/inhands/equipment/palette_lefthand.dmi'
|
||||
righthand_file = 'icons/mob/inhands/equipment/palette_righthand.dmi'
|
||||
w_class = WEIGHT_CLASS_TINY
|
||||
///Chosen paint color
|
||||
var/current_color
|
||||
|
||||
/obj/structure/sign/painting/vv_do_topic(list/href_list)
|
||||
/obj/item/paint_palette/attack_self(mob/user, modifiers)
|
||||
. = ..()
|
||||
if(href_list[VV_HK_REMOVE_PAINTING])
|
||||
if(!check_rights(NONE))
|
||||
return
|
||||
var/mob/user = usr
|
||||
if(!persistence_id || !current_canvas)
|
||||
to_chat(user,span_warning("This is not a persistent painting."))
|
||||
return
|
||||
var/md5 = md5(lowertext(current_canvas.get_data_string()))
|
||||
var/author = current_canvas.author_ckey
|
||||
var/list/current = SSpersistent_paintings.paintings[persistence_id]
|
||||
if(current)
|
||||
for(var/list/entry in current)
|
||||
if(entry["md5"] == md5)
|
||||
current -= entry
|
||||
var/png = "data/paintings/[persistence_id]/[md5].png"
|
||||
fdel(png)
|
||||
for(var/obj/structure/sign/painting/painting as anything in SSpersistent_paintings.painting_frames)
|
||||
if(painting.current_canvas && md5(painting.current_canvas.get_data_string()) == md5)
|
||||
QDEL_NULL(painting.current_canvas)
|
||||
painting.update_appearance()
|
||||
log_admin("[key_name(user)] has deleted a persistent painting made by [author].")
|
||||
message_admins(span_notice("[key_name_admin(user)] has deleted persistent painting made by [author]."))
|
||||
var/chosen_color = input(user,"Pick new color","Palette") as color|null
|
||||
if(chosen_color)
|
||||
current_color = chosen_color
|
||||
|
||||
@@ -442,28 +442,18 @@
|
||||
..()
|
||||
|
||||
/datum/asset/simple/portraits
|
||||
var/tab = "use subtypes of this please"
|
||||
assets = list()
|
||||
|
||||
/datum/asset/simple/portraits/New()
|
||||
if(!length(SSpersistent_paintings.paintings[tab]))
|
||||
if(!length(SSpersistent_paintings.paintings))
|
||||
return
|
||||
for(var/list/portrait as anything in SSpersistent_paintings.paintings[tab])
|
||||
var/png = "data/paintings/[tab]/[portrait["md5"]].png"
|
||||
for(var/datum/painting/portrait as anything in SSpersistent_paintings.paintings)
|
||||
var/png = "data/paintings/images/[portrait.md5].png"
|
||||
if(fexists(png))
|
||||
var/asset_name = "[tab]_[portrait["md5"]]"
|
||||
var/asset_name = "paintings_[portrait.md5]"
|
||||
assets[asset_name] = png
|
||||
..() //this is where it registers all these assets we added to the list
|
||||
|
||||
/datum/asset/simple/portraits/library
|
||||
tab = "library"
|
||||
|
||||
/datum/asset/simple/portraits/library_secure
|
||||
tab = "library_secure"
|
||||
|
||||
/datum/asset/simple/portraits/library_private
|
||||
tab = "library_private"
|
||||
|
||||
/datum/asset/simple/safe
|
||||
assets = list(
|
||||
"safe_dial.png" = 'icons/ui_icons/safe/safe_dial.png'
|
||||
|
||||
@@ -30,16 +30,12 @@
|
||||
|
||||
/datum/portrait_picker/ui_assets(mob/user)
|
||||
return list(
|
||||
get_asset_datum(/datum/asset/simple/portraits/library),
|
||||
get_asset_datum(/datum/asset/simple/portraits/library_secure),
|
||||
get_asset_datum(/datum/asset/simple/portraits/library_private)
|
||||
get_asset_datum(/datum/asset/simple/portraits)
|
||||
)
|
||||
|
||||
/datum/portrait_picker/ui_data(mob/user)
|
||||
var/list/data = list()
|
||||
data["library"] = SSpersistent_paintings.paintings["library"] ? SSpersistent_paintings.paintings["library"] : 0
|
||||
data["library_secure"] = SSpersistent_paintings.paintings["library_secure"] ? SSpersistent_paintings.paintings["library_secure"] : 0
|
||||
data["library_private"] = SSpersistent_paintings.paintings["library_private"] ? SSpersistent_paintings.paintings["library_private"] : 0 //i'm gonna regret this, won't i?
|
||||
data["paintings"] = SSpersistent_paintings.painting_ui_data(filter = PAINTINGS_FILTER_AI_PORTRAIT)
|
||||
return data
|
||||
|
||||
/datum/portrait_picker/ui_act(action, params)
|
||||
@@ -48,11 +44,13 @@
|
||||
return
|
||||
switch(action)
|
||||
if("select")
|
||||
var/list/tab2key = list(TAB_LIBRARY = "library", TAB_SECURE = "library_secure", TAB_PRIVATE = "library_private")
|
||||
var/folder = tab2key[params["tab"]]
|
||||
var/list/current_list = SSpersistent_paintings.paintings[folder]
|
||||
var/list/chosen_portrait = current_list[params["selected"]]
|
||||
var/png = "data/paintings/[folder]/[chosen_portrait["md5"]].png"
|
||||
//var/list/tab2key = list(TAB_LIBRARY = "library", TAB_SECURE = "library_secure", TAB_PRIVATE = "library_private")
|
||||
//var/folder = tab2key[params["tab"]]
|
||||
//var/list/current_list = SSpersistent_paintings.paintings[folder]
|
||||
var/datum/painting/chosen_portrait = locate(params["selected"]) in SSpersistent_paintings.paintings
|
||||
if(!chosen_portrait)
|
||||
return
|
||||
var/png = "data/paintings/images/[chosen_portrait.md5].png"
|
||||
var/icon/portrait_icon = new(png)
|
||||
var/mob/living/ai = holder.mob
|
||||
var/w = portrait_icon.Width()
|
||||
|
||||
@@ -23,16 +23,12 @@
|
||||
|
||||
/datum/computer_file/program/portrait_printer/ui_data(mob/user)
|
||||
var/list/data = list()
|
||||
data["library"] = SSpersistent_paintings.paintings["library"] ? SSpersistent_paintings.paintings["library"] : 0
|
||||
data["library_secure"] = SSpersistent_paintings.paintings["library_secure"] ? SSpersistent_paintings.paintings["library_secure"] : 0
|
||||
data["library_private"] = SSpersistent_paintings.paintings["library_private"] ? SSpersistent_paintings.paintings["library_private"] : 0 //i'm gonna regret this, won't i?
|
||||
data["paintings"] = SSpersistent_paintings.painting_ui_data()
|
||||
return data
|
||||
|
||||
/datum/computer_file/program/portrait_printer/ui_assets(mob/user)
|
||||
return list(
|
||||
get_asset_datum(/datum/asset/simple/portraits/library),
|
||||
get_asset_datum(/datum/asset/simple/portraits/library_secure),
|
||||
get_asset_datum(/datum/asset/simple/portraits/library_private)
|
||||
get_asset_datum(/datum/asset/simple/portraits)
|
||||
)
|
||||
|
||||
/datum/computer_file/program/portrait_printer/ui_act(action, params)
|
||||
@@ -53,13 +49,9 @@
|
||||
printer.stored_paper -= CANVAS_PAPER_COST
|
||||
|
||||
//canvas printing!
|
||||
var/list/tab2key = list(TAB_LIBRARY = "library", TAB_SECURE = "library_secure", TAB_PRIVATE = "library_private")
|
||||
var/folder = tab2key[params["tab"]]
|
||||
var/list/current_list = SSpersistent_paintings.paintings[folder]
|
||||
var/list/chosen_portrait = current_list[params["selected"]]
|
||||
var/author = chosen_portrait["author"]
|
||||
var/title = chosen_portrait["title"]
|
||||
var/png = "data/paintings/[folder]/[chosen_portrait["md5"]].png"
|
||||
var/datum/painting/chosen_portrait = locate(params["selected"]) in SSpersistent_paintings.paintings
|
||||
|
||||
var/png = "data/paintings/images/[chosen_portrait.md5].png"
|
||||
var/icon/art_icon = new(png)
|
||||
var/obj/item/canvas/printed_canvas
|
||||
var/art_width = art_icon.Width()
|
||||
@@ -69,15 +61,17 @@
|
||||
if(initial(printed_canvas.width) == art_width && initial(printed_canvas.height) == art_height)
|
||||
printed_canvas = new canvas_type(get_turf(computer.physical))
|
||||
break
|
||||
printed_canvas = null
|
||||
if(!printed_canvas)
|
||||
return
|
||||
printed_canvas.painting_metadata = chosen_portrait
|
||||
printed_canvas.fill_grid_from_icon(art_icon)
|
||||
printed_canvas.generated_icon = art_icon
|
||||
printed_canvas.icon_generated = TRUE
|
||||
printed_canvas.finalized = TRUE
|
||||
printed_canvas.painting_name = title
|
||||
printed_canvas.author_ckey = author
|
||||
printed_canvas.name = "painting - [title]"
|
||||
printed_canvas.name = "painting - [chosen_portrait.title]"
|
||||
///this is a copy of something that is already in the database- it should not be able to be saved.
|
||||
printed_canvas.no_save = TRUE
|
||||
printed_canvas.update_icon()
|
||||
to_chat(usr, span_notice("You have printed [title] onto a new canvas."))
|
||||
to_chat(usr, span_notice("You have printed [chosen_portrait.title] onto a new canvas."))
|
||||
playsound(computer.physical, 'sound/items/poster_being_created.ogg', 100, TRUE)
|
||||
|
||||
@@ -34,7 +34,8 @@
|
||||
/obj/item/razor=3,
|
||||
/obj/item/canvas/nineteen_nineteen = 5,
|
||||
/obj/item/canvas/twentythree_nineteen = 5,
|
||||
/obj/item/canvas/twentythree_twentythree = 5
|
||||
/obj/item/canvas/twentythree_twentythree = 5,
|
||||
/obj/item/paint_palette = 3
|
||||
)
|
||||
//SKYRAT EDIT: Adds Ceramic, Glassblowing, and Fishing Skillchips
|
||||
contraband = list(
|
||||
|
||||
BIN
icons/mob/inhands/equipment/palette_lefthand.dmi
Normal file
BIN
icons/mob/inhands/equipment/palette_lefthand.dmi
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 383 B |
BIN
icons/mob/inhands/equipment/palette_righthand.dmi
Normal file
BIN
icons/mob/inhands/equipment/palette_righthand.dmi
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 381 B |
Binary file not shown.
|
Before Width: | Height: | Size: 980 B After Width: | Height: | Size: 1.3 KiB |
@@ -1775,6 +1775,7 @@
|
||||
#include "code\modules\admin\outfit_editor.dm"
|
||||
#include "code\modules\admin\outfit_manager.dm"
|
||||
#include "code\modules\admin\outfits.dm"
|
||||
#include "code\modules\admin\painting_manager.dm"
|
||||
#include "code\modules\admin\permissionedit.dm"
|
||||
#include "code\modules\admin\player_panel.dm"
|
||||
#include "code\modules\admin\poll_management.dm"
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
import { Component, createRef } from 'inferno';
|
||||
import { useBackend } from '../backend';
|
||||
import { Box, Button } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
|
||||
const PX_PER_UNIT = 24;
|
||||
|
||||
class PaintCanvas extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.canvasRef = createRef();
|
||||
this.onCVClick = props.onCanvasClick;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.drawCanvas(this.props);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.drawCanvas(this.props);
|
||||
}
|
||||
|
||||
drawCanvas(propSource) {
|
||||
const ctx = this.canvasRef.current.getContext("2d");
|
||||
const grid = propSource.value;
|
||||
const x_size = grid.length;
|
||||
if (!x_size) {
|
||||
return;
|
||||
}
|
||||
const y_size = grid[0].length;
|
||||
const x_scale = Math.round(this.canvasRef.current.width / x_size);
|
||||
const y_scale = Math.round(this.canvasRef.current.height / y_size);
|
||||
ctx.save();
|
||||
ctx.scale(x_scale, y_scale);
|
||||
for (let x = 0; x < grid.length; x++) {
|
||||
const element = grid[x];
|
||||
for (let y = 0; y < element.length; y++) {
|
||||
const color = element[y];
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(x, y, 1, 1);
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
clickwrapper(event) {
|
||||
const x_size = this.props.value.length;
|
||||
if (!x_size) {
|
||||
return;
|
||||
}
|
||||
const y_size = this.props.value[0].length;
|
||||
const x_scale = this.canvasRef.current.width / x_size;
|
||||
const y_scale = this.canvasRef.current.height / y_size;
|
||||
const x = Math.floor(event.offsetX / x_scale)+1;
|
||||
const y = Math.floor(event.offsetY / y_scale)+1;
|
||||
this.onCVClick(x, y);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
res = 1,
|
||||
value,
|
||||
dotsize = PX_PER_UNIT,
|
||||
...rest
|
||||
} = this.props;
|
||||
const [width, height] = getImageSize(value);
|
||||
return (
|
||||
<canvas
|
||||
ref={this.canvasRef}
|
||||
width={(width * dotsize) || 300}
|
||||
height={(height * dotsize) || 300}
|
||||
{...rest}
|
||||
onClick={e => this.clickwrapper(e)}>
|
||||
Canvas failed to render.
|
||||
</canvas>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getImageSize = value => {
|
||||
const width = value.length;
|
||||
const height = width !== 0 ? value[0].length : 0;
|
||||
return [width, height];
|
||||
};
|
||||
|
||||
export const Canvas = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
const dotsize = PX_PER_UNIT;
|
||||
const [width, height] = getImageSize(data.grid);
|
||||
return (
|
||||
<Window
|
||||
width={Math.min(700, width * dotsize + 72)}
|
||||
height={Math.min(700, height * dotsize + 72)}>
|
||||
<Window.Content>
|
||||
<Box textAlign="center">
|
||||
<PaintCanvas
|
||||
value={data.grid}
|
||||
dotsize={dotsize}
|
||||
onCanvasClick={(x, y) => act("paint", { x, y })} />
|
||||
<Box>
|
||||
{!data.finalized && (
|
||||
<Button.Confirm
|
||||
onClick={() => act("finalize")}
|
||||
content="Finalize" />
|
||||
)}
|
||||
{data.name}
|
||||
</Box>
|
||||
</Box>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
253
tgui/packages/tgui/interfaces/Canvas.tsx
Normal file
253
tgui/packages/tgui/interfaces/Canvas.tsx
Normal file
@@ -0,0 +1,253 @@
|
||||
import { Color } from 'common/color';
|
||||
import { Component, createRef, RefObject } from 'inferno';
|
||||
import { useBackend } from '../backend';
|
||||
import { Box, Button, Flex } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
|
||||
const PX_PER_UNIT = 24;
|
||||
|
||||
type PaintCanvasProps = Partial<{
|
||||
onCanvasModifiedHandler: (data : PointData[]) => void,
|
||||
value: string[][],
|
||||
width: number,
|
||||
height: number,
|
||||
imageWidth: number,
|
||||
imageHeight: number,
|
||||
editable: boolean,
|
||||
drawing_color: string | null,
|
||||
}>;
|
||||
|
||||
type PointData = {
|
||||
x: number,
|
||||
y: number,
|
||||
color: Color
|
||||
}
|
||||
|
||||
const fromDM = (data: string[][]) => {
|
||||
return data.map(inner => inner.map(v => Color.fromHex(v)));
|
||||
};
|
||||
|
||||
const toMassPaintFormat = (data: PointData[]) => {
|
||||
return data.map(p => ({ x: p.x+1, y: p.y+1 })); // 1-based index dm side
|
||||
};
|
||||
|
||||
class PaintCanvas extends Component<PaintCanvasProps> {
|
||||
canvasRef: RefObject<HTMLCanvasElement>;
|
||||
baseImageData: Color[][]
|
||||
modifiedElements: PointData[];
|
||||
onCanvasModified: (data: PointData[]) => void;
|
||||
drawing: boolean;
|
||||
drawing_color: string;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.canvasRef = createRef<HTMLCanvasElement>();
|
||||
this.modifiedElements = [];
|
||||
this.drawing = false;
|
||||
this.onCanvasModified = props.onCanvasModifiedHandler;
|
||||
|
||||
this.handleStartDrawing = this.handleStartDrawing.bind(this);
|
||||
this.handleDrawing = this.handleDrawing.bind(this);
|
||||
this.handleEndDrawing = this.handleEndDrawing.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.prepareCanvas();
|
||||
this.syncCanvas();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
// eslint-disable-next-line max-len
|
||||
if (this.props.value !== undefined && JSON.stringify(this.baseImageData) !== JSON.stringify(fromDM(this.props.value))) {
|
||||
this.syncCanvas();
|
||||
}
|
||||
}
|
||||
|
||||
prepareCanvas() {
|
||||
const canvas = this.canvasRef.current!;
|
||||
const ctx = canvas.getContext("2d");
|
||||
const width = this.props.width || canvas.width || 360;
|
||||
const height = this.props.height || canvas.height || 360;
|
||||
const x_resolution = this.props.imageWidth || 36;
|
||||
const y_resolution = this.props.imageHeight || 36;
|
||||
const x_scale = Math.round(width / x_resolution);
|
||||
const y_scale = Math.round(height / y_resolution);
|
||||
ctx?.setTransform(1, 0, 0, 1, 0, 0);
|
||||
ctx?.scale(x_scale, y_scale); // This clears the canvas.
|
||||
}
|
||||
|
||||
syncCanvas() {
|
||||
if (this.props.value === undefined) {
|
||||
return;
|
||||
}
|
||||
this.baseImageData = fromDM(this.props.value);
|
||||
this.modifiedElements = [];
|
||||
|
||||
const canvas = this.canvasRef.current!;
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
for (let x = 0; x < this.baseImageData.length; x++) {
|
||||
const element = this.baseImageData[x];
|
||||
for (let y = 0; y < element.length; y++) {
|
||||
const color = element[y];
|
||||
ctx.fillStyle = color.toString();
|
||||
ctx.fillRect(x, y, 1, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eventToCoords(event : MouseEvent) {
|
||||
const canvas = this.canvasRef.current!;
|
||||
const width = this.props.width || canvas.width || 360;
|
||||
const height = this.props.height || canvas.height || 360;
|
||||
const x_resolution = this.props.imageWidth || 36;
|
||||
const y_resolution = this.props.imageHeight || 36;
|
||||
const x_scale = Math.round(width / x_resolution);
|
||||
const y_scale = Math.round(height / y_resolution);
|
||||
const x = Math.floor(event.offsetX / x_scale);
|
||||
const y = Math.floor(event.offsetY / y_scale);
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
handleStartDrawing(event : MouseEvent) {
|
||||
if (!this.props.editable
|
||||
|| this.props.drawing_color === undefined
|
||||
|| this.props.drawing_color === null) {
|
||||
return;
|
||||
}
|
||||
this.modifiedElements = [];
|
||||
this.drawing = true;
|
||||
this.drawing_color = this.props.drawing_color;
|
||||
const coords = this.eventToCoords(event);
|
||||
this.drawPoint(coords.x, coords.y, this.drawing_color);
|
||||
}
|
||||
|
||||
drawPoint(x: number, y: number, color: any) {
|
||||
let p: PointData = { x, y, color: Color.fromHex(color) };
|
||||
this.modifiedElements.push(p);
|
||||
const canvas = this.canvasRef.current!;
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(x, y, 1, 1);
|
||||
}
|
||||
|
||||
handleDrawing(event: MouseEvent) {
|
||||
if (!this.drawing) {
|
||||
return;
|
||||
}
|
||||
const coords = this.eventToCoords(event);
|
||||
this.drawPoint(coords.x, coords.y, this.drawing_color);
|
||||
}
|
||||
|
||||
handleEndDrawing(event: MouseEvent) {
|
||||
if (!this.drawing) {
|
||||
return;
|
||||
}
|
||||
this.drawing = false;
|
||||
const canvas = this.canvasRef.current!;
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
if (this.onCanvasModified !== undefined) {
|
||||
this.onCanvasModified(this.modifiedElements);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
value,
|
||||
width = 300,
|
||||
height = 300,
|
||||
imageWidth = 36,
|
||||
imageHeight = 36,
|
||||
...rest
|
||||
} = this.props;
|
||||
return (
|
||||
<canvas
|
||||
ref={this.canvasRef}
|
||||
width={width}
|
||||
height={height}
|
||||
{...rest}
|
||||
onMouseDown={this.handleStartDrawing}
|
||||
onMouseMove={this.handleDrawing}
|
||||
onMouseUp={this.handleEndDrawing}
|
||||
onMouseOut={this.handleEndDrawing}>
|
||||
Canvas failed to render.
|
||||
</canvas>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getImageSize = value => {
|
||||
const width = value.length;
|
||||
const height = width !== 0 ? value[0].length : 0;
|
||||
return [width, height];
|
||||
};
|
||||
|
||||
type CanvasData = {
|
||||
grid: string[][],
|
||||
finalized: boolean,
|
||||
name: string,
|
||||
editable: boolean,
|
||||
paint_tool_color: string | null,
|
||||
author: string | null,
|
||||
medium: string | null,
|
||||
patron: string | null,
|
||||
date: string | null,
|
||||
show_plaque: boolean
|
||||
}
|
||||
|
||||
export const Canvas = (props, context) => {
|
||||
const { act, data } = useBackend<CanvasData>(context);
|
||||
const [width, height] = getImageSize(data.grid);
|
||||
const scaled_width = width * PX_PER_UNIT;
|
||||
const scaled_height = height * PX_PER_UNIT;
|
||||
const average_plaque_height = 90;
|
||||
return (
|
||||
<Window
|
||||
width={scaled_width + 72}
|
||||
height={scaled_height + 70
|
||||
+ (data.show_plaque ? average_plaque_height : 0)}>
|
||||
<Window.Content>
|
||||
<Box textAlign="center">
|
||||
<PaintCanvas
|
||||
value={data.grid}
|
||||
imageWidth={width}
|
||||
imageHeight={height}
|
||||
width={scaled_width}
|
||||
height={scaled_height}
|
||||
drawing_color={data.paint_tool_color}
|
||||
onCanvasModifiedHandler={(changed) => act("paint", { data: toMassPaintFormat(changed) })}
|
||||
editable={data.editable}
|
||||
/>
|
||||
<Flex align="center" justify="center">
|
||||
{!data.finalized && (
|
||||
<Flex.Item>
|
||||
<Button.Confirm
|
||||
onClick={() => act("finalize")}
|
||||
content="Finalize" />
|
||||
</Flex.Item>
|
||||
)}
|
||||
{!!data.finalized && !!data.show_plaque && (
|
||||
<Flex.Item
|
||||
p={2}
|
||||
width="60%"
|
||||
textColor="black"
|
||||
textAlign="left"
|
||||
backgroundColor="white"
|
||||
style={{ "border-style": "inset" }}>
|
||||
<Box mb={1} fontSize="18px" bold>{data.name}</Box>
|
||||
<Box bold>
|
||||
{data.author}
|
||||
{!!data.date && `- ${new Date(data.date).getFullYear()+540}`}
|
||||
</Box>
|
||||
<Box italic>{data.medium}</Box>
|
||||
<Box italic>
|
||||
{!!data.patron && `Sponsored by ${data.patron} `}
|
||||
<Button icon="hand-holding-usd" color="transparent" iconColor="black" onClick={() => act("patronage")} />
|
||||
</Box>
|
||||
</Flex.Item>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
@@ -1,37 +1,16 @@
|
||||
import { resolveAsset } from '../assets';
|
||||
import { useBackend, useLocalState } from '../backend';
|
||||
import { Button, NoticeBox, Section, Stack, Tabs } from '../components';
|
||||
import { Button, NoticeBox, Section, Stack } from '../components';
|
||||
import { NtosWindow } from '../layouts';
|
||||
|
||||
export const NtosPortraitPrinter = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
const [tabIndex, setTabIndex] = useLocalState(context, 'tabIndex', 0);
|
||||
const [listIndex, setListIndex] = useLocalState(context, 'listIndex', 0);
|
||||
const {
|
||||
library,
|
||||
library_secure,
|
||||
library_private,
|
||||
paintings,
|
||||
} = data;
|
||||
const TABS = [
|
||||
{
|
||||
name: 'Common Portraits',
|
||||
asset_prefix: "library",
|
||||
list: library,
|
||||
},
|
||||
{
|
||||
name: 'Secure Portraits',
|
||||
asset_prefix: "library_secure",
|
||||
list: library_secure,
|
||||
},
|
||||
{
|
||||
name: 'Private Portraits',
|
||||
asset_prefix: "library_private",
|
||||
list: library_private,
|
||||
},
|
||||
];
|
||||
const tab2list = TABS[tabIndex].list;
|
||||
const current_portrait_title = tab2list[listIndex]["title"];
|
||||
const current_portrait_asset_name = TABS[tabIndex].asset_prefix + "_" + tab2list[listIndex]["md5"];
|
||||
const current_portrait_title = paintings[listIndex]["title"];
|
||||
const current_portrait_asset_name = "paintings" + "_" + paintings[listIndex]["md5"];
|
||||
return (
|
||||
<NtosWindow
|
||||
title="Art Galaxy"
|
||||
@@ -39,23 +18,6 @@ export const NtosPortraitPrinter = (props, context) => {
|
||||
height={406}>
|
||||
<NtosWindow.Content>
|
||||
<Stack vertical fill>
|
||||
<Stack.Item>
|
||||
<Section fitted>
|
||||
<Tabs fluid textAlign="center">
|
||||
{TABS.map((tabObj, i) => !!tabObj.list && (
|
||||
<Tabs.Tab
|
||||
key={i}
|
||||
selected={i === tabIndex}
|
||||
onClick={() => {
|
||||
setListIndex(0);
|
||||
setTabIndex(i);
|
||||
}}>
|
||||
{tabObj.name}
|
||||
</Tabs.Tab>
|
||||
))}
|
||||
</Tabs>
|
||||
</Section>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow={2}>
|
||||
<Section fill>
|
||||
<Stack
|
||||
@@ -103,23 +65,22 @@ export const NtosPortraitPrinter = (props, context) => {
|
||||
icon="check"
|
||||
content="Print Portrait"
|
||||
onClick={() => act("select", {
|
||||
tab: tabIndex+1,
|
||||
selected: listIndex+1,
|
||||
selected: paintings[listIndex]["ref"],
|
||||
})}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow={1}>
|
||||
<Button
|
||||
icon="chevron-right"
|
||||
disabled={listIndex === tab2list.length-1}
|
||||
disabled={listIndex === paintings.length-1}
|
||||
onClick={() => setListIndex(listIndex+1)}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Button
|
||||
icon="angle-double-right"
|
||||
disabled={listIndex === tab2list.length-1}
|
||||
onClick={() => setListIndex(tab2list.length-1)}
|
||||
disabled={listIndex === paintings.length-1}
|
||||
onClick={() => setListIndex(paintings.length-1)}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
|
||||
127
tgui/packages/tgui/interfaces/PaintingAdminPanel.tsx
Normal file
127
tgui/packages/tgui/interfaces/PaintingAdminPanel.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import { resolveAsset } from '../assets';
|
||||
import { useBackend, useLocalState } from '../backend';
|
||||
import { Box, Button, LabeledList, Section, Table } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
|
||||
type PaintingAdminPanelData = {
|
||||
paintings: PaintingData[];
|
||||
};
|
||||
|
||||
type PaintingData = {
|
||||
md5: string,
|
||||
title: string,
|
||||
creator_ckey: string,
|
||||
creator_name: string | null,
|
||||
creation_date: Date | null,
|
||||
creation_round_id: number | null,
|
||||
patron_ckey: string | null,
|
||||
patron_name: string | null,
|
||||
credit_value: number,
|
||||
width: number,
|
||||
height: number,
|
||||
ref: string,
|
||||
tags: string[] | null,
|
||||
medium: string | null
|
||||
}
|
||||
|
||||
export const PaintingAdminPanel = (props, context) => {
|
||||
const { act, data } = useBackend<PaintingAdminPanelData>(context);
|
||||
const [chosenPaintingRef, setChosenPaintingRef] = useLocalState<string|null>(context, 'chosenPainting', null);
|
||||
const {
|
||||
paintings,
|
||||
} = data;
|
||||
const chosenPainting = paintings.find(p => p.ref === chosenPaintingRef);
|
||||
return (
|
||||
<Window
|
||||
title="Painting Admin Panel"
|
||||
width={800}
|
||||
height={600}>
|
||||
<Window.Content scrollable>
|
||||
{chosenPainting && (
|
||||
<Section title="Painting Information" buttons={<Button onClick={() => setChosenPaintingRef(null)}>Close</Button>}>
|
||||
<img
|
||||
src={resolveAsset(`paintings_${chosenPainting.md5}`)}
|
||||
height="96px"
|
||||
width="96px"
|
||||
style={{
|
||||
'vertical-align': 'middle',
|
||||
'-ms-interpolation-mode': 'nearest-neighbor',
|
||||
}} />
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="md5" content={chosenPainting.md5} />
|
||||
<LabeledList.Item label="title">
|
||||
<Box inline>{chosenPainting.title}</Box>
|
||||
<Button onClick={() => act("rename", { ref: chosenPainting.ref })} icon="edit" />
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="creator ckey" content={chosenPainting.creator_ckey} />
|
||||
<LabeledList.Item label="creator name">
|
||||
<Box inline>{chosenPainting.creator_name}</Box>
|
||||
<Button onClick={() => act("rename_author", { ref: chosenPainting.ref })} icon="edit" />
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="creation date" content={chosenPainting.creation_date} />
|
||||
<LabeledList.Item label="creation round id" content={chosenPainting.creation_round_id} />
|
||||
<LabeledList.Item label="medium" content={chosenPainting.medium} />
|
||||
<LabeledList.Item label="tags">
|
||||
{chosenPainting.tags?.map((tag) => (
|
||||
<Button
|
||||
key={tag}
|
||||
color="red"
|
||||
icon="minus-circle"
|
||||
iconPosition="right" content={tag}
|
||||
onClick={() => act("remove_tag", { tag, ref: chosenPainting.ref })} />
|
||||
))}
|
||||
<Button
|
||||
color="green"
|
||||
icon="plus-circle"
|
||||
onClick={() => act("add_tag", { ref: chosenPainting.ref })} />
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="patron ckey" content={chosenPainting.patron_ckey} />
|
||||
<LabeledList.Item label="patron name" content={chosenPainting.patron_name} />
|
||||
<LabeledList.Item label="credit value" content={chosenPainting.credit_value} />
|
||||
<LabeledList.Item label="width" content={chosenPainting.width} />
|
||||
<LabeledList.Item label="height" content={chosenPainting.height} />
|
||||
</LabeledList>
|
||||
<Section title="Actions">
|
||||
<Button.Confirm onClick={() => { setChosenPaintingRef(null); act("delete", { ref: chosenPainting.ref }); }}>Delete</Button.Confirm>
|
||||
<Button onClick={() => act("dumpit", { ref: chosenPainting.ref })}>Reset Patronage</Button>
|
||||
</Section>
|
||||
</Section>
|
||||
)}
|
||||
{!chosenPainting && (
|
||||
<Table>
|
||||
<Table.Row>
|
||||
<Table.Cell color="label">Title</Table.Cell>
|
||||
<Table.Cell color="label">Author</Table.Cell>
|
||||
<Table.Cell color="label">Preview</Table.Cell>
|
||||
<Table.Cell color="label">Actions</Table.Cell>
|
||||
</Table.Row>
|
||||
{paintings.map(painting => (
|
||||
<Table.Row
|
||||
key={painting.ref}
|
||||
className="candystripe">
|
||||
<Table.Cell>{painting.title}</Table.Cell>
|
||||
<Table.Cell>{painting.creator_ckey}</Table.Cell>
|
||||
<Table.Cell><img
|
||||
src={resolveAsset(`paintings_${painting.md5}`)}
|
||||
height="36px"
|
||||
width="36px"
|
||||
style={{
|
||||
'vertical-align': 'middle',
|
||||
'-ms-interpolation-mode': 'nearest-neighbor',
|
||||
}} />
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Button onClick={() => setChosenPaintingRef(painting.ref)}>
|
||||
Edit
|
||||
</Button>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table>
|
||||
)}
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,37 +1,16 @@
|
||||
import { resolveAsset } from '../assets';
|
||||
import { useBackend, useLocalState } from '../backend';
|
||||
import { Button, Flex, NoticeBox, Section, Tabs } from '../components';
|
||||
import { Button, Flex, NoticeBox, Section } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
|
||||
export const PortraitPicker = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
const [tabIndex, setTabIndex] = useLocalState(context, 'tabIndex', 0);
|
||||
const [listIndex, setListIndex] = useLocalState(context, 'listIndex', 0);
|
||||
const {
|
||||
library,
|
||||
library_secure,
|
||||
library_private,
|
||||
paintings,
|
||||
} = data;
|
||||
const TABS = [
|
||||
{
|
||||
name: 'Common Portraits',
|
||||
asset_prefix: "library",
|
||||
list: library,
|
||||
},
|
||||
{
|
||||
name: 'Secure Portraits',
|
||||
asset_prefix: "library_secure",
|
||||
list: library_secure,
|
||||
},
|
||||
{
|
||||
name: 'Private Portraits',
|
||||
asset_prefix: "library_private",
|
||||
list: library_private,
|
||||
},
|
||||
];
|
||||
const tab2list = TABS[tabIndex].list;
|
||||
const current_portrait_title = tab2list[listIndex]["title"];
|
||||
const current_portrait_asset_name = TABS[tabIndex].asset_prefix + "_" + tab2list[listIndex]["md5"];
|
||||
const current_portrait_title = paintings[listIndex]["title"];
|
||||
const current_portrait_asset_name = "paintings" + "_" + paintings[listIndex]["md5"];
|
||||
return (
|
||||
<Window
|
||||
theme="ntos"
|
||||
@@ -40,23 +19,6 @@ export const PortraitPicker = (props, context) => {
|
||||
height={406}>
|
||||
<Window.Content>
|
||||
<Flex height="100%" direction="column">
|
||||
<Flex.Item mb={1}>
|
||||
<Section fitted>
|
||||
<Tabs fluid textAlign="center">
|
||||
{TABS.map((tabObj, i) => (
|
||||
<Tabs.Tab
|
||||
key={i}
|
||||
selected={i === tabIndex}
|
||||
onClick={() => {
|
||||
setListIndex(0);
|
||||
setTabIndex(i);
|
||||
}}>
|
||||
{tabObj.name}
|
||||
</Tabs.Tab>
|
||||
))}
|
||||
</Tabs>
|
||||
</Section>
|
||||
</Flex.Item>
|
||||
<Flex.Item mb={1} grow={2}>
|
||||
<Section fill>
|
||||
<Flex
|
||||
@@ -104,23 +66,22 @@ export const PortraitPicker = (props, context) => {
|
||||
icon="check"
|
||||
content="Select Portrait"
|
||||
onClick={() => act("select", {
|
||||
tab: tabIndex+1,
|
||||
selected: listIndex+1,
|
||||
selected: paintings[listIndex]["ref"],
|
||||
})}
|
||||
/>
|
||||
</Flex.Item>
|
||||
<Flex.Item grow={1}>
|
||||
<Button
|
||||
icon="chevron-right"
|
||||
disabled={listIndex === tab2list.length-1}
|
||||
disabled={listIndex === paintings.length-1}
|
||||
onClick={() => setListIndex(listIndex+1)}
|
||||
/>
|
||||
</Flex.Item>
|
||||
<Flex.Item>
|
||||
<Button
|
||||
icon="angle-double-right"
|
||||
disabled={listIndex === tab2list.length-1}
|
||||
onClick={() => setListIndex(tab2list.length-1)}
|
||||
disabled={listIndex === paintings.length-1}
|
||||
onClick={() => setListIndex(paintings.length-1)}
|
||||
/>
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
|
||||
Reference in New Issue
Block a user