diff --git a/code/__DEFINES/ai/trader.dm b/code/__DEFINES/ai/trader.dm new file mode 100644 index 00000000000..853dd8736b6 --- /dev/null +++ b/code/__DEFINES/ai/trader.dm @@ -0,0 +1,6 @@ +///The ability to setup our "shop" +#define BB_SETUP_SHOP "BB_setup_shop" +///Reference to the plastic chair that is considered as our shop +#define BB_SHOP_SPOT "BB_shop_spot" +///Reference to our first customer to harass with deals +#define BB_FIRST_CUSTOMER "BB_first_customer" diff --git a/code/__DEFINES/trader.dm b/code/__DEFINES/trader.dm new file mode 100644 index 00000000000..aae6da26499 --- /dev/null +++ b/code/__DEFINES/trader.dm @@ -0,0 +1,16 @@ +#define ITEM_REJECTED_PHRASE "ITEM_REJECTED_PHRASE" +#define ITEM_SELLING_CANCELED_PHRASE "ITEM_SELLING_CANCELED_PHRASE" +#define ITEM_SELLING_ACCEPTED_PHRASE "ITEM_SELLING_ACCEPTED_PHRASE" +#define INTERESTED_PHRASE "INTERESTED_PHRASE" +#define BUY_PHRASE "BUY_PHRASE" +#define NO_CASH_PHRASE "NO_CASH_PHRASE" +#define NO_STOCK_PHRASE "NO_STOCK_PHRASE" +#define NOT_WILLING_TO_BUY_PHRASE "NOT_WILLING_TO_BUY_PHRASE" +#define ITEM_IS_WORTHLESS_PHRASE "ITEM_IS_WORTHLESS_PHRASE" +#define TRADER_HAS_ENOUGH_ITEM_PHRASE "TRADER_HAS_ENOUGH_ITEM_PHRASE" +#define TRADER_LORE_PHRASE "TRADER_LORE_PHRASE" +#define TRADER_BATTLE_START_PHRASE "TRADER_AGGRO_PHRASE" +#define TRADER_BATTLE_END_PHRASE "TRADER_DEAGGRO_PHRASE" +#define TRADER_NOT_BUYING_ANYTHING "TRADER_NOT_BUYING_ANYTHING" +#define TRADER_NOT_SELLING_ANYTHING "TRADER_NOT_SELLING_ANYTHING" +#define TRADER_SHOP_OPENING_PHRASE "TRADER_SHOP_OPENING_PHRASE" diff --git a/code/_onclick/hud/radial.dm b/code/_onclick/hud/radial.dm index 5bc75d85f47..c7a4cb9ab0a 100644 --- a/code/_onclick/hud/radial.dm +++ b/code/_onclick/hud/radial.dm @@ -345,9 +345,13 @@ GLOBAL_LIST_EMPTY(radial_menus) Choices should be a list where list keys are movables or text used for element names and return value and list values are movables/icons/images used for element icons */ -/proc/show_radial_menu(mob/user, atom/anchor, list/choices, uniqueid, radius, datum/callback/custom_check, require_near = FALSE, tooltips = FALSE, no_repeat_close = FALSE, radial_slice_icon = "radial_slice") +/proc/show_radial_menu(mob/user, atom/anchor, list/choices, uniqueid, radius, datum/callback/custom_check, require_near = FALSE, tooltips = FALSE, no_repeat_close = FALSE, radial_slice_icon = "radial_slice", autopick_single_option = TRUE) if(!user || !anchor || !length(choices)) return + + if(length(choices)==1 && autopick_single_option) + return choices[1] + if(!uniqueid) uniqueid = "defmenu_[REF(user)]_[REF(anchor)]" diff --git a/code/datums/ai/_ai_controller.dm b/code/datums/ai/_ai_controller.dm index f9938e08c88..04493b44e5d 100644 --- a/code/datums/ai/_ai_controller.dm +++ b/code/datums/ai/_ai_controller.dm @@ -619,6 +619,7 @@ multiple modular subtrees with behaviors // We found the value that's been deleted, it was an assoc value. Clear it out entirely else if(associated_value == source) next_to_clear -= inner_value + SEND_SIGNAL(pawn, COMSIG_AI_BLACKBOARD_KEY_CLEARED(inner_value)) index += 1 diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/set_travel_destination.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/set_travel_destination.dm new file mode 100644 index 00000000000..207df442457 --- /dev/null +++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/set_travel_destination.dm @@ -0,0 +1,13 @@ +/datum/ai_behavior/set_travel_destination + +/datum/ai_behavior/set_travel_destination/perform(seconds_per_tick, datum/ai_controller/controller, target_key, location_key) + . = ..() + var/atom/target = controller.blackboard[target_key] + + if(QDELETED(target)) + finish_action(controller, FALSE, target_key) + return + + controller.set_blackboard_key(location_key, target) + + finish_action(controller, TRUE, target_key) diff --git a/code/datums/ai/basic_mobs/basic_subtrees/prepare_travel_to_destination.dm b/code/datums/ai/basic_mobs/basic_subtrees/prepare_travel_to_destination.dm new file mode 100644 index 00000000000..2718ca630ff --- /dev/null +++ b/code/datums/ai/basic_mobs/basic_subtrees/prepare_travel_to_destination.dm @@ -0,0 +1,23 @@ + +///Subtree that checks if we are on the target atom's tile, and sets it as a travel target if not +///The target is taken from the blackboard. This one always requires a specific implementation. +/datum/ai_planning_subtree/prepare_travel_to_destination + var/target_key + var/travel_destination_key = BB_TRAVEL_DESTINATION + +/datum/ai_planning_subtree/prepare_travel_to_destination/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + var/atom/target = controller.blackboard[target_key] + + //Target is deleted, or we are already standing on it + if(QDELETED(target) || (isturf(target) && controller.pawn.loc == target) || (target.loc == controller.pawn.loc)) + return + + //Already set with this value, return + if(controller.blackboard[target_key] == controller.blackboard[travel_destination_key]) + return + + controller.queue_behavior(/datum/ai_behavior/set_travel_destination, target_key, travel_destination_key) + return //continue planning regardless of success + +/datum/ai_planning_subtree/prepare_travel_to_destination/trader + target_key = BB_SHOP_SPOT diff --git a/code/datums/ai/generic/find_and_set.dm b/code/datums/ai/generic/find_and_set.dm index 43337674969..6107b1664ee 100644 --- a/code/datums/ai/generic/find_and_set.dm +++ b/code/datums/ai/generic/find_and_set.dm @@ -135,3 +135,14 @@ if (nearby_bodies.len) return pick(nearby_bodies) + +/** + * A variant that looks for a human who is not dead or incapacitated, and has a mind + */ +/datum/ai_behavior/find_and_set/conscious_person + +/datum/ai_behavior/find_and_set/conscious_person/search_tactic(datum/ai_controller/controller, locate_path, search_range) + for(var/mob/living/carbon/human/target in oview(search_range, controller.pawn)) + if(IS_DEAD_OR_INCAP(target) || !target.mind) + continue + return target diff --git a/code/datums/ai/idle_behaviors/idle_random_walk.dm b/code/datums/ai/idle_behaviors/idle_random_walk.dm index d99957f419b..f189e89b423 100644 --- a/code/datums/ai/idle_behaviors/idle_random_walk.dm +++ b/code/datums/ai/idle_behaviors/idle_random_walk.dm @@ -25,6 +25,20 @@ return return ..() +/// Only walk if we are not on the target's location +/datum/idle_behavior/idle_random_walk/not_while_on_target + ///What is the spot we have to stand on? + var/target_key + +/datum/idle_behavior/idle_random_walk/not_while_on_target/perform_idle_behavior(seconds_per_tick, datum/ai_controller/controller) + var/atom/target = controller.blackboard[target_key] + + //Don't move, if we are are already standing on it + if(!QDELETED(target) && ((isturf(target) && controller.pawn.loc == target) || (target.loc == controller.pawn.loc))) + return + + return ..() + /// walk randomly however stick near a target /datum/idle_behavior/walk_near_target /// chance to walk diff --git a/code/datums/components/appearance_on_aggro.dm b/code/datums/components/appearance_on_aggro.dm index 33a3d7c2e90..8c0df88e6fd 100644 --- a/code/datums/components/appearance_on_aggro.dm +++ b/code/datums/components/appearance_on_aggro.dm @@ -45,7 +45,6 @@ return current_target = target - RegisterSignal(target, COMSIG_QDELETING, PROC_REF(on_clear_target)) if (!isnull(aggro_overlay) || !isnull(aggro_state)) source.update_appearance(UPDATE_ICON) if (!isnull(alpha_on_aggro)) @@ -61,7 +60,6 @@ revert_appearance(parent) /datum/component/appearance_on_aggro/proc/revert_appearance(mob/living/source) - UnregisterSignal(current_target, COMSIG_QDELETING) current_target = null if (!isnull(aggro_overlay) || !isnull(aggro_state)) source.update_appearance(UPDATE_ICON) diff --git a/code/datums/components/trader/trader.dm b/code/datums/components/trader/trader.dm new file mode 100644 index 00000000000..b1004138527 --- /dev/null +++ b/code/datums/components/trader/trader.dm @@ -0,0 +1,457 @@ +#define TRADER_RADIAL_BUY "TRADER_RADIAL_BUY" +#define TRADER_RADIAL_SELL "TRADER_RADIAL_SELL" +#define TRADER_RADIAL_TALK "TRADER_RADIAL_TALK" +#define TRADER_RADIAL_LORE "TRADER_RADIAL_LORE" +#define TRADER_RADIAL_NO "TRADER_RADIAL_NO" +#define TRADER_RADIAL_YES "TRADER_RADIAL_YES" +#define TRADER_RADIAL_OUT_OF_STOCK "TRADER_RADIAL_OUT_OF_STOCK" +#define TRADER_RADIAL_DISCUSS_BUY "TRADER_RADIAL_DISCUSS_BUY" +#define TRADER_RADIAL_DISCUSS_SELL "TRADER_RADIAL_DISCUSS_SELL" + +#define TRADER_OPTION_BUY "Buy" +#define TRADER_OPTION_SELL "Sell" +#define TRADER_OPTION_TALK "Talk" +#define TRADER_OPTION_LORE "Lore" +#define TRADER_OPTION_NO "No" +#define TRADER_OPTION_YES "Yes" +#define TRADER_OPTION_BUYING "Buying?" +#define TRADER_OPTION_SELLING "Selling?" + +//The defines below show the index the info is located in the product_info entry list + +#define TRADER_PRODUCT_INFO_PRICE 1 +#define TRADER_PRODUCT_INFO_QUANTITY 2 +//Only valid for wanted_items +#define TRADER_PRODUCT_INFO_PRICE_MOD_DESCRIPTION 3 + +/** + * # Trader NPC Component + * Manages the barks and the stocks of the traders + * Also manages the interactive radial menu + */ +/datum/component/trader + + /** + * Format; list(TYPEPATH = list(PRICE, QUANTITY)) + * Associated list of items the NPC sells with how much they cost and the quantity available before a restock + * This list is filled by Initialize(), if you want to change the starting products, modify initial_products() + * * + */ + var/list/obj/item/products = list() + /** + * A list of wanted items that the trader would wish to buy, each typepath has a assigned value, quantity and additional flavor text + * + * CHILDREN OF TYPEPATHS INCLUDED IN WANTED_ITEMS WILL BE TREATED AS THE PARENT IF NO ENTRY EXISTS FOR THE CHILDREN + * + * As an additional note; if you include multiple children of a typepath; the typepath with the most children should be placed after all other typepaths + * Bad; list(/obj/item/milk = list(100, 1, ""), /obj/item/milk/small = list(50, 2, "")) + * Good; list(/obj/item/milk/small = list(50, 2, ""), /obj/item/milk = list(100, 1, "")) + * This is mainly because sell_item() uses a istype(item_being_sold, item_in_entry) to determine what parent should the child be automatically considered as + * If /obj/item/milk/small/spooky was being sold; /obj/item/milk/small would be the first to check against rather than /obj/item/milk + * + * Format; list(TYPEPATH = list(PRICE, QUANTITY, ADDITIONAL_DESCRIPTION)) + * Associated list of items able to be sold to the NPC with the money given for them. + * The price given should be the "base" price; any price manipulation based on variables should be done with apply_sell_price_mods() + * ADDITIONAL_DESCRIPTION is any additional text added to explain how the variables of the item effect the price; if it's stack based, it's final price depends how much is in the stack + * EX; /obj/item/stack/sheet/mineral/diamond = list(500, INFINITY, ", per 100 cm3 sheet of diamond") + * This list is filled by Initialize(), if you want to change the starting wanted items, modify initial_wanteds() + */ + var/list/wanted_items = list() + + ///Contains images of all radial icons + var/static/list/radial_icons_cache = list() + + ///Contains information of a specific trader + var/datum/trader_data/trader_data + +/* +Can accept both a type path, and an instance of a datum. Type path has priority. +*/ +/datum/component/trader/Initialize(trader_data_path = null, trader_data = null) + . = ..() + if(!isliving(parent)) + return COMPONENT_INCOMPATIBLE + + if(ispath(trader_data_path, /datum/trader_data)) + trader_data = new trader_data_path + if(isnull(trader_data)) + CRASH("Initialised trader component with no trader data.") + + src.trader_data = trader_data + + radial_icons_cache = list( + TRADER_RADIAL_BUY = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_buy"), + TRADER_RADIAL_SELL = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_sell"), + TRADER_RADIAL_TALK = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_talk"), + TRADER_RADIAL_LORE = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_lore"), + TRADER_RADIAL_DISCUSS_BUY = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_buying"), + TRADER_RADIAL_DISCUSS_SELL = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_selling"), + TRADER_RADIAL_YES = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_yes"), + TRADER_RADIAL_NO = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_no"), + TRADER_RADIAL_OUT_OF_STOCK = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_center"), + ) + + restock_products() + renew_item_demands() + +/datum/component/trader/RegisterWithParent() + RegisterSignal(parent, COMSIG_ATOM_ATTACK_HAND, PROC_REF(on_attack_hand)) + +/datum/component/trader/UnregisterFromParent() + UnregisterSignal(parent, COMSIG_ATOM_ATTACK_HAND) + +///If our trader is alive, and the customer left clicks them with an empty hand without combat mode +/datum/component/trader/proc/on_attack_hand(atom/source, mob/living/carbon/customer) + SIGNAL_HANDLER + if(!can_trade(customer) || customer.combat_mode) + return + var/list/npc_options = list() + if(length(products)) + npc_options[TRADER_OPTION_BUY] = radial_icons_cache[TRADER_RADIAL_BUY] + if(length(wanted_items)) + npc_options[TRADER_OPTION_SELL] = radial_icons_cache[TRADER_RADIAL_SELL] + if(length(trader_data.say_phrases)) + npc_options[TRADER_OPTION_TALK] = radial_icons_cache[TRADER_RADIAL_TALK] + if(!length(npc_options)) + return + + var/mob/living/trader = parent + trader.face_atom(customer) + + INVOKE_ASYNC(src, PROC_REF(open_npc_options), customer, npc_options) + + return COMPONENT_CANCEL_ATTACK_CHAIN + +/** + * Generates a radial of the initial radials of the NPC + * Called via asynch, due to the sleep caused by show_radial_menu + * Arguments: + * * customer - (Mob REF) The mob trying to buy something + */ +/datum/component/trader/proc/open_npc_options(mob/living/carbon/customer, list/npc_options) + if(!can_trade(customer)) + return + var/npc_result = show_radial_menu(customer, parent, npc_options, custom_check = CALLBACK(src, PROC_REF(check_menu), customer), require_near = TRUE, tooltips = TRUE) + switch(npc_result) + if(TRADER_OPTION_BUY) + buy_item(customer) + if(TRADER_OPTION_SELL) + try_sell(customer) + if(TRADER_OPTION_TALK) + discuss(customer) + +/** + * Checks if the customer is ok to use the radial + * + * Checks if the customer is not a mob or is incapacitated or not adjacent to the source of the radial, in those cases returns FALSE, otherwise returns TRUE + * Arguments: + * * customer - (Mob REF) The mob checking the menu + */ +/datum/component/trader/proc/check_menu(mob/customer) + if(!istype(customer)) + return FALSE + if(IS_DEAD_OR_INCAP(customer) || !customer.Adjacent(parent)) + return FALSE + return TRUE + +/** + * Generates a radial of the items the NPC sells and lets the user try to buy one + * Arguments: + * * customer - (Mob REF) The mob trying to buy something + */ +/datum/component/trader/proc/buy_item(mob/customer) + if(!can_trade(customer)) + return + + if(!LAZYLEN(products)) + return + + var/list/display_names = list() + var/list/items = list() + var/list/product_info + + for(var/obj/item/thing as anything in products) + display_names["[initial(thing.name)]"] = thing + + if(!radial_icons_cache[thing]) + radial_icons_cache[thing] = image(icon = initial(thing.icon), icon_state = initial(thing.icon_state_preview) ? initial(thing.icon_state_preview) : initial(thing.icon_state)) + + var/image/item_image = radial_icons_cache[thing] + product_info = products[thing] + + if(product_info[TRADER_PRODUCT_INFO_QUANTITY] <= 0) //out of stock + item_image.overlays += radial_icons_cache[TRADER_RADIAL_OUT_OF_STOCK] + + items += list("[initial(thing.name)]" = item_image) + + var/pick = show_radial_menu(customer, parent, items, custom_check = CALLBACK(src, PROC_REF(check_menu), customer), require_near = TRUE, tooltips = TRUE) + if(!pick || !can_trade(customer)) + return + + var/obj/item/item_to_buy = display_names[pick] + var/mob/living/trader = parent + trader.face_atom(customer) + product_info = products[item_to_buy] + + if(!product_info[TRADER_PRODUCT_INFO_QUANTITY]) + trader.say("[initial(item_to_buy.name)] appears to be out of stock.") + return + + trader.say("It will cost you [product_info[TRADER_PRODUCT_INFO_PRICE]] [trader_data.currency_name] to buy \the [initial(item_to_buy.name)]. Are you sure you want to buy it?") + var/list/npc_options = list( + TRADER_OPTION_YES = radial_icons_cache[TRADER_RADIAL_YES], + TRADER_OPTION_NO = radial_icons_cache[TRADER_RADIAL_NO], + ) + + var/buyer_will_buy = show_radial_menu(customer, trader, npc_options, custom_check = CALLBACK(src, PROC_REF(check_menu), customer), require_near = TRUE, tooltips = TRUE) + if(buyer_will_buy != TRADER_OPTION_YES || !can_trade(customer)) + return + + trader.face_atom(customer) + + if(!spend_buyer_offhand_money(customer, product_info[TRADER_PRODUCT_INFO_PRICE])) + trader.say(trader_data.return_trader_phrase(NO_CASH_PHRASE)) + return + + item_to_buy = new item_to_buy(get_turf(customer)) + customer.put_in_hands(item_to_buy) + playsound(trader, trader_data.sell_sound, 50, TRUE) + log_econ("[item_to_buy] has been sold to [customer] (typepath used for product info; [item_to_buy.type]) by [trader] for [product_info[TRADER_PRODUCT_INFO_PRICE]] cash.") + product_info[TRADER_PRODUCT_INFO_QUANTITY] -= 1 + trader.say(trader_data.return_trader_phrase(BUY_PHRASE)) + +///Calculates the value of money in the hand of the buyer and spends it if it's sufficient +/datum/component/trader/proc/spend_buyer_offhand_money(mob/customer, the_cost) + var/value = 0 + var/obj/item/holochip/cash = customer.is_holding_item_of_type(/obj/item/holochip) + if(cash) + value += cash.credits + if((value >= the_cost) && cash) + return cash.spend(the_cost) + return FALSE //Purchase unsuccessful + +/** + * Tries to call sell_item on one of the customer's held items, if fail gives a chat message + * + * Gets both items in the customer's hands, and then tries to call sell_item on them, if both fail, he gives a chat message + * Arguments: + * * customer - (Mob REF) The mob trying to sell something + */ +/datum/component/trader/proc/try_sell(mob/customer) + if(!can_trade(customer)) + return + var/sold_item = FALSE + for(var/obj/item/an_item in customer.held_items) + if(sell_item(customer, an_item)) + sold_item = TRUE + break + if(!sold_item && can_trade(customer)) //only talk if you are not dead or in combat + var/mob/living/trader = parent + trader.say(trader_data.return_trader_phrase(ITEM_REJECTED_PHRASE)) + + +/** + * Checks if an item is in the list of wanted items and if it is after a Yes/No radial returns generate_cash with the value of the item for the NPC + * Arguments: + * * customer - (Mob REF) The mob trying to sell something + * * selling - (Item REF) The item being sold + */ +/datum/component/trader/proc/sell_item(mob/customer, obj/item/selling) + if(isnull(selling)) + return FALSE + var/list/product_info + //Keep track of the typepath; rather mundane but it's required for correctly modifying the wanted_items + //should a product be sellable because even if it doesn't have a entry because it's a child of a parent that is present on the list + var/typepath_for_product_info + + if(selling.type in wanted_items) + product_info = wanted_items[selling.type] + typepath_for_product_info = selling.type + else //Assume wanted_items is setup in the correct way; read wanted_items documentation for more info + for(var/typepath in wanted_items) + if(!istype(selling, typepath)) + continue + + product_info = wanted_items[typepath] + typepath_for_product_info = typepath + break + + if(!product_info) //Nothing interesting to sell + return FALSE + + var/mob/living/trader = parent + + if(product_info[TRADER_PRODUCT_INFO_QUANTITY] <= 0) + trader.say(trader_data.return_trader_phrase(TRADER_HAS_ENOUGH_ITEM_PHRASE)) + return FALSE + + var/cost = apply_sell_price_mods(selling, product_info[TRADER_PRODUCT_INFO_PRICE]) + if(cost <= 0) + trader.say(trader_data.return_trader_phrase(ITEM_IS_WORTHLESS_PHRASE)) + return FALSE + + trader.say(trader_data.return_trader_phrase(INTERESTED_PHRASE)) + trader.say("You will receive [cost] [trader_data.currency_name] for the [selling].") + var/list/npc_options = list( + TRADER_OPTION_YES = radial_icons_cache[TRADER_RADIAL_YES], + TRADER_OPTION_NO = radial_icons_cache[TRADER_RADIAL_NO], + ) + + trader.face_atom(customer) + + var/npc_result = show_radial_menu(customer, trader, npc_options, custom_check = CALLBACK(src, PROC_REF(check_menu), customer), require_near = TRUE, tooltips = TRUE) + if(!can_trade(customer)) + return + if(npc_result != TRADER_OPTION_YES) + trader.say(trader_data.return_trader_phrase(ITEM_SELLING_CANCELED_PHRASE)) + return TRUE + + trader.say(trader_data.return_trader_phrase(ITEM_SELLING_ACCEPTED_PHRASE)) + playsound(trader, trader_data.sell_sound, 50, TRUE) + log_econ("[selling] has been sold to [trader] (typepath used for product info; [typepath_for_product_info]) by [customer] for [cost] cash.") + exchange_sold_items(selling, cost, typepath_for_product_info) + generate_cash(cost, customer) + return TRUE + +/** + * Modifies the 'base' price of a item based on certain variables + * + * Arguments: + * * Reference to the item; this is the item being sold + * * Original cost; the original cost of the item, to be manipulated depending on the variables of the item, one example is using item.amount if it's a stack + */ +/datum/component/trader/proc/apply_sell_price_mods(obj/item/selling, original_cost) + if(isstack(selling)) + var/obj/item/stack/stackoverflow = selling + original_cost *= stackoverflow.amount + return original_cost + +/** + * Handles modifying/deleting the items to ensure that a proper amount is converted into cash; put into it's own proc to make the children of this not override a 30+ line sell_item() + * + * Arguments: + * * selling - (Item REF) this is the item being sold + * * value_exchanged_for - (Number) the "value", useful for a scenario where you want to remove enough items equal to the value + * * original_typepath - (Typepath) For scenarios where a children of a parent is being sold but we want to modify the parent's product information + */ +/datum/component/trader/proc/exchange_sold_items(obj/item/selling, value_exchanged_for, original_typepath) + var/list/product_info = wanted_items[original_typepath] + if(isstack(selling)) + var/obj/item/stack/the_stack = selling + var/actually_sold = min(the_stack.amount, product_info[TRADER_PRODUCT_INFO_QUANTITY]) + the_stack.use(actually_sold) + product_info[TRADER_PRODUCT_INFO_QUANTITY] -= (actually_sold) + else + qdel(selling) + product_info[TRADER_PRODUCT_INFO_QUANTITY] -= 1 + +/** + * Creates an item equal to the value set by the proc and puts it in the user's hands if possible + * Arguments: + * * value - A number; The amount of cash that will be on the holochip + * * customer - Reference to a mob; The mob we put the holochip in hands of + */ +/datum/component/trader/proc/generate_cash(value, mob/customer) + var/obj/item/holochip/chip = new /obj/item/holochip(get_turf(customer), value) + customer.put_in_hands(chip) + +///Talk about what items are being sold/wanted by the trader and in what quantity or lore +/datum/component/trader/proc/discuss(mob/customer) + var/list/npc_options = list( + TRADER_OPTION_LORE = radial_icons_cache[TRADER_RADIAL_LORE], + TRADER_OPTION_SELLING = radial_icons_cache[TRADER_RADIAL_DISCUSS_SELL], + TRADER_OPTION_BUYING = radial_icons_cache[TRADER_RADIAL_DISCUSS_BUY], + ) + var/pick = show_radial_menu(customer, parent, npc_options, custom_check = CALLBACK(src, PROC_REF(check_menu), customer), require_near = TRUE, tooltips = TRUE) + if(!can_trade(customer)) + return + switch(pick) + if(TRADER_OPTION_LORE) + var/mob/living/trader = parent + trader.say(trader_data.return_trader_phrase(TRADER_LORE_PHRASE)) + if(TRADER_OPTION_BUYING) + trader_buys_what(customer) + if(TRADER_OPTION_SELLING) + trader_sells_what(customer) + +///Displays to the customer what the trader is willing to buy and how much until a restock happens +/datum/component/trader/proc/trader_buys_what(mob/customer) + if(!can_trade(customer)) + return + if(!length(wanted_items)) + var/mob/living/trader = parent + trader.say(trader_data.return_trader_phrase(TRADER_NOT_BUYING_ANYTHING)) + return + + var/list/buy_info = list(span_green("I'm willing to buy the following:")) + + var/list/product_info + for(var/obj/item/thing as anything in wanted_items) + product_info = wanted_items[thing] + var/tern_op_result = (product_info[TRADER_PRODUCT_INFO_QUANTITY] == INFINITY ? "as many as I can." : "[product_info[TRADER_PRODUCT_INFO_QUANTITY]]") //Coder friendly string concat + if(product_info[TRADER_PRODUCT_INFO_QUANTITY] <= 0) //Zero demand + buy_info += span_notice("• [span_red("(DOESN'T WANT MORE)")] [initial(thing.name)] for [product_info[TRADER_PRODUCT_INFO_PRICE]] [trader_data.currency_name][product_info[TRADER_PRODUCT_INFO_PRICE_MOD_DESCRIPTION]]; willing to buy [span_red("[tern_op_result]")] more.") + else + buy_info += span_notice("• [initial(thing.name)] for [product_info[TRADER_PRODUCT_INFO_PRICE]] [trader_data.currency_name][product_info[TRADER_PRODUCT_INFO_PRICE_MOD_DESCRIPTION]]; willing to buy [span_green("[tern_op_result]")]") + + to_chat(customer, examine_block(buy_info.Join("\n"))) + +///Displays to the customer what the trader is selling and how much is in stock +/datum/component/trader/proc/trader_sells_what(mob/customer) + if(!can_trade(customer)) + return + var/mob/living/trader = parent + if(!length(products)) + trader.say(trader_data.return_trader_phrase(TRADER_NOT_SELLING_ANYTHING)) + return + var/list/sell_info = list(span_green("I'm currently selling the following:")) + var/list/product_info + for(var/obj/item/thing as anything in products) + product_info = products[thing] + var/tern_op_result = (product_info[TRADER_PRODUCT_INFO_QUANTITY] == INFINITY ? "an infinite amount" : "[product_info[TRADER_PRODUCT_INFO_QUANTITY]]") //Coder friendly string concat + if(product_info[TRADER_PRODUCT_INFO_QUANTITY] <= 0) //Out of stock + sell_info += span_notice("• [span_red("(OUT OF STOCK)")] [initial(thing.name)] for [product_info[TRADER_PRODUCT_INFO_PRICE]] [trader_data.currency_name]; [span_red("[tern_op_result]")] left in stock") + else + sell_info += span_notice("• [initial(thing.name)] for [product_info[TRADER_PRODUCT_INFO_PRICE]] [trader_data.currency_name]; [span_green("[tern_op_result]")] left in stock") + to_chat(customer, examine_block(sell_info.Join("\n"))) + +///Sets quantity of all products to initial(quanity); this proc is currently called during initialize +/datum/component/trader/proc/restock_products() + products = trader_data.initial_products.Copy() + +///Sets quantity of all wanted_items to initial(quanity); this proc is currently called during initialize +/datum/component/trader/proc/renew_item_demands() + wanted_items = trader_data.initial_wanteds.Copy() + +///Returns if the trader is conscious and its combat mode is disabled. +/datum/component/trader/proc/can_trade(mob/customer) + var/mob/living/trader = parent + if(trader.combat_mode) + trader.balloon_alert(customer, "in combat!") + return FALSE + if(IS_DEAD_OR_INCAP(trader)) + trader.balloon_alert(customer, "indisposed!") + return FALSE + return TRUE + +#undef TRADER_RADIAL_BUY +#undef TRADER_RADIAL_SELL +#undef TRADER_RADIAL_TALK +#undef TRADER_RADIAL_LORE +#undef TRADER_RADIAL_DISCUSS_BUY +#undef TRADER_RADIAL_DISCUSS_SELL +#undef TRADER_RADIAL_NO +#undef TRADER_RADIAL_YES +#undef TRADER_RADIAL_OUT_OF_STOCK +#undef TRADER_PRODUCT_INFO_PRICE +#undef TRADER_PRODUCT_INFO_QUANTITY +#undef TRADER_PRODUCT_INFO_PRICE_MOD_DESCRIPTION + +#undef TRADER_OPTION_BUY +#undef TRADER_OPTION_SELL +#undef TRADER_OPTION_TALK +#undef TRADER_OPTION_LORE +#undef TRADER_OPTION_NO +#undef TRADER_OPTION_YES +#undef TRADER_OPTION_BUYING +#undef TRADER_OPTION_SELLING diff --git a/code/datums/elements/ai_swap_combat_mode.dm b/code/datums/elements/ai_swap_combat_mode.dm new file mode 100644 index 00000000000..2b7167000fc --- /dev/null +++ b/code/datums/elements/ai_swap_combat_mode.dm @@ -0,0 +1,66 @@ +/** + * Attached to a mob with an AI controller, updates combat mode when the affected mob acquires or loses targets + */ +/datum/element/ai_swap_combat_mode + element_flags = ELEMENT_BESPOKE + argument_hash_start_idx = 2 + /// The message we yell when we enter combat mode + var/list/battle_start_barks + /// A one liner said when we exit combat mode + var/list/battle_end_barks + /// The chance to yell the above lines + var/speech_chance + /// Target key + var/target_key + +/datum/element/ai_swap_combat_mode/Attach(datum/target, target_key, list/battle_start_barks = null, list/battle_end_barks = null) + . = ..() + if(!isliving(target)) + return ELEMENT_INCOMPATIBLE + var/mob/living/living_target = target + if(!living_target.ai_controller) + return ELEMENT_INCOMPATIBLE + + if(isnull(battle_start_barks)) + battle_start_barks = list("En Garde!",) + + if(isnull(battle_end_barks)) + battle_end_barks = list("Never should have come here",) + + src.battle_start_barks = battle_start_barks + src.battle_end_barks = battle_end_barks + src.target_key = target_key + RegisterSignal(target, COMSIG_AI_BLACKBOARD_KEY_SET(target_key), PROC_REF(on_target_gained)) + RegisterSignal(target, COMSIG_AI_BLACKBOARD_KEY_CLEARED(target_key), PROC_REF(on_target_cleared)) + +/datum/element/ai_swap_combat_mode/Detach(datum/source) + . = ..() + UnregisterSignal(source, list( + COMSIG_AI_BLACKBOARD_KEY_SET(target_key), + COMSIG_AI_BLACKBOARD_KEY_CLEARED(target_key), + )) + +/// When the mob gains a target, and it was not already in combat mode, enter it +/datum/element/ai_swap_combat_mode/proc/on_target_gained(mob/living/source) + SIGNAL_HANDLER + + if(swap_mode(source, TRUE)) + INVOKE_ASYNC(src, PROC_REF(speak_bark), source, battle_start_barks) + +/// When the mob loses its target, and it was not already out of combat mode, exit it +/datum/element/ai_swap_combat_mode/proc/on_target_cleared(mob/living/source) + SIGNAL_HANDLER + + if(swap_mode(source, FALSE)) + INVOKE_ASYNC(src, PROC_REF(speak_bark), source, battle_end_barks) + +///Says a quip, if the RNG allows it +/datum/element/ai_swap_combat_mode/proc/speak_bark(mob/living/source, line) + source.say(pick(line)) + +///If the combat mode would be changed into a different state, updates it and returns TRUE, otherwise returns FALSE +/datum/element/ai_swap_combat_mode/proc/swap_mode(mob/living/source, new_mode) + if(source.combat_mode == new_mode) + return FALSE + source.set_combat_mode(new_mode) + return TRUE diff --git a/code/modules/hallucination/body.dm b/code/modules/hallucination/body.dm index 5167b63c5ee..2d017f96967 100644 --- a/code/modules/hallucination/body.dm +++ b/code/modules/hallucination/body.dm @@ -151,11 +151,11 @@ body_floats = TRUE /datum/hallucination/body/weird/faceless - body_image_file = 'icons/mob/simple/traders.dmi' + body_image_file = 'icons/obj/trader_signs.dmi' body_image_state = "faceless" /datum/hallucination/body/weird/bones - body_image_file = 'icons/mob/simple/traders.dmi' + body_image_file = 'icons/obj/trader_signs.dmi' body_image_state = "mrbones" /datum/hallucination/body/weird/freezer diff --git a/code/modules/mob/living/basic/trader/trader.dm b/code/modules/mob/living/basic/trader/trader.dm new file mode 100644 index 00000000000..29a2bda4199 --- /dev/null +++ b/code/modules/mob/living/basic/trader/trader.dm @@ -0,0 +1,79 @@ +/mob/living/basic/trader + name = "Trader" + desc = "Come buy some!" + unique_name = FALSE + icon = 'icons/mob/simple/simple_human.dmi' + maxHealth = 200 + health = 200 + melee_damage_lower = 10 + melee_damage_upper = 10 + attack_verb_continuous = "punches" + attack_verb_simple = "punch" + attack_sound = 'sound/weapons/punch1.ogg' + basic_mob_flags = DEL_ON_DEATH + unsuitable_atmos_damage = 2.5 + combat_mode = FALSE + move_resist = MOVE_FORCE_STRONG + mob_biotypes = MOB_ORGANIC|MOB_HUMANOID + sentience_type = SENTIENCE_HUMANOID + speed = 0 + + ai_controller = /datum/ai_controller/basic_controller/trader + + ///Sound used when item sold/bought + var/sell_sound = 'sound/effects/cashregister.ogg' + ///The currency name + var/currency_name = "credits" + ///The spawner we use to create our look + var/spawner_path = /obj/effect/mob_spawn/corpse/human/generic_assistant + ///Our species to create our look + var/species_path = /datum/species/human + ///The loot we drop when we die + var/loot = list(/obj/effect/mob_spawn/corpse/human/generic_assistant) + ///Casing used to shoot during retaliation + var/ranged_attack_casing = /obj/item/ammo_casing/shotgun/buckshot + ///Sound to make while doing a retalitory attack + var/ranged_attack_sound = 'sound/weapons/gun/pistol/shot.ogg' + ///Weapon path, for visuals + var/held_weapon_visual = /obj/item/gun/ballistic/shotgun + + ///Type path for the trader datum to use for retrieving the traders wares, speech, etc + var/trader_data_path = /datum/trader_data + + +/mob/living/basic/trader/Initialize(mapload) + . = ..() + apply_dynamic_human_appearance(src, species_path = species_path, mob_spawn_path = spawner_path, r_hand = held_weapon_visual) + + var/datum/trader_data/trader_data = new trader_data_path + AddComponent(/datum/component/trader, trader_data = trader_data) + AddComponent(/datum/component/ranged_attacks, casing_type = ranged_attack_casing, projectile_sound = ranged_attack_sound, cooldown_time = 3 SECONDS) + AddElement(/datum/element/ai_retaliate) + AddElement(/datum/element/ai_swap_combat_mode, BB_BASIC_MOB_CURRENT_TARGET, string_list(trader_data.say_phrases[TRADER_BATTLE_START_PHRASE]), string_list(trader_data.say_phrases[TRADER_BATTLE_END_PHRASE])) + if(LAZYLEN(loot)) + loot = string_list(loot) + AddElement(/datum/element/death_drops, loot) + + var/datum/action/setup_shop/setup_shop = new (src, trader_data.shop_spot_type, trader_data.sign_type, trader_data.sell_sound, trader_data.say_phrases[TRADER_SHOP_OPENING_PHRASE]) + setup_shop.Grant(src) + ai_controller.set_blackboard_key(BB_SETUP_SHOP, setup_shop) + +/mob/living/basic/trader/mrbones + name = "Mr. Bones" + desc = "A skeleton merchant, he seems very humerus." + speak_emote = list("rattles") + speech_span = SPAN_SANS + mob_biotypes = MOB_UNDEAD|MOB_HUMANOID + icon_state = "mrbones" + gender = MALE + + ai_controller = /datum/ai_controller/basic_controller/trader/jumpscare + + sell_sound = 'sound/voice/hiss2.ogg' + species_path = /datum/species/skeleton + spawner_path = /obj/effect/mob_spawn/corpse/human/skeleton/mrbones + loot = list(/obj/effect/decal/remains/human) + ranged_attack_casing = /obj/item/ammo_casing/energy/bolt/halloween + held_weapon_visual = /obj/item/gun/ballistic/revolver + + trader_data_path = /datum/trader_data/mr_bones diff --git a/code/modules/mob/living/basic/trader/trader_actions.dm b/code/modules/mob/living/basic/trader/trader_actions.dm new file mode 100644 index 00000000000..ded9fbd46d0 --- /dev/null +++ b/code/modules/mob/living/basic/trader/trader_actions.dm @@ -0,0 +1,72 @@ +/datum/action/setup_shop + name = "Setup shop" + desc = "Summons a wacky sales sign, and a comfy sitting spot to conduct your business from." + button_icon = 'icons/mob/actions/actions_trader.dmi' + button_icon_state = "setup_shop" + /// The shop spot + var/datum/weakref/shop_spot_ref + /// The server this console is connected to. + var/datum/weakref/sign_ref + /// The type of the chair we sit on + var/shop_spot_type + /// The type of our advertising sign + var/sign_type + /// The sound we make when we summon our shop gear + var/shop_sound + /// Lines we say when we open our shop + var/opening_lines + +/datum/action/setup_shop/IsAvailable(feedback = FALSE) + . = ..() + if (!.) + return FALSE + if(shop_spot_ref?.resolve()) + if(feedback) + owner.balloon_alert(owner, "already set up!") + return FALSE + return TRUE + +/datum/action/setup_shop/New(Target, shop_spot_type = /obj/structure/chair/plastic, sign_type = /obj/structure/trader_sign, sell_sound = 'sound/effects/cashregister.ogg', opening_lines = list("Welcome to my shop, friend!")) + . = ..() + + src.shop_spot_type = shop_spot_type + src.sign_type = sign_type + src.shop_sound = sell_sound + src.opening_lines = opening_lines + +/datum/action/setup_shop/Trigger(trigger_flags) + . = ..() + if(!.) + return + + owner.say(pick(opening_lines)) + var/obj/shop_spot = new shop_spot_type(owner.loc) + shop_spot.dir = owner.dir + shop_spot_ref = WEAKREF(shop_spot) + owner.ai_controller?.set_blackboard_key(BB_SHOP_SPOT, shop_spot) + + playsound(owner, shop_sound, 50, TRUE) + + var/turf/sign_turf + + sign_turf = try_find_valid_spot(owner.loc, turn(shop_spot.dir, -90)) + if(isnull(sign_turf)) //No space to my left, lets try right + sign_turf = try_find_valid_spot(owner.loc, turn(shop_spot.dir, 90)) + + if(isnull(sign_turf)) + return + + var/obj/sign = sign_ref?.resolve() + if(QDELETED(sign)) + var/obj/new_sign = new sign_type(sign_turf) + sign_ref = WEAKREF(sign) + do_sparks(3, FALSE, new_sign) + else + do_teleport(sign,sign_turf) + +///Look for a spot we can place our sign on +/datum/action/setup_shop/proc/try_find_valid_spot(origin_turf, direction_to_check) + var/turf/sign_turf = get_step(origin_turf, direction_to_check) + if(sign_turf && !isgroundlessturf(sign_turf) && !isclosedturf(sign_turf) && !sign_turf.is_blocked_turf()) + return sign_turf + return null diff --git a/code/modules/mob/living/basic/trader/trader_ai.dm b/code/modules/mob/living/basic/trader/trader_ai.dm new file mode 100644 index 00000000000..d6cf0095c7d --- /dev/null +++ b/code/modules/mob/living/basic/trader/trader_ai.dm @@ -0,0 +1,95 @@ +/datum/ai_controller/basic_controller/trader + blackboard = list( + BB_TARGETTING_DATUM = new /datum/targetting_datum/basic, + ) + + ai_movement = /datum/ai_movement/basic_avoidance + idle_behavior = /datum/idle_behavior/idle_random_walk/not_while_on_target/trader + planning_subtrees = list( + /datum/ai_planning_subtree/target_retaliate, + /datum/ai_planning_subtree/basic_ranged_attack_subtree/trader, + /datum/ai_planning_subtree/prepare_travel_to_destination/trader, + /datum/ai_planning_subtree/travel_to_point/and_clear_target, + /datum/ai_planning_subtree/setup_shop, + ) + +/datum/ai_controller/basic_controller/trader/jumpscare + planning_subtrees = list( + /datum/ai_planning_subtree/target_retaliate, + /datum/ai_planning_subtree/basic_ranged_attack_subtree/trader, + /datum/ai_planning_subtree/prepare_travel_to_destination/trader, + /datum/ai_planning_subtree/travel_to_point/and_clear_target, + /datum/ai_planning_subtree/setup_shop/jumpscare, + ) + +/datum/ai_planning_subtree/basic_ranged_attack_subtree/trader + ranged_attack_behavior = /datum/ai_behavior/basic_ranged_attack/trader + +/datum/ai_behavior/basic_ranged_attack/trader + action_cooldown = 3 SECONDS + avoid_friendly_fire = TRUE + +///Subtree to find our very first customer and set up our shop after walking right into their face +/datum/ai_planning_subtree/setup_shop + ///What do we do in order to offer our deals? + var/datum/ai_behavior/setup_shop/setup_shop_behavior = /datum/ai_behavior/setup_shop + +/datum/ai_planning_subtree/setup_shop/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + + //If we don't have our ability, return + if(!controller.blackboard_key_exists(BB_SETUP_SHOP)) + return + + //If we already have a shop spot, return + if(controller.blackboard_key_exists(BB_SHOP_SPOT)) + return + + //If we don't have a costurmer to greet, look for one + if(!controller.blackboard_key_exists(BB_FIRST_CUSTOMER)) + controller.queue_behavior(/datum/ai_behavior/find_and_set/conscious_person, BB_FIRST_CUSTOMER, /mob/living/carbon/human) + return + + //We have our first customer, time to tell them about incredible deals + controller.queue_behavior(setup_shop_behavior, BB_FIRST_CUSTOMER) + return SUBTREE_RETURN_FINISH_PLANNING + +///The ai will create a shop the moment they see a potential costumer +/datum/ai_behavior/setup_shop + +/datum/ai_behavior/setup_shop/setup(datum/ai_controller/controller, target_key) + var/obj/target = controller.blackboard[target_key] + return !QDELETED(target) + +/datum/ai_behavior/setup_shop/perform(seconds_per_tick, datum/ai_controller/controller, target_key) + . = ..() + + //We lost track of our costumer or our ability, abort + if(!controller.blackboard_key_exists(target_key) || !controller.blackboard_key_exists(BB_SETUP_SHOP)) + finish_action(controller, FALSE, target_key) + return + + var/datum/action/setup_shop/shop = controller.blackboard[BB_SETUP_SHOP] + shop.Trigger() + + controller.clear_blackboard_key(BB_FIRST_CUSTOMER) + + finish_action(controller, TRUE, target_key) + +/datum/idle_behavior/idle_random_walk/not_while_on_target/trader + target_key = BB_SHOP_SPOT + +///Version of setup show where the trader will run at you to assault you with incredible deals +/datum/ai_planning_subtree/setup_shop/jumpscare + setup_shop_behavior = /datum/ai_behavior/setup_shop/jumpscare + +/datum/ai_behavior/setup_shop/jumpscare + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH + +/datum/ai_behavior/setup_shop/jumpscare/setup(datum/ai_controller/controller, target_key) + . = ..() + if(.) + set_movement_target(controller, controller.blackboard[target_key]) + +/datum/ai_behavior/setup_shop/finish_action(datum/ai_controller/controller, succeeded, target_key) + . = ..() + controller.clear_blackboard_key(target_key) diff --git a/code/modules/mob/living/basic/trader/trader_data.dm b/code/modules/mob/living/basic/trader/trader_data.dm new file mode 100644 index 00000000000..9762dc02be5 --- /dev/null +++ b/code/modules/mob/living/basic/trader/trader_data.dm @@ -0,0 +1,156 @@ +///Used to contain the traders initial wares, and speech +/datum/trader_data + + ///The item that marks the shopkeeper will sit on + var/shop_spot_type = /obj/structure/chair/plastic + ///The sign that will greet the customers + var/sign_type = /obj/structure/trader_sign + ///Sound used when item sold/bought + var/sell_sound = 'sound/effects/cashregister.ogg' + ///The currency name + var/currency_name = "credits" + ///The initial products that the trader offers + var/list/initial_products = list( + /obj/item/food/burger/ghost = list(PAYCHECK_CREW * 4, INFINITY), + ) + ///The initial products that the trader buys + var/list/initial_wanteds = list( + /obj/item/ectoplasm = list(PAYCHECK_CREW * 2, INFINITY, ""), + ) + ///The speech data of the trader + var/list/say_phrases = list( + ITEM_REJECTED_PHRASE = list( + "Sorry, I'm not a fan of anything you're showing me. Give me something better and we'll talk.", + ), + ITEM_SELLING_CANCELED_PHRASE = list( + "What a shame, tell me if you changed your mind.", + ), + ITEM_SELLING_ACCEPTED_PHRASE = list( + "Pleasure doing business with you.", + ), + INTERESTED_PHRASE = list( + "Hey, you've got an item that interests me, I'd like to buy it, I'll give you some cash for it, deal?", + ), + BUY_PHRASE = list( + "Pleasure doing business with you.", + ), + NO_CASH_PHRASE = list( + "Sorry adventurer, I can't give credit! Come back when you're a little mmmmm... richer!", + ), + NO_STOCK_PHRASE = list( + "Sorry adventurer, but that item is not in stock at the moment.", + ), + NOT_WILLING_TO_BUY_PHRASE = list( + "I don't want to buy that item for the time being, check back another time.", + ), + ITEM_IS_WORTHLESS_PHRASE = list( + "This item seems to be worthless on a closer look, I won't buy this.", + ), + TRADER_HAS_ENOUGH_ITEM_PHRASE = list( + "I already bought enough of this for the time being.", + ), + TRADER_LORE_PHRASE = list( + "Hello! I am the test trader.", + "Oooooooo~!", + ), + TRADER_NOT_BUYING_ANYTHING = list( + "I'm currently buying nothing at the moment.", + ), + TRADER_NOT_SELLING_ANYTHING = list( + "I'm currently selling nothing at the moment.", + ), + TRADER_BATTLE_START_PHRASE = list( + "Thief!", + ), + TRADER_BATTLE_END_PHRASE = list( + "That is a discount I call death.", + ), + TRADER_SHOP_OPENING_PHRASE = list( + "Welcome to my shop, friend!", + ), + ) + +/** + * Depending on the passed parameter/override, returns a randomly picked string out of a list + * + * Do note when overriding this argument, you will need to ensure pick(the list) doesn't get supplied with a list of zero length + * Arguments: + * * say_text - (String) a define that matches the key of a entry in say_phrases + */ +/datum/trader_data/proc/return_trader_phrase(say_text) + if(!length(say_phrases[say_text])) + return + return pick(say_phrases[say_text]) + +/datum/trader_data/mr_bones + shop_spot_type = /obj/structure/chair/wood/wings + sign_type = /obj/structure/trader_sign/mrbones + sell_sound = 'sound/voice/hiss2.ogg' + + initial_products = list( + /obj/item/clothing/head/helmet/skull = list(PAYCHECK_CREW * 3, INFINITY), + /obj/item/clothing/mask/bandana/skull/black = list(PAYCHECK_CREW, INFINITY), + /obj/item/food/cookie/sugar/spookyskull = list(PAYCHECK_CREW * 0.2, INFINITY), + /obj/item/instrument/trombone/spectral = list(PAYCHECK_CREW * 200, INFINITY), + /obj/item/shovel/serrated = list(PAYCHECK_CREW * 3, INFINITY), + ) + + initial_wanteds = list( + /obj/item/reagent_containers/condiment/milk = list(PAYCHECK_CREW * 20, INFINITY, ""), + /obj/item/stack/sheet/bone = list(PAYCHECK_CREW * 8.4, INFINITY, ", per sheet of bone"), + ) + + say_phrases = list( + ITEM_REJECTED_PHRASE = list( + "Sorry, I'm not a fan of anything you're showing me. Give me something better and we'll talk.", + ), + ITEM_SELLING_CANCELED_PHRASE = list( + "What a shame, tell me if you changed your mind.", + ), + ITEM_SELLING_ACCEPTED_PHRASE = list( + "Pleasure doing business with you.", + ), + INTERESTED_PHRASE = list( + "Hey, you've got an item that interests me, I'd like to buy it, I'll give you some cash for it, deal?", + ), + BUY_PHRASE = list( + "Bone appetit!", + ), + NO_CASH_PHRASE = list( + "Sorry adventurer, I can't give credit! Come back when you're a little mmmmm... richer!", + ), + NO_STOCK_PHRASE = list( + "Sorry adventurer, but that item is not in stock at the moment.", + ), + NOT_WILLING_TO_BUY_PHRASE = list( + "I don't want to buy that item for the time being, check back another time.", + ), + ITEM_IS_WORTHLESS_PHRASE = list( + "This item seems to be worthless on a closer look, I won't buy this.", + ), + TRADER_HAS_ENOUGH_ITEM_PHRASE = list( + "I already bought enough of this for the time being.", + ), + TRADER_LORE_PHRASE = list( + "Hello, I am Mr. Bones!", + "The ride never ends!", + "I'd really like a refreshing carton of milk!", + "I'm willing to play big prices for BONES! Need materials to make merch, eh?", + "It's a beautiful day outside. Birds are singing, Flowers are blooming... On days like these, kids like you... Should be buying my wares!", + ), + TRADER_NOT_BUYING_ANYTHING = list( + "I'm currently buying nothing at the moment.", + ), + TRADER_NOT_SELLING_ANYTHING = list( + "I'm currently selling nothing at the moment.", + ), + TRADER_BATTLE_START_PHRASE = list( + "The ride ends for you!", + ), + TRADER_BATTLE_END_PHRASE = list( + "Mr. Bones never misses!", + ), + TRADER_SHOP_OPENING_PHRASE = list( + "My wild ride is open!", + ), + ) diff --git a/code/modules/mob/living/basic/trader/trader_items.dm b/code/modules/mob/living/basic/trader/trader_items.dm new file mode 100644 index 00000000000..173e33d4c2f --- /dev/null +++ b/code/modules/mob/living/basic/trader/trader_items.dm @@ -0,0 +1,36 @@ +///Sale signs +/obj/structure/trader_sign + name = "holographic store sign" + desc = "A holographic sign that promises great deals." + icon = 'icons/obj/trader_signs.dmi' + icon_state = "faceless" + anchored = TRUE + armor_type = /datum/armor/trader_sign + max_integrity = 15 + layer = FLY_LAYER + +/datum/armor/trader_sign + bullet = 50 + laser = 50 + energy = 50 + fire = 20 + acid = 20 + +/obj/structure/trader_sign/Initialize(mapload) + . = ..() + add_overlay("sign") + makeHologram() + + +/obj/structure/trader_sign/mrbones + icon_state = "mrbones" + + +///Spawners for outfits +/obj/effect/mob_spawn/corpse/human/skeleton/mrbones + mob_species = /datum/species/skeleton + outfit = /datum/outfit/mrbonescorpse + +/datum/outfit/mrbonescorpse + name = "Mr Bones' Corpse" + head = /obj/item/clothing/head/hats/tophat diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/trader.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/trader.dm deleted file mode 100644 index 5be6fc9575d..00000000000 --- a/code/modules/mob/living/simple_animal/hostile/retaliate/trader.dm +++ /dev/null @@ -1,501 +0,0 @@ -#define ITEM_REJECTED_PHRASE "ITEM_REJECTED_PHRASE" -#define ITEM_SELLING_CANCELED_PHRASE "ITEM_SELLING_CANCELED_PHRASE" -#define ITEM_SELLING_ACCEPTED_PHRASE "ITEM_SELLING_ACCEPTED_PHRASE" -#define INTERESTED_PHRASE "INTERESTED_PHRASE" -#define BUY_PHRASE "BUY_PHRASE" -#define NO_CASH_PHRASE "NO_CASH_PHRASE" -#define NO_STOCK_PHRASE "NO_STOCK_PHRASE" -#define NOT_WILLING_TO_BUY_PHRASE "NOT_WILLING_TO_BUY_PHRASE" -#define ITEM_IS_WORTHLESS_PHRASE "ITEM_IS_WORTHLESS_PHRASE" -#define TRADER_HAS_ENOUGH_ITEM_PHRASE "TRADER_HAS_ENOUGH_ITEM_PHRASE" -#define TRADER_LORE_PHRASE "TRADER_LORE_PHRASE" -#define TRADER_NOT_BUYING_ANYTHING "TRADER_NOT_BUYING_ANYTHING" -#define TRADER_NOT_SELLING_ANYTHING "TRADER_NOT_SELLING_ANYTHING" - -#define TRADER_PRODUCT_INFO_PRICE 1 -#define TRADER_PRODUCT_INFO_QUANTITY 2 -//Only valid for wanted_items -#define TRADER_PRODUCT_INFO_PRICE_MOD_DESCRIPTION 3 - -/** - * # Trader - * - * A mob that has some dialogue options with radials, allows for selling items and buying em' - * - */ -/mob/living/simple_animal/hostile/retaliate/trader - name = "Trader" - desc = "Come buy some!" - icon = 'icons/mob/simple/traders.dmi' - icon_state = "faceless" - maxHealth = 200 - health = 200 - melee_damage_lower = 10 - melee_damage_upper = 10 - attack_verb_continuous = "punches" - attack_verb_simple = "punch" - attack_sound = 'sound/weapons/punch1.ogg' - del_on_death = TRUE - loot = list(/obj/effect/mob_spawn/corpse/human) - atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_plas" = 0, "max_plas" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0) - unsuitable_atmos_damage = 2.5 - casingtype = /obj/item/ammo_casing/shotgun/buckshot - wander = FALSE - ranged = TRUE - combat_mode = TRUE - move_resist = MOVE_FORCE_STRONG - mob_biotypes = MOB_ORGANIC|MOB_HUMANOID - sentience_type = SENTIENCE_HUMANOID - speed = 0 - stat_attack = HARD_CRIT - robust_searching = TRUE - check_friendly_fire = TRUE - interaction_flags_atom = INTERACT_ATOM_NO_FINGERPRINT_ATTACK_HAND|INTERACT_ATOM_ATTACK_HAND|INTERACT_ATOM_NO_FINGERPRINT_INTERACT - ///Sound used when item sold/bought - var/sell_sound = 'sound/effects/cashregister.ogg' - /** - * Format; list(TYPEPATH = list(PRICE, QUANTITY)) - * Associated list of items the NPC sells with how much they cost and the quantity available before a restock - * This list is filled by Initialize(), if you want to change the starting products, modify initial_products() - * * - */ - var/list/products - /** - * A list of wanted items that the trader would wish to buy, each typepath has a assigned value, quantity and additional flavor text - * - * CHILDREN OF TYPEPATHS INCLUDED IN WANTED_ITEMS WILL BE TREATED AS THE PARENT IF NO ENTRY EXISTS FOR THE CHILDREN - * - * As an additional note; if you include multiple children of a typepath; the typepath with the most children should be placed after all other typepaths - * Bad; list(/obj/item/milk = list(100, 1, ""), /obj/item/milk/small = list(50, 2, "")) - * Good; list(/obj/item/milk/small = list(50, 2, ""), /obj/item/milk = list(100, 1, "")) - * This is mainly because sell_item() uses a istype(item_being_sold, item_in_entry) to determine what parent should the child be automatically considered as - * If /obj/item/milk/small/spooky was being sold; /obj/item/milk/small would be the first to check against rather than /obj/item/milk - * - * Format; list(TYPEPATH = list(PRICE, QUANTITY, ADDITIONAL_DESCRIPTION)) - * Associated list of items able to be sold to the NPC with the money given for them. - * The price given should be the "base" price; any price manipulation based on variables should be done with apply_sell_price_mods() - * ADDITIONAL_DESCRIPTION is any additional text added to explain how the variables of the item effect the price; if it's stack based, it's final price depends how much is in the stack - * EX; /obj/item/stack/sheet/mineral/diamond = list(500, INFINITY, ", per 100 cm3 sheet of diamond") - * This list is filled by Initialize(), if you want to change the starting wanted items, modify initial_wanteds() - */ - var/list/wanted_items - ///Associated list of defines matched with list of phrases; phrase to be said is dealt by return_trader_phrase() - var/list/say_phrases = list( - ITEM_REJECTED_PHRASE = list( - "Sorry, I'm not a fan of anything you're showing me. Give me something better and we'll talk." - ), - ITEM_SELLING_CANCELED_PHRASE = list( - "What a shame, tell me if you changed your mind." - ), - ITEM_SELLING_ACCEPTED_PHRASE = list( - "Pleasure doing business with you." - ), - INTERESTED_PHRASE = list( - "Hey, you've got an item that interests me, I'd like to buy it, I'll give you some cash for it, deal?" - ), - BUY_PHRASE = list( - "Pleasure doing business with you." - ), - NO_CASH_PHRASE = list( - "Sorry adventurer, I can't give credit! Come back when you're a little mmmmm... richer!" - ), - NO_STOCK_PHRASE = list( - "Sorry adventurer, but that item is not in stock at the moment." - ), - NOT_WILLING_TO_BUY_PHRASE = list( - "I don't want to buy that item for the time being, check back another time." - ), - ITEM_IS_WORTHLESS_PHRASE = list( - "This item seems to be worthless on a closer look, I won't buy this." - ), - TRADER_HAS_ENOUGH_ITEM_PHRASE = list( - "I already bought enough of this for the time being." - ), - TRADER_LORE_PHRASE = list( - "Hello! I am the test trader.", - "Oooooooo~!" - ), - TRADER_NOT_BUYING_ANYTHING = list( - "I'm currently buying nothing at the moment." - ), - TRADER_NOT_SELLING_ANYTHING = list( - "I'm currently selling nothing at the moment." - ), - ) - ///The name of the currency that is used when buying or selling items - var/currency_name = "credits" - -///Initializes the products and item demands of the trader -/mob/living/simple_animal/hostile/retaliate/trader/Initialize(mapload) - . = ..() - restock_products() - renew_item_demands() - -///Returns a list of the starting price/quanity/fluff text about the product listings; products = initial(products) doesn't work so this exists mainly for restock_products() -/mob/living/simple_animal/hostile/retaliate/trader/proc/initial_products() - return list(/obj/item/food/burger/ghost = list(200, INFINITY), - ) - -///Returns a list of the starting price/quanity/fluff text about the wanted items; wanted_items = initial(wanted_items) doesn't work so this exists mainly for renew_item_demands() -/mob/living/simple_animal/hostile/retaliate/trader/proc/initial_wanteds() - return list(/obj/item/ectoplasm = list(100, INFINITY, ""), - ) - -/** - * Depending on the passed parameter/override, returns a randomly picked string out of a list - * - * Do note when overriding this argument, you will need to ensure pick(the list) doesn't get supplied with a list of zero length - * Arguments: - * * say_text - (String) a define that matches the key of a entry in say_phrases - */ -/mob/living/simple_animal/hostile/retaliate/trader/proc/return_trader_phrase(say_text) - if(!length(say_phrases[say_text])) - return - return pick(say_phrases[say_text]) - //return (length(say_phrases[say_text]) ? pick(say_phrases[say_text]) : "") - -///Sets up the radials for the user and calls procs related to the actions the user wants to take -/mob/living/simple_animal/hostile/retaliate/trader/interact(mob/user) - if(user == target) - return FALSE - var/list/npc_options = list() - if(products.len) - npc_options["Buy"] = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_buy") - if(length(say_phrases[TRADER_LORE_PHRASE])) - npc_options["Talk"] = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_talk") - if(wanted_items.len) - npc_options["Sell"] = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_sell") - if(!npc_options.len) - return FALSE - var/npc_result = show_radial_menu(user, src, npc_options, custom_check = CALLBACK(src, PROC_REF(check_menu), user), require_near = TRUE, tooltips = TRUE) - switch(npc_result) - if("Buy") - buy_item(user) - if("Sell") - try_sell(user) - if("Talk") - discuss(user) - face_atom(user) - return TRUE - -/** - * Checks if the user is ok to use the radial - * - * Checks if the user is not a mob or is incapacitated or not adjacent to the source of the radial, in those cases returns FALSE, otherwise returns TRUE - * Arguments: - * * user - (Mob REF) The mob checking the menu - */ -/mob/living/simple_animal/hostile/retaliate/trader/proc/check_menu(mob/user) - if(!istype(user)) - return FALSE - if(user.incapacitated() || !user.Adjacent(src)) - return FALSE - return TRUE - -///Talk about what items are being sold/wanted by the trader and in what quantity or lore -/mob/living/simple_animal/hostile/retaliate/trader/proc/discuss(mob/user) - var/list/npc_options = list( - "Lore" = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_lore"), - "Selling?" = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_selling"), - "Buying?" = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_buying"), - ) - var/pick = show_radial_menu(user, src, npc_options, custom_check = CALLBACK(src, PROC_REF(check_menu), user), require_near = TRUE, tooltips = TRUE) - switch(pick) - if("Lore") - say(return_trader_phrase(TRADER_LORE_PHRASE)) - if("Buying?") - trader_buys_what(user) - if("Selling?") - trader_sells_what(user) - -///Displays to the user what the trader is willing to buy and how much until a restock happens -/mob/living/simple_animal/hostile/retaliate/trader/proc/trader_buys_what(mob/user) - if(!wanted_items.len) - say(return_trader_phrase(TRADER_NOT_BUYING_ANYTHING)) - return - var/list/product_info - to_chat(user, span_green("I'm willing to buy the following; ")) - for(var/obj/item/thing as anything in wanted_items) - product_info = wanted_items[thing] - var/tern_op_result = (product_info[TRADER_PRODUCT_INFO_QUANTITY] == INFINITY ? "as many as I can." : "[product_info[TRADER_PRODUCT_INFO_QUANTITY]]") //Coder friendly string concat - if(product_info[TRADER_PRODUCT_INFO_QUANTITY] <= 0) //Zero demand - to_chat(user, span_notice("[span_red("(DOESN'T WANT MORE)")] [initial(thing.name)] for [product_info[TRADER_PRODUCT_INFO_PRICE]] [currency_name][product_info[TRADER_PRODUCT_INFO_PRICE_MOD_DESCRIPTION]]; willing to buy [span_red("[tern_op_result]")] more.")) - else - to_chat(user, span_notice("[initial(thing.name)] for [product_info[TRADER_PRODUCT_INFO_PRICE]] [currency_name][product_info[TRADER_PRODUCT_INFO_PRICE_MOD_DESCRIPTION]]; willing to buy [span_green("[tern_op_result]")]")) - -///Displays to the user what the trader is selling and how much is in stock -/mob/living/simple_animal/hostile/retaliate/trader/proc/trader_sells_what(mob/user) - if(!products.len) - say(return_trader_phrase(TRADER_NOT_SELLING_ANYTHING)) - return - var/list/product_info - to_chat(user, span_green("I'm currently selling the following; ")) - for(var/obj/item/thing as anything in products) - product_info = products[thing] - var/tern_op_result = (product_info[TRADER_PRODUCT_INFO_QUANTITY] == INFINITY ? "an infinite amount" : "[product_info[TRADER_PRODUCT_INFO_QUANTITY]]") //Coder friendly string concat - if(product_info[TRADER_PRODUCT_INFO_QUANTITY] <= 0) //Out of stock - to_chat(user, span_notice("[span_red("(OUT OF STOCK)")] [initial(thing.name)] for [product_info[TRADER_PRODUCT_INFO_PRICE]] [currency_name]; [span_red("[tern_op_result]")] left in stock")) - else - to_chat(user, span_notice("[initial(thing.name)] for [product_info[TRADER_PRODUCT_INFO_PRICE]] [currency_name]; [span_green("[tern_op_result]")] left in stock")) - -/** - * Generates a radial of the items the NPC sells and lets the user try to buy one - * Arguments: - * * user - (Mob REF) The mob trying to buy something - */ -/mob/living/simple_animal/hostile/retaliate/trader/proc/buy_item(mob/user) - if(!LAZYLEN(products)) - return - - var/list/display_names = list() - var/list/items = list() - var/list/product_info - for(var/obj/item/thing as anything in products) - display_names["[initial(thing.name)]"] = thing - var/image/item_image = image(icon = initial(thing.icon), icon_state = initial(thing.icon_state)) - product_info = products[thing] - if(product_info[TRADER_PRODUCT_INFO_QUANTITY] <= 0) //out of stock - item_image.overlays += image(icon = 'icons/hud/radial.dmi', icon_state = "radial_center") - items += list("[initial(thing.name)]" = item_image) - var/pick = show_radial_menu(user, src, items, custom_check = CALLBACK(src, PROC_REF(check_menu), user), require_near = TRUE, tooltips = TRUE) - if(!pick) - return - var/obj/item/item_to_buy = display_names[pick] - face_atom(user) - product_info = products[item_to_buy] - if(!product_info[TRADER_PRODUCT_INFO_QUANTITY]) - say("[initial(item_to_buy.name)] appears to be out of stock.") - return - say("It will cost you [product_info[TRADER_PRODUCT_INFO_PRICE]] [currency_name] to buy \the [initial(item_to_buy.name)]. Are you sure you want to buy it?") - var/list/npc_options = list( - "Yes" = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_yes"), - "No" = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_no") - ) - var/buyer_will_buy = show_radial_menu(user, src, npc_options, custom_check = CALLBACK(src, PROC_REF(check_menu), user), require_near = TRUE, tooltips = TRUE) - if(buyer_will_buy != "Yes") - return - face_atom(user) - if(!spend_buyer_offhand_money(user, product_info[TRADER_PRODUCT_INFO_PRICE])) - say(return_trader_phrase(NO_CASH_PHRASE)) - return - item_to_buy = new item_to_buy(get_turf(user)) - user.put_in_hands(item_to_buy) - playsound(src, sell_sound, 50, TRUE) - product_info[TRADER_PRODUCT_INFO_QUANTITY] -= 1 - say(return_trader_phrase(BUY_PHRASE)) - -///Calculates the value of money in the hand of the buyer and spends it if it's sufficient -/mob/living/simple_animal/hostile/retaliate/trader/proc/spend_buyer_offhand_money(mob/user, the_cost) - var/value = 0 - var/obj/item/holochip/cash = user.is_holding_item_of_type(/obj/item/holochip) - if(cash) - value += cash.credits - if((value >= the_cost) && cash) - return cash.spend(the_cost) - return FALSE //Purchase unsuccessful - -/** - * Tries to call sell_item on one of the user's held items, if fail gives a chat message - * - * Gets both items in the user's hands, and then tries to call sell_item on them, if both fail, he gives a chat message - * Arguments: - * * user - (Mob REF) The mob trying to sell something - */ -/mob/living/simple_animal/hostile/retaliate/trader/proc/try_sell(mob/user) - var/sold_item = FALSE - for(var/obj/item/an_item in user.held_items) - if(sell_item(user, an_item)) - sold_item = TRUE - break - if(!sold_item) - say(return_trader_phrase(ITEM_REJECTED_PHRASE)) - -/** - * Checks if an item is in the list of wanted items and if it is after a Yes/No radial returns generate_cash with the value of the item for the NPC - * Arguments: - * * user - (Mob REF) The mob trying to sell something - * * selling - (Item REF) The item being sold - */ -/mob/living/simple_animal/hostile/retaliate/trader/proc/sell_item(mob/user, obj/item/selling) - var/cost - if(!selling) - return FALSE - var/list/product_info - //Keep track of the typepath; rather mundane but it's required for correctly modifying the wanted_items - //should a product be sellable because even if it doesn't have a entry because it's a child of a parent that is present on the list - var/typepath_for_product_info - if(selling.type in wanted_items) - product_info = wanted_items[selling.type] - typepath_for_product_info = selling.type - else //Assume wanted_items is setup in the correct way; read wanted_items documentation for more info - for(var/typepath in wanted_items) - if(istype(selling, typepath)) - product_info = wanted_items[typepath] - typepath_for_product_info = typepath - break - - if(!product_info) //Nothing interesting to sell - return FALSE - if(product_info[TRADER_PRODUCT_INFO_QUANTITY] <= 0) - say(return_trader_phrase(TRADER_HAS_ENOUGH_ITEM_PHRASE)) - return FALSE - cost = apply_sell_price_mods(selling, product_info[TRADER_PRODUCT_INFO_PRICE]) - if(cost <= 0) - say(return_trader_phrase(ITEM_IS_WORTHLESS_PHRASE)) - return FALSE - say(return_trader_phrase(INTERESTED_PHRASE)) - say("You will receive [cost] [currency_name] for the [selling].") - var/list/npc_options = list( - "Yes" = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_yes"), - "No" = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_no"), - ) - face_atom(user) - var/npc_result = show_radial_menu(user, src, npc_options, custom_check = CALLBACK(src, PROC_REF(check_menu), user), require_near = TRUE, tooltips = TRUE) - if(npc_result != "Yes") - say(return_trader_phrase(ITEM_SELLING_CANCELED_PHRASE)) - return TRUE - say(return_trader_phrase(ITEM_SELLING_ACCEPTED_PHRASE)) - playsound(src, sell_sound, 50, TRUE) - log_econ("[selling] has been sold to [src] (typepath used for product info; [typepath_for_product_info]) by [user] for [cost] cash.") - exchange_sold_items(selling, cost, typepath_for_product_info) - generate_cash(cost, user) - return TRUE - -/** - * Handles modifying/deleting the items to ensure that a proper amount is converted into cash; put into it's own proc to make the children of this not override a 30+ line sell_item() - * - * Arguments: - * * selling - (Item REF) this is the item being sold - * * value_exchanged_for - (Number) the "value", useful for a scenario where you want to remove enough items equal to the value - * * original_typepath - (Typepath) For scenarios where a children of a parent is being sold but we want to modify the parent's product information - */ -/mob/living/simple_animal/hostile/retaliate/trader/proc/exchange_sold_items(obj/item/selling, value_exchanged_for, original_typepath) - var/list/product_info = wanted_items[original_typepath] - if(isstack(selling)) - var/obj/item/stack/the_stack = selling - var/actually_sold = min(the_stack.amount, product_info[TRADER_PRODUCT_INFO_QUANTITY]) - the_stack.use(actually_sold) - product_info[TRADER_PRODUCT_INFO_QUANTITY] -= (actually_sold) - else - qdel(selling) - product_info[TRADER_PRODUCT_INFO_QUANTITY] -= 1 - -/** - * Modifies the 'base' price of a item based on certain variables - * - * Arguments: - * * Reference to the item; this is the item being sold - * * Original cost; the original cost of the item, to be manipulated depending on the variables of the item, one example is using item.amount if it's a stack - */ -/mob/living/simple_animal/hostile/retaliate/trader/proc/apply_sell_price_mods(obj/item/selling, original_cost) - if(isstack(selling)) - var/obj/item/stack/stackoverflow = selling - original_cost *= stackoverflow.amount - return original_cost - -/** - * Creates an item equal to the value set by the proc and puts it in the user's hands if possible - * Arguments: - * * value - A number; The amount of cash that will be on the holochip - * * user - Reference to a mob; The mob we put the holochip in hands of - */ -/mob/living/simple_animal/hostile/retaliate/trader/proc/generate_cash(value, mob/user) - var/obj/item/holochip/chip = new /obj/item/holochip(get_turf(user), value) - user.put_in_hands(chip) - -///Sets quantity of all products to initial(quanity); this proc is currently not called anywhere on the base class of traders -/mob/living/simple_animal/hostile/retaliate/trader/proc/restock_products() - products = initial_products() - -///Sets quantity of all wanted_items to initial(quanity); this proc is currently not called anywhere on the base class of traders -/mob/living/simple_animal/hostile/retaliate/trader/proc/renew_item_demands() - wanted_items = initial_wanteds() - -/mob/living/simple_animal/hostile/retaliate/trader/mrbones - name = "Mr. Bones" - desc = "A skeleton merchant, he seems very humerus." - speak_emote = list("rattles") - speech_span = SPAN_SANS - sell_sound = 'sound/voice/hiss2.ogg' - mob_biotypes = MOB_UNDEAD|MOB_HUMANOID - icon_state = "mrbones" - gender = MALE - loot = list(/obj/effect/decal/remains/human) - - say_phrases = list( - ITEM_REJECTED_PHRASE = list( - "Sorry, I'm not a fan of anything you're showing me. Give me something better and we'll talk." - ), - ITEM_SELLING_CANCELED_PHRASE = list( - "What a shame, tell me if you changed your mind." - ), - ITEM_SELLING_ACCEPTED_PHRASE = list( - "Pleasure doing business with you." - ), - INTERESTED_PHRASE = list( - "Hey, you've got an item that interests me, I'd like to buy it, I'll give you some cash for it, deal?" - ), - BUY_PHRASE = list( - "Bone appetit!" - ), - NO_CASH_PHRASE = list( - "Sorry adventurer, I can't give credit! Come back when you're a little mmmmm... richer!" - ), - NO_STOCK_PHRASE = list( - "Sorry adventurer, but that item is not in stock at the moment." - ), - NOT_WILLING_TO_BUY_PHRASE = list( - "I don't want to buy that item for the time being, check back another time." - ), - ITEM_IS_WORTHLESS_PHRASE = list( - "This item seems to be worthless on a closer look, I won't buy this." - ), - TRADER_HAS_ENOUGH_ITEM_PHRASE = list( - "I already bought enough of this for the time being." - ), - TRADER_LORE_PHRASE = list( - "Hello, I am Mr. Bones!", - "The ride never ends!", - "I'd really like a refreshing carton of milk!", - "I'm willing to play big prices for BONES! Need materials to make merch, eh?", - "It's a beautiful day outside. Birds are singing, Flowers are blooming... On days like these, kids like you... Should be buying my wares!" - ), - TRADER_NOT_BUYING_ANYTHING = list( - "I'm currently buying nothing at the moment." - ), - TRADER_NOT_SELLING_ANYTHING = list( - "I'm currently selling nothing at the moment." - ), - ) - -/mob/living/simple_animal/hostile/retaliate/trader/mrbones/initial_products() - return list( - /obj/item/clothing/head/helmet/skull = list(150, INFINITY), - /obj/item/clothing/mask/bandana/skull/black = list(50, INFINITY), - /obj/item/food/cookie/sugar/spookyskull = list(10, INFINITY), - /obj/item/instrument/trombone/spectral = list(10000, INFINITY), - /obj/item/shovel/serrated = list(150, INFINITY), - ) - -/mob/living/simple_animal/hostile/retaliate/trader/mrbones/initial_wanteds() - return list( - /obj/item/reagent_containers/condiment/milk = list(1000, INFINITY, ""), - /obj/item/stack/sheet/bone = list(420, INFINITY, ", per sheet of bone"), - ) - -#undef ITEM_REJECTED_PHRASE -#undef ITEM_SELLING_CANCELED_PHRASE -#undef ITEM_SELLING_ACCEPTED_PHRASE -#undef INTERESTED_PHRASE -#undef BUY_PHRASE -#undef NO_CASH_PHRASE -#undef NO_STOCK_PHRASE -#undef NOT_WILLING_TO_BUY_PHRASE -#undef ITEM_IS_WORTHLESS_PHRASE -#undef TRADER_HAS_ENOUGH_ITEM_PHRASE -#undef TRADER_LORE_PHRASE -#undef TRADER_NOT_BUYING_ANYTHING -#undef TRADER_NOT_SELLING_ANYTHING -#undef TRADER_PRODUCT_INFO_PRICE -#undef TRADER_PRODUCT_INFO_QUANTITY -#undef TRADER_PRODUCT_INFO_PRICE_MOD_DESCRIPTION diff --git a/code/modules/unit_tests/simple_animal_freeze.dm b/code/modules/unit_tests/simple_animal_freeze.dm index d6d864ca51c..2a163b2f535 100644 --- a/code/modules/unit_tests/simple_animal_freeze.dm +++ b/code/modules/unit_tests/simple_animal_freeze.dm @@ -118,8 +118,6 @@ /mob/living/simple_animal/hostile/retaliate, /mob/living/simple_animal/hostile/retaliate/goose, /mob/living/simple_animal/hostile/retaliate/goose/vomit, - /mob/living/simple_animal/hostile/retaliate/trader, - /mob/living/simple_animal/hostile/retaliate/trader/mrbones, /mob/living/simple_animal/hostile/vatbeast, /mob/living/simple_animal/hostile/wizard, /mob/living/simple_animal/hostile/zombie, diff --git a/icons/mob/actions/actions_trader.dmi b/icons/mob/actions/actions_trader.dmi new file mode 100644 index 00000000000..59330b4aac8 Binary files /dev/null and b/icons/mob/actions/actions_trader.dmi differ diff --git a/icons/mob/simple/traders.dmi b/icons/mob/simple/traders.dmi deleted file mode 100644 index 2f2b828bed9..00000000000 Binary files a/icons/mob/simple/traders.dmi and /dev/null differ diff --git a/icons/obj/trader_signs.dmi b/icons/obj/trader_signs.dmi new file mode 100644 index 00000000000..a6789e0bb4c Binary files /dev/null and b/icons/obj/trader_signs.dmi differ diff --git a/tgstation.dme b/tgstation.dme index 5b46203c027..36b5687e146 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -231,6 +231,7 @@ #include "code\__DEFINES\time.dm" #include "code\__DEFINES\tools.dm" #include "code\__DEFINES\toys.dm" +#include "code\__DEFINES\trader.dm" #include "code\__DEFINES\traits.dm" #include "code\__DEFINES\transport.dm" #include "code\__DEFINES\tts.dm" @@ -259,6 +260,7 @@ #include "code\__DEFINES\ai\pets.dm" #include "code\__DEFINES\ai\simplemob.dm" #include "code\__DEFINES\ai\tourist.dm" +#include "code\__DEFINES\ai\trader.dm" #include "code\__DEFINES\ai\vending.dm" #include "code\__DEFINES\ai\ventcrawling.dm" #include "code\__DEFINES\atmospherics\atmos_core.dm" @@ -821,6 +823,7 @@ #include "code\datums\ai\basic_mobs\basic_ai_behaviors\nearest_targetting.dm" #include "code\datums\ai\basic_mobs\basic_ai_behaviors\pick_up_item.dm" #include "code\datums\ai\basic_mobs\basic_ai_behaviors\run_away_from_target.dm" +#include "code\datums\ai\basic_mobs\basic_ai_behaviors\set_travel_destination.dm" #include "code\datums\ai\basic_mobs\basic_ai_behaviors\step_towards_turf.dm" #include "code\datums\ai\basic_mobs\basic_ai_behaviors\stop_and_stare.dm" #include "code\datums\ai\basic_mobs\basic_ai_behaviors\targeted_mob_ability.dm" @@ -843,6 +846,7 @@ #include "code\datums\ai\basic_mobs\basic_subtrees\mine_walls.dm" #include "code\datums\ai\basic_mobs\basic_subtrees\move_to_cardinal.dm" #include "code\datums\ai\basic_mobs\basic_subtrees\opportunistic_ventcrawler.dm" +#include "code\datums\ai\basic_mobs\basic_subtrees\prepare_travel_to_destination.dm" #include "code\datums\ai\basic_mobs\basic_subtrees\ranged_skirmish.dm" #include "code\datums\ai\basic_mobs\basic_subtrees\run_emote.dm" #include "code\datums\ai\basic_mobs\basic_subtrees\shapechange_ambush.dm" @@ -1208,6 +1212,7 @@ #include "code\datums\components\riding\riding_vehicle.dm" #include "code\datums\components\style\style.dm" #include "code\datums\components\style\style_meter.dm" +#include "code\datums\components\trader\trader.dm" #include "code\datums\diseases\_disease.dm" #include "code\datums\diseases\_MobProcs.dm" #include "code\datums\diseases\adrenal_crisis.dm" @@ -1276,6 +1281,7 @@ #include "code\datums\elements\ai_flee_while_injured.dm" #include "code\datums\elements\ai_held_item.dm" #include "code\datums\elements\ai_retaliate.dm" +#include "code\datums\elements\ai_swap_combat_mode.dm" #include "code\datums\elements\ai_target_damagesource.dm" #include "code\datums\elements\amputating_limbs.dm" #include "code\datums\elements\animal_variety.dm" @@ -4577,6 +4583,11 @@ #include "code\modules\mob\living\basic\space_fauna\wumborian_fugu\inflation.dm" #include "code\modules\mob\living\basic\space_fauna\wumborian_fugu\wumborian_ai.dm" #include "code\modules\mob\living\basic\space_fauna\wumborian_fugu\wumborian_fugu.dm" +#include "code\modules\mob\living\basic\trader\trader.dm" +#include "code\modules\mob\living\basic\trader\trader_actions.dm" +#include "code\modules\mob\living\basic\trader\trader_ai.dm" +#include "code\modules\mob\living\basic\trader\trader_data.dm" +#include "code\modules\mob\living\basic\trader\trader_items.dm" #include "code\modules\mob\living\basic\trooper\nanotrasen.dm" #include "code\modules\mob\living\basic\trooper\pirate.dm" #include "code\modules\mob\living\basic\trooper\russian.dm" @@ -4803,7 +4814,6 @@ #include "code\modules\mob\living\simple_animal\hostile\mining_mobs\elites\pandora.dm" #include "code\modules\mob\living\simple_animal\hostile\retaliate\goose.dm" #include "code\modules\mob\living\simple_animal\hostile\retaliate\retaliate.dm" -#include "code\modules\mob\living\simple_animal\hostile\retaliate\trader.dm" #include "code\modules\mob\living\simple_animal\slime\death.dm" #include "code\modules\mob\living\simple_animal\slime\emote.dm" #include "code\modules\mob\living\simple_animal\slime\life.dm" diff --git a/tools/UpdatePaths/Scripts/79187_simple_to_basic_traders.txt b/tools/UpdatePaths/Scripts/79187_simple_to_basic_traders.txt new file mode 100644 index 00000000000..77731b53c5d --- /dev/null +++ b/tools/UpdatePaths/Scripts/79187_simple_to_basic_traders.txt @@ -0,0 +1,2 @@ +/mob/living/simple_animal/hostile/retaliate/trader : /mob/living/basic/trader{@OLD} +/mob/living/simple_animal/hostile/retaliate/trader/mrbones : /mob/living/basic/trader/mrbones{@OLD} \ No newline at end of file