mirror of
https://github.com/CHOMPStation2/CHOMPStation2.git
synced 2026-08-28 15:48:49 +01:00
[MIRROR] spawn verb port [No GBP] (#12618)
Co-authored-by: Kashargul <144968721+Kashargul@users.noreply.github.com>
This commit is contained in:
co-authored by
Kashargul
parent
060ae4e7fa
commit
14ff5d9ebc
@@ -0,0 +1,70 @@
|
||||
/// Returns a list of all abstract typepaths for all datums
|
||||
/proc/get_abstract_types()
|
||||
var/static/list/abstracts
|
||||
if(abstracts)
|
||||
return abstracts
|
||||
abstracts = list()
|
||||
for(var/datum/sometype as anything in subtypesof(/datum))
|
||||
if(sometype == sometype::abstract_type)
|
||||
abstracts[sometype::abstract_type] = TRUE
|
||||
|
||||
return abstracts
|
||||
|
||||
// The time complexity of these two procs is really bad, sitting at O(n * m).
|
||||
// And yet, the overhead is ridiculously small for some reason. We never figured out why.
|
||||
// So doing list subtraction instead of filtering is faster in all cases, at least for now.
|
||||
// If we ever breach like 3000-5000 abstract types, that will likely change.
|
||||
// As of right now we're at 272 or so, with subtraction being ~10x faster.
|
||||
// Curse this language, for you shall live in hell alongside me.
|
||||
|
||||
/// Like subtypesof, but automatically excludes abstract typepaths
|
||||
/proc/valid_subtypesof(datum/sometype)
|
||||
return subtypesof(sometype) - get_abstract_types()
|
||||
|
||||
/// Like typesof, but automatically excludes abstract typepaths
|
||||
/proc/valid_typesof(datum/sometype)
|
||||
return typesof(sometype) - get_abstract_types()
|
||||
|
||||
/// Returns a list of concrete types under abstract sub-branches of `root`
|
||||
/proc/get_abstract_branch_descendants(datum/root)
|
||||
var/list/abstracts = get_abstract_types()
|
||||
var/list/to_remove = list()
|
||||
var/list/seen_abstract_parents = list()
|
||||
|
||||
for (var/datum/sometype as anything in subtypesof(root))
|
||||
if (abstracts[sometype])
|
||||
continue
|
||||
|
||||
var/datum/parenttype = sometype.parent_type
|
||||
while (parenttype && parenttype != root)
|
||||
if (seen_abstract_parents[parenttype] || (abstracts[parenttype]))
|
||||
seen_abstract_parents[parenttype] = TRUE
|
||||
to_remove += sometype
|
||||
break
|
||||
parenttype = parenttype.parent_type
|
||||
|
||||
return to_remove
|
||||
|
||||
/// Like valid_subtypesof(), but excludes concrete descendants of abstract sub-branches
|
||||
/proc/valid_direct_subtypesof(datum/root)
|
||||
var/list/result = subtypesof(root)
|
||||
|
||||
// Remove all abstract types
|
||||
result -= get_abstract_types()
|
||||
|
||||
// Remove concrete types under abstract sub-branches
|
||||
result -= get_abstract_branch_descendants(root)
|
||||
|
||||
return result
|
||||
|
||||
/// Like valid_typesof(), but excludes concrete descendants of abstract sub-branches
|
||||
/proc/valid_direct_typesof(datum/root)
|
||||
var/list/result = typesof(root)
|
||||
|
||||
// Remove all abstract types
|
||||
result -= get_abstract_types()
|
||||
|
||||
// Remove concrete types under abstract sub-branches
|
||||
result -= get_abstract_branch_descendants(root)
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,86 @@
|
||||
// Longer paths should come after shorter ones
|
||||
GLOBAL_LIST_INIT(fancy_type_replacements, list(
|
||||
/datum = "DATUM",
|
||||
/area = "AREA",
|
||||
/atom/movable = "MOVABLE",
|
||||
/obj = "OBJ",
|
||||
/turf = "TURF",
|
||||
/turf/simulated = "SIMULATED",
|
||||
/turf/simulated/open = "OPEN",
|
||||
|
||||
/mob = "MOB",
|
||||
/mob/living = "LIVING",
|
||||
/mob/living/carbon = "CARBON",
|
||||
/mob/living/carbon/human = "HUMANOID",
|
||||
/mob/living/simple_mob = "SIMPLE",
|
||||
/mob/living/silicon = "SILICON",
|
||||
/mob/living/silicon/robot = "CYBORG",
|
||||
/mob/living/silicon/robot/drone = "DRONE",
|
||||
/mob/living/silicon/pai = "PAI",
|
||||
/mob/living/silicon/ai = "AI",
|
||||
|
||||
/obj/item = "ITEM",
|
||||
/obj/item/mecha_parts/mecha_equipment = "MECHA_EQUIP",
|
||||
/obj/item/mecha_parts/mecha_equipment/weapon = "MECHA_WEAPON",
|
||||
/obj/item/organ = "ORGAN",
|
||||
/obj/item/rig = "RIGSUIT",
|
||||
/obj/item/rig_module = "RIGSUIT_MOD",
|
||||
/obj/item/ammo_magazine = "AMMO",
|
||||
/obj/item/ammo_magazine/ammo_box = "AMMOBOX",
|
||||
/obj/item/gun = "GUN",
|
||||
/obj/item/gun/magic = "GUN_MAGIC",
|
||||
/obj/item/gun/energy = "GUN_ENERGY",
|
||||
/obj/item/gun/energy/laser = "GUN_LASER",
|
||||
/obj/item/gun/projectile = "GUN_BALLISTIC",
|
||||
/obj/item/gun/projectile/automatic = "GUN_AUTOMATIC",
|
||||
/obj/item/gun/projectile/revolver = "GUN_REVOLVER",
|
||||
/obj/item/gun/projectile/heavysniper = "GUN_RIFLE",
|
||||
/obj/item/gun/projectile/shotgun = "GUN_SHOTGUN",
|
||||
/obj/item/stack/material = "SHEET",
|
||||
/obj/item/ore = "ORE",
|
||||
/obj/item/aiModule = "AI_LAW_MODULE",
|
||||
/obj/item/circuitboard = "CIRCUITBOARD",
|
||||
/obj/item/circuitboard/machine = "MACHINE_BOARD",
|
||||
/obj/item/reagent_containers = "REAGENT_CONTAINERS",
|
||||
/obj/item/reagent_containers/pill = "PILL",
|
||||
/obj/item/reagent_containers/hypospray/autoinjector = "MEDIPEN",
|
||||
/obj/item/reagent_containers/food/drinks = "DRINK",
|
||||
/obj/item/reagent_containers/food/snacks = "FOOD",
|
||||
/obj/item/organ/external = "BODYPART",
|
||||
/obj/effect/decal/cleanable = "CLEANABLE",
|
||||
/obj/item/radio/headset = "HEADSET",
|
||||
/obj/item/clothing = "CLOTHING",
|
||||
/obj/item/clothing/accessory = "ACCESSORY",
|
||||
/obj/item/clothing/mask/gas = "GASMASK",
|
||||
/obj/item/clothing/mask = "MASK",
|
||||
/obj/item/clothing/gloves = "GLOVES",
|
||||
/obj/item/clothing/shoes = "SHOES",
|
||||
/obj/item/clothing/under = "JUMPSUIT",
|
||||
/obj/item/clothing/suit/armor = "ARMOR",
|
||||
/obj/item/clothing/suit = "SUIT",
|
||||
/obj/item/clothing/head/helmet = "HELMET",
|
||||
/obj/item/clothing/head = "HEAD",
|
||||
/obj/item/storage/backpack = "BACKPACK",
|
||||
/obj/item/storage/belt = "BELT",
|
||||
/obj/item/storage/pill_bottle = "PILL_BOTTLE",
|
||||
/obj/item/book/manual = "MANUAL",
|
||||
|
||||
/obj/structure = "STRUCTURE",
|
||||
/obj/structure/closet = "CLOSET",
|
||||
/obj/structure/closet/crate = "CRATE",
|
||||
/obj/structure/closet/crate/secure = "LOCKED_CRATE",
|
||||
/obj/structure/closet/secure_closet = "LOCKED_CLOSET",
|
||||
|
||||
/obj/machinery = "MACHINERY",
|
||||
/obj/machinery/atmospherics = "ATMOS_MECH",
|
||||
/obj/machinery/portable_atmospherics = "PORT_ATMOS",
|
||||
/obj/machinery/door = "DOOR",
|
||||
/obj/machinery/door/airlock = "AIRLOCK",
|
||||
/obj/machinery/rnd/production = "RND_FABRICATOR",
|
||||
/obj/machinery/computer = "COMPUTER",
|
||||
/obj/machinery/embedded_controller/radio/docking_port_multi = "DOCKING_CONTROLLER",
|
||||
/obj/machinery/vending = "VENDING",
|
||||
/obj/machinery/vending/wardrobe = "JOBDROBE",
|
||||
/obj/effect = "EFFECT",
|
||||
/obj/item/projectile = "PROJECTILE",
|
||||
))
|
||||
+29
-20
@@ -1024,33 +1024,42 @@ ADMIN_VERB(spawn_plant, R_SPAWN, "Spawn Plant", "Spawn a spreading plant effect.
|
||||
new /obj/effect/plant(get_turf(user_mob), SSplants.seeds[seedtype])
|
||||
log_admin("[key_name(user)] spawned [seedtype] vines at ([user_mob.x],[user_mob.y],[user_mob.z])")
|
||||
|
||||
ADMIN_VERB(spawn_atom, R_SPAWN, "Spawn", "(atom path) Spawn an atom", ADMIN_CATEGORY_DEBUG_GAME, object as text)
|
||||
var/list/types = typesof(/atom)
|
||||
var/list/matches = new()
|
||||
ADMIN_VERB(spawn_atom, R_SPAWN, "Spawn", "(atom path) Spawn an atom", ADMIN_CATEGORY_DEBUG_GAME, object as text|null)
|
||||
var/static/list/atom_types
|
||||
if(isnull(atom_types))
|
||||
atom_types = subtypesof(/atom)
|
||||
|
||||
for(var/path in types)
|
||||
if(findtext("[path]", object))
|
||||
matches += path
|
||||
var/chosen_path = null
|
||||
var/list/preparsed = null
|
||||
if(object)
|
||||
preparsed = splittext(object, ":")
|
||||
var/list/matches = filter_fancy_list(atom_types, preparsed[1])
|
||||
if(length(matches) == 1)
|
||||
chosen_path = matches[1]
|
||||
|
||||
if(matches.len==0)
|
||||
if(!chosen_path)
|
||||
var/datum/spawn_menu/menu = user.holder.spawn_menu
|
||||
if(!menu)
|
||||
menu = new()
|
||||
user.holder.spawn_menu = menu
|
||||
menu.init_value = object
|
||||
menu.tgui_interact(user.mob)
|
||||
feedback_add_details("admin_verb","SA")
|
||||
return
|
||||
|
||||
var/chosen
|
||||
if(matches.len==1)
|
||||
chosen = matches[1]
|
||||
else
|
||||
chosen = tgui_input_list(user, "Select an atom type", "Spawn Atom", matches)
|
||||
if(!chosen)
|
||||
return
|
||||
var/amount = 1
|
||||
if(length(preparsed) > 1)
|
||||
amount = clamp(text2num(preparsed[2]), 1, ADMIN_SPAWN_CAP)
|
||||
|
||||
var/mob/user_mob = user.mob
|
||||
if(ispath(chosen,/turf))
|
||||
var/turf/T = get_turf(user_mob.loc)
|
||||
T.ChangeTurf(chosen)
|
||||
var/turf/target_turf = get_turf(user.mob)
|
||||
if(ispath(chosen_path, /turf))
|
||||
target_turf.ChangeTurf(chosen_path)
|
||||
else
|
||||
new chosen(user_mob.loc)
|
||||
for(var/i in 1 to amount)
|
||||
var/atom/spawned = new chosen_path(target_turf)
|
||||
spawned.flags |= ADMIN_SPAWNED
|
||||
|
||||
log_and_message_admins("spawned [chosen] at ([user_mob.x],[user_mob.y],[user_mob.z])")
|
||||
log_and_message_admins("spawned [amount] x [chosen_path] at [AREACOORD(user.mob)]", user)
|
||||
feedback_add_details("admin_verb","SA") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
ADMIN_VERB(show_traitor_panel, R_ADMIN|R_FUN|R_EVENT, "Show Traitor Panel", "Edit mobs's memory and role", ADMIN_CATEGORY_EVENTS, mob/M in GLOB.mob_list)
|
||||
|
||||
@@ -32,6 +32,7 @@ GLOBAL_PROTECT(href_token)
|
||||
var/datum/filter_editor/filteriffic
|
||||
var/datum/particle_editor/particle_test
|
||||
var/datum/whitelist_editor/whitelist_editor
|
||||
var/datum/spawn_menu/spawn_menu
|
||||
var/datum/spawnpanel/spawn_panel
|
||||
|
||||
/// A lazylist of tagged datums, for quick reference with the View Tags verb
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/datum/spawn_menu
|
||||
/// Does the menu default to a regex prefix?
|
||||
var/regex_search = FALSE
|
||||
/// Does the search include atom names?
|
||||
var/name_search = TRUE
|
||||
/// Should we display full typepaths or the condensed versions?
|
||||
var/fancy_types = TRUE
|
||||
/// Should abstract types be included in the search?
|
||||
var/include_abstracts = FALSE
|
||||
/// Initial search value from the latest command
|
||||
var/init_value = null
|
||||
|
||||
/datum/spawn_menu/tgui_interact(mob/user, datum/tgui/ui)
|
||||
ui = SStgui.try_update_ui(user, src, ui)
|
||||
if (!ui)
|
||||
ui = new(user, src, "SpawnSearch")
|
||||
ui.open()
|
||||
|
||||
/datum/spawn_menu/tgui_state(mob/user)
|
||||
return ADMIN_STATE(R_SPAWN)
|
||||
|
||||
/datum/spawn_menu/tgui_act(action, params, datum/tgui/ui)
|
||||
if (..() || !check_rights_for(ui.user.client, R_SPAWN))
|
||||
return FALSE
|
||||
|
||||
switch (action)
|
||||
if ("setRegexSearch")
|
||||
regex_search = params["regexSearch"]
|
||||
return TRUE
|
||||
|
||||
if ("setNameSearch")
|
||||
name_search = params["searchNames"]
|
||||
return TRUE
|
||||
|
||||
if ("setFancyTypes")
|
||||
fancy_types = params["fancyTypes"]
|
||||
return TRUE
|
||||
|
||||
if ("setIncludeAbstracts")
|
||||
include_abstracts = params["includeAbstracts"]
|
||||
return TRUE
|
||||
|
||||
if ("spawn")
|
||||
var/path = text2path(params["type"])
|
||||
if (!path)
|
||||
return TRUE
|
||||
var/amount = clamp(text2num(params["amount"]) || 1, 1, ADMIN_SPAWN_CAP)
|
||||
var/turf/target_turf = get_turf(ui.user)
|
||||
if(ispath(path, /turf))
|
||||
target_turf.ChangeTurf(path)
|
||||
else
|
||||
for(var/i in 1 to amount)
|
||||
var/atom/spawned = new path(target_turf)
|
||||
spawned.flags |= ADMIN_SPAWNED
|
||||
|
||||
log_admin("[key_name(ui.user)] spawned [amount] x [path] at [AREACOORD(ui.user)]")
|
||||
SStgui.close_uis(src)
|
||||
return TRUE
|
||||
|
||||
if ("cancel")
|
||||
SStgui.close_uis(src)
|
||||
return TRUE
|
||||
|
||||
/datum/spawn_menu/tgui_data(mob/user)
|
||||
var/list/data = list()
|
||||
data["initValue"] = init_value
|
||||
data["searchNames"] = name_search
|
||||
data["regexSearch"] = regex_search
|
||||
data["fancyTypes"] = fancy_types
|
||||
data["includeAbstracts"] = include_abstracts
|
||||
return data
|
||||
|
||||
/datum/spawn_menu/ui_assets(mob/user)
|
||||
return list(
|
||||
get_asset_datum(/datum/asset/json/spawn_menu),
|
||||
)
|
||||
|
||||
/datum/asset/json/spawn_menu
|
||||
name = "spawn_menu_atom_data"
|
||||
|
||||
/datum/asset/json/spawn_menu/generate()
|
||||
var/list/data = list()
|
||||
var/static/list/types_list
|
||||
if (isnull(types_list))
|
||||
var/list/local_types = list()
|
||||
for (var/atom/atom_type as anything in subtypesof(/atom))
|
||||
local_types[atom_type] = atom_type::name || ""
|
||||
types_list = local_types
|
||||
data["types"] = types_list
|
||||
data["abstractTypes"] = get_abstract_types()
|
||||
data["fancyTypes"] = GLOB.fancy_type_replacements
|
||||
return data
|
||||
@@ -0,0 +1,413 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Autofocus,
|
||||
Box,
|
||||
Button,
|
||||
Icon,
|
||||
Input,
|
||||
Section,
|
||||
Stack,
|
||||
VirtualList,
|
||||
} from 'tgui-core/components';
|
||||
import { fetchRetry } from 'tgui-core/http';
|
||||
import {
|
||||
KEY_DOWN,
|
||||
KEY_ENTER,
|
||||
KEY_ESCAPE,
|
||||
KEY_F,
|
||||
KEY_N,
|
||||
KEY_R,
|
||||
KEY_UP,
|
||||
} from 'tgui-core/keycodes';
|
||||
import type { BooleanLike } from 'tgui-core/react';
|
||||
import { resolveAsset } from '../assets';
|
||||
import { useBackend } from '../backend';
|
||||
import { Window } from '../layouts';
|
||||
import { logger } from '../logging';
|
||||
|
||||
type SpawnSearchData = {
|
||||
initValue: string | null;
|
||||
searchNames: BooleanLike;
|
||||
regexSearch: BooleanLike;
|
||||
fancyTypes: BooleanLike;
|
||||
includeAbstracts: BooleanLike;
|
||||
};
|
||||
|
||||
type SpawnAtomData = {
|
||||
// Type -> Name
|
||||
types: Record<string, string>;
|
||||
abstractTypes: Record<string, boolean>;
|
||||
fancyTypes: Record<string, string>;
|
||||
};
|
||||
|
||||
type AtomPathData = {
|
||||
types: AtomTypeData[];
|
||||
abstractTypes: Record<string, boolean>;
|
||||
fancyTypes: Record<string, string>;
|
||||
};
|
||||
|
||||
type AtomTypeData = {
|
||||
typepath: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
const initialAtomPathData: AtomPathData = {
|
||||
types: [],
|
||||
abstractTypes: {},
|
||||
fancyTypes: {},
|
||||
};
|
||||
|
||||
export function SpawnSearch() {
|
||||
const { act, data } = useBackend<SpawnSearchData>();
|
||||
const {
|
||||
fancyTypes,
|
||||
includeAbstracts,
|
||||
initValue = '',
|
||||
regexSearch,
|
||||
searchNames,
|
||||
} = data;
|
||||
|
||||
const [atomData, setAtomData] = useState(initialAtomPathData);
|
||||
const [selected, setSelected] = useState(0);
|
||||
const [query, setQuery] = useState(initValue || '');
|
||||
const [searchBarVisible, setSearchBarVisible] = useState(true);
|
||||
|
||||
const { invalidInput, spawnAmount } = useMemo(() => {
|
||||
let invalidInput = false;
|
||||
let spawnAmount = 1;
|
||||
|
||||
const possibleAmountData = query.split(':');
|
||||
const amountElement = possibleAmountData[possibleAmountData.length - 1];
|
||||
|
||||
if (possibleAmountData.length > 1 && !Number.isNaN(+amountElement)) {
|
||||
if (+amountElement <= 0) {
|
||||
invalidInput = true;
|
||||
} else {
|
||||
spawnAmount = +amountElement;
|
||||
}
|
||||
}
|
||||
|
||||
if (regexSearch) {
|
||||
try {
|
||||
new RegExp(query);
|
||||
} catch (error) {
|
||||
invalidInput = true;
|
||||
}
|
||||
}
|
||||
|
||||
return { invalidInput, spawnAmount };
|
||||
}, [query, regexSearch]);
|
||||
|
||||
const filteredItems = useMemo(() => {
|
||||
let filterQuery = query;
|
||||
|
||||
// Extract amount suffix (e.g., ":5" from "query:5")
|
||||
const amountMatch = query.match(/^(.+):(\d+)$/);
|
||||
if (amountMatch) {
|
||||
const amount = +amountMatch[2];
|
||||
if (amount <= 0) {
|
||||
return [];
|
||||
}
|
||||
filterQuery = amountMatch[1].trimEnd();
|
||||
}
|
||||
|
||||
if (filterQuery.length === 0) return [];
|
||||
|
||||
if (regexSearch) {
|
||||
try {
|
||||
const queryRegex = new RegExp(filterQuery);
|
||||
return atomData.types.filter(
|
||||
(type: AtomTypeData) =>
|
||||
queryRegex.test(type.typepath) ||
|
||||
(searchNames && queryRegex.test(type.name)),
|
||||
);
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const finalizer = filterQuery.slice(filterQuery.length - 1);
|
||||
if (finalizer === '*' || finalizer === '!')
|
||||
filterQuery = filterQuery.slice(0, filterQuery.length - 1);
|
||||
filterQuery = filterQuery.toLowerCase();
|
||||
|
||||
let searchLambda = (x: string) => x.toLowerCase().includes(filterQuery);
|
||||
if (finalizer === '!') {
|
||||
searchLambda = (x: string) =>
|
||||
x.toLowerCase().includes(filterQuery) &&
|
||||
x.toLowerCase().lastIndexOf(filterQuery) ===
|
||||
x.length - filterQuery.length;
|
||||
} else if (finalizer === '*') {
|
||||
searchLambda = (x: string) =>
|
||||
x.toLowerCase().includes(filterQuery) &&
|
||||
!x.slice(x.toLowerCase().lastIndexOf(filterQuery)).includes('/');
|
||||
}
|
||||
|
||||
return atomData.types.filter(
|
||||
(type: AtomTypeData) =>
|
||||
(searchLambda(type.typepath) ||
|
||||
(searchNames && searchLambda(type.name))) &&
|
||||
(includeAbstracts || !atomData.abstractTypes[type.typepath]),
|
||||
);
|
||||
}, [query, atomData, regexSearch, includeAbstracts, searchNames]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchRetry(resolveAsset('spawn_menu_atom_data.json'))
|
||||
.then((response) => response.json())
|
||||
.then((data: SpawnAtomData) => {
|
||||
setAtomData({
|
||||
types: Object.keys(data.types).map((x: string) => ({
|
||||
typepath: x,
|
||||
name: data.types[x],
|
||||
})),
|
||||
abstractTypes: data.abstractTypes,
|
||||
fancyTypes: data.fancyTypes,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.log(
|
||||
'Failed to fetch spawn_menu_atom_data.json',
|
||||
JSON.stringify(error),
|
||||
);
|
||||
});
|
||||
}, []);
|
||||
|
||||
// User presses up or down on keyboard
|
||||
// Simulates clicking an item
|
||||
function handleArrowKey(key: number): void {
|
||||
if (!filteredItems.length) return;
|
||||
|
||||
const len = filteredItems.length - 1;
|
||||
|
||||
if (key === KEY_DOWN) {
|
||||
const next = selected >= len ? 0 : selected + 1;
|
||||
setSelected(next);
|
||||
document?.getElementById(next.toString())?.scrollIntoView();
|
||||
} else if (key === KEY_UP) {
|
||||
const prev = selected <= 0 ? len : selected - 1;
|
||||
setSelected(prev);
|
||||
document?.getElementById(prev.toString())?.scrollIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>): void {
|
||||
const keyCode = window.event ? event.which : event.keyCode;
|
||||
if (keyCode === KEY_DOWN || keyCode === KEY_UP) {
|
||||
event.preventDefault();
|
||||
handleArrowKey(keyCode);
|
||||
}
|
||||
|
||||
if (keyCode === KEY_ENTER) {
|
||||
event.preventDefault();
|
||||
handleSelect(filteredItems[selected]);
|
||||
}
|
||||
|
||||
if (keyCode === KEY_ESCAPE) {
|
||||
event.preventDefault();
|
||||
act('cancel');
|
||||
}
|
||||
|
||||
if (keyCode === KEY_R && event.altKey) {
|
||||
act('setRegexSearch', { regexSearch: !regexSearch });
|
||||
}
|
||||
|
||||
if (keyCode === KEY_N && event.altKey) {
|
||||
act('setNameSearch', { searchNames: !searchNames });
|
||||
}
|
||||
|
||||
if (keyCode === KEY_F && event.altKey) {
|
||||
act('setFancyTypes', { fancyTypes: !fancyTypes });
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelect(selection?: AtomTypeData): void {
|
||||
if (!selection) return;
|
||||
|
||||
act('spawn', { type: selection.typepath, amount: spawnAmount });
|
||||
}
|
||||
|
||||
function handleSearch(newQuery: string): void {
|
||||
if (newQuery === query) return;
|
||||
|
||||
setQuery(newQuery);
|
||||
setSelected(0);
|
||||
document!.getElementById('0')?.scrollIntoView();
|
||||
}
|
||||
|
||||
// Grabs the cursor when no search bar is visible.
|
||||
if (!searchBarVisible) {
|
||||
setTimeout(() => document!.getElementById(selected.toString())?.focus(), 1);
|
||||
}
|
||||
|
||||
const modeText = regexSearch ? 'RegEx Mode' : 'Standard Mode';
|
||||
|
||||
return (
|
||||
<Window
|
||||
title="Spawn Atom"
|
||||
width={400}
|
||||
height={500}
|
||||
buttons={
|
||||
<>
|
||||
<Button
|
||||
icon="font"
|
||||
selected={includeAbstracts}
|
||||
tooltip="Include Abstract Types"
|
||||
onClick={() =>
|
||||
act('setIncludeAbstracts', {
|
||||
includeAbstracts: !includeAbstracts,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
icon="file-signature"
|
||||
selected={searchNames}
|
||||
tooltip="Name Search"
|
||||
onClick={() => act('setNameSearch', { searchNames: !searchNames })}
|
||||
/>
|
||||
<Button
|
||||
icon="wand-magic-sparkles"
|
||||
selected={fancyTypes}
|
||||
tooltip="Fancy Type Display"
|
||||
onClick={() => act('setFancyTypes', { fancyTypes: !fancyTypes })}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Window.Content onKeyDown={handleKeyDown}>
|
||||
<Stack fill vertical>
|
||||
<Stack.Item grow>
|
||||
<Section fill scrollable>
|
||||
<Autofocus />
|
||||
<VirtualList>
|
||||
{filteredItems.map((item, index) => (
|
||||
<Button
|
||||
className="candystripe"
|
||||
color="transparent"
|
||||
fluid
|
||||
id={index.toString()}
|
||||
key={index}
|
||||
onClick={() => {
|
||||
if (index !== selected) setSelected(index);
|
||||
}}
|
||||
onDoubleClick={() => handleSelect(item)}
|
||||
onKeyDown={(event) => {
|
||||
if (/^[a-z]$/i.test(event.key)) {
|
||||
event.preventDefault();
|
||||
setSearchBarVisible(false);
|
||||
setTimeout(() => {
|
||||
setSearchBarVisible(true);
|
||||
}, 1);
|
||||
}
|
||||
}}
|
||||
selected={index === selected}
|
||||
style={{
|
||||
animation: 'none',
|
||||
transition: 'none',
|
||||
}}
|
||||
>
|
||||
<ListItem atomData={atomData} item={item} />
|
||||
</Button>
|
||||
))}
|
||||
</VirtualList>
|
||||
</Section>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
{!!searchBarVisible && (
|
||||
<Stack fill align="center" g={0.5}>
|
||||
<Stack.Item width={2}>
|
||||
<Button
|
||||
color="transparent"
|
||||
tooltip={modeText}
|
||||
onClick={() => {
|
||||
act('setRegexSearch', { regexSearch: !regexSearch });
|
||||
}}
|
||||
>
|
||||
{regexSearch ? (
|
||||
<Box as="span" color="good">
|
||||
re:
|
||||
</Box>
|
||||
) : (
|
||||
<Icon name="search" />
|
||||
)}
|
||||
</Button>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<Input
|
||||
autoFocus
|
||||
autoSelect
|
||||
expensive
|
||||
fluid
|
||||
onEnter={() => handleSelect(filteredItems[selected])}
|
||||
onChange={handleSearch}
|
||||
placeholder="Search..."
|
||||
value={query}
|
||||
style={{
|
||||
borderColor: invalidInput ? 'red' : undefined,
|
||||
}}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
}
|
||||
|
||||
type AtomSpanProps = {
|
||||
atomData: AtomPathData;
|
||||
item: AtomTypeData;
|
||||
};
|
||||
|
||||
function ListItem(props: AtomSpanProps) {
|
||||
const { atomData, item } = props;
|
||||
|
||||
const { data } = useBackend<SpawnSearchData>();
|
||||
const { fancyTypes } = data;
|
||||
|
||||
const matchingKey = fancyTypes
|
||||
? Object.keys(atomData.fancyTypes).findLast(
|
||||
(x: string) => item.typepath.indexOf(x) === 0,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const displayPath = matchingKey
|
||||
? item.typepath.replace(matchingKey, atomData.fancyTypes[matchingKey])
|
||||
: item.typepath;
|
||||
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
style={
|
||||
atomData.abstractTypes[item.typepath]
|
||||
? { opacity: 0.75, color: '#FFA246' }
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{displayPath}
|
||||
</span>
|
||||
<span
|
||||
className="label label-info"
|
||||
style={{
|
||||
marginLeft: '0.5em',
|
||||
color: 'rgba(200, 200, 200, 0.5)',
|
||||
fontSize: '10px',
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</span>
|
||||
{!!atomData.abstractTypes[item.typepath] && (
|
||||
<span
|
||||
style={{
|
||||
float: 'right',
|
||||
marginRight: '0.5em',
|
||||
color: 'rgba(255, 162, 70, 0.5)',
|
||||
}}
|
||||
>
|
||||
Abstract
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -389,6 +389,7 @@
|
||||
#include "code\_global_vars\traits\_traits.dm"
|
||||
#include "code\_helpers\_global_objects.dm"
|
||||
#include "code\_helpers\_lists.dm"
|
||||
#include "code\_helpers\abstract_types.dm"
|
||||
#include "code\_helpers\admin.dm"
|
||||
#include "code\_helpers\announcements.dm"
|
||||
#include "code\_helpers\atmospherics.dm"
|
||||
@@ -432,6 +433,7 @@
|
||||
#include "code\_helpers\traits.dm"
|
||||
#include "code\_helpers\turfs.dm"
|
||||
#include "code\_helpers\type2type.dm"
|
||||
#include "code\_helpers\type_processing.dm"
|
||||
#include "code\_helpers\typelists.dm"
|
||||
#include "code\_helpers\unsorted.dm"
|
||||
#include "code\_helpers\unsorted_vr.dm"
|
||||
@@ -2285,6 +2287,7 @@
|
||||
#include "code\modules\admin\player_notes.dm"
|
||||
#include "code\modules\admin\player_panel.dm"
|
||||
#include "code\modules\admin\random_quest.dm"
|
||||
#include "code\modules\admin\spawn_menu.dm"
|
||||
#include "code\modules\admin\tag.dm"
|
||||
#include "code\modules\admin\topic.dm"
|
||||
#include "code\modules\admin\ToRban.dm"
|
||||
|
||||
Reference in New Issue
Block a user