diff --git a/code/modules/cargo/materials_market.dm b/code/modules/cargo/materials_market.dm index e2bedd2f19a..22ab4f2239c 100644 --- a/code/modules/cargo/materials_market.dm +++ b/code/modules/cargo/materials_market.dm @@ -1,3 +1,8 @@ +/// The maximum number of stacks you can place in 1 order +#define MAX_STACK_LIMIT 10 +/// The order rank for all galactic material market orders +#define GALATIC_MATERIAL_ORDER "Galactic Materials Market" + /obj/machinery/materials_market name = "galactic materials market" desc = "This machine allows the user to buy and sell sheets of minerals \ @@ -22,8 +27,6 @@ ) /// Are we ordering sheets from our own card balance or the cargo budget? var/ordering_private = TRUE - /// Currently, can we order sheets from our own card balance or the cargo budget? - var/can_buy_via_budget = FALSE /obj/machinery/materials_market/update_icon_state() if(panel_open) @@ -36,15 +39,21 @@ return ..() /obj/machinery/materials_market/wrench_act(mob/living/user, obj/item/tool) - ..() - default_unfasten_wrench(user, tool, time = 1.5 SECONDS) - return TOOL_ACT_TOOLTYPE_SUCCESS + . = ..() + if(default_unfasten_wrench(user, tool, time = 1.5 SECONDS) == SUCCESSFUL_UNFASTEN) + return TOOL_ACT_TOOLTYPE_SUCCESS + +/obj/machinery/materials_market/screwdriver_act(mob/living/user, obj/item/tool) + . = ..() + if(default_deconstruction_screwdriver(user, "[base_icon_state]_open", "[base_icon_state]", tool)) + return TOOL_ACT_TOOLTYPE_SUCCESS + +/obj/machinery/materials_market/crowbar_act(mob/living/user, obj/item/tool) + . = ..() + if(default_deconstruction_crowbar(tool)) + return TOOL_ACT_TOOLTYPE_SUCCESS /obj/machinery/materials_market/attackby(obj/item/O, mob/user, params) - if(default_deconstruction_screwdriver(user, "[base_icon_state]_open", "[base_icon_state]", O)) - return - else if(default_deconstruction_crowbar(O)) - return if(is_type_in_list(O, exportable_material_items)) var/amount = 0 var/value = 0 @@ -62,6 +71,7 @@ playsound(src, 'sound/machines/scanbuzz.ogg', 25, FALSE) return TRUE qdel(exportable) + var/obj/item/stock_block/new_block = new /obj/item/stock_block(drop_location()) new_block.export_value = amount * value * MARKET_PROFIT_MODIFIER new_block.export_mat = material_to_export @@ -71,6 +81,21 @@ return TRUE return ..() +/** + * Find the order purchased either privately or by cargo budget + * Arguments + * * [user][mob] - the user who placed this order + * * is_ordering_private - is the player ordering privatly. If FALSE it means they are using cargo budget + */ +/obj/machinery/materials_market/proc/find_order(mob/user, is_ordering_private) + for(var/datum/supply_order/order in SSshuttle.shopping_list) + // Must be a Galactic Materials Market order and payed by the null account(if ordered via cargo budget) or by correct user for private purchase + if(order.orderer_rank == GALATIC_MATERIAL_ORDER && ( \ + (!is_ordering_private && isnull(order.paying_account)) || \ + (is_ordering_private && !isnull(order.paying_account) && order.orderer == user) \ + )) + return order + return null /obj/machinery/materials_market/ui_interact(mob/user, datum/tgui/ui) ui = SStgui.try_update_ui(user, src, ui) @@ -81,36 +106,69 @@ ui.open() /obj/machinery/materials_market/ui_data(mob/user) - var/data = list() + . = list() + + //can this player use cargo budget + var/can_buy_via_budget = FALSE + var/obj/item/card/id/used_id_card + if(isliving(user)) + var/mob/living/living_user = user + used_id_card = living_user.get_idcard(TRUE) + can_buy_via_budget = (ACCESS_CARGO in used_id_card?.GetAccess()) + + //if no cargo access then force private purchase + var/is_ordering_private = ordering_private || !can_buy_via_budget + + //find current order based on ordering mode & player + var/datum/supply_order/current_order = find_order(user, is_ordering_private) + var/material_data + var/trend_string + var/color_string + var/sheet_to_buy + var/requested_amount for(var/datum/material/traded_mat as anything in SSstock_market.materials_prices) - var/trend_string = "" - if(SSstock_market.materials_trends[traded_mat] == 0) - trend_string = "neutral" - else if(SSstock_market.materials_trends[traded_mat] == 1) - trend_string = "up" - else if(SSstock_market.materials_trends[traded_mat] == -1) - trend_string = "down" - var/color_string = "" - if (initial(traded_mat.greyscale_colors)) - color_string = splicetext(initial(traded_mat.greyscale_colors), 7, length(initial(traded_mat.greyscale_colors)), "") //slice it to a standard 6 char hex - else if(initial(traded_mat.color)) - color_string = initial(traded_mat.color) + //convert trend into text + switch(SSstock_market.materials_trends[traded_mat]) + if(0) + trend_string = "neutral" + if(1) + trend_string = "up" + else + trend_string = "down" + + //get mat color + var/initial_colors = initial(traded_mat.greyscale_colors) + if(initial_colors) + color_string = splicetext(initial_colors, 7, length(initial_colors), "") //slice it to a standard 6 char hex + else + initial_colors = initial(traded_mat.color) + if(initial_colors) + color_string = initial_colors + else + color_string = COLOR_CYAN + + //get sheet type from material + sheet_to_buy = initial(traded_mat.sheet_type) + if(!sheet_to_buy) + CRASH("Material with no sheet type being sold on materials market!") + + //get the ordered amount from the order + requested_amount = 0 + if(!isnull(current_order)) + requested_amount = current_order.pack.contains[sheet_to_buy] + + //send data material_data += list(list( "name" = initial(traded_mat.name), "price" = SSstock_market.materials_prices[traded_mat], "quantity" = SSstock_market.materials_quantity[traded_mat], "trend" = trend_string, "color" = color_string, + "requested" = requested_amount )) - can_buy_via_budget = FALSE - var/obj/item/card/id/used_id_card - if(isliving(user)) - var/mob/living/living_user = user - used_id_card = living_user.get_idcard(TRUE) - can_buy_via_budget = (ACCESS_CARGO in used_id_card?.GetAccess()) - + //get account balance var/balance = 0 if(!ordering_private) var/datum/bank_account/dept = SSeconomy.get_dep_account(ACCOUNT_CAR) @@ -119,16 +177,23 @@ else balance = used_id_card?.registered_account?.account_balance + //is market crashing var/market_crashing = FALSE if(HAS_TRAIT(SSeconomy, TRAIT_MARKET_CRASHING)) market_crashing = TRUE - data["catastrophe"] = market_crashing - data["materials"] = material_data - data["creditBalance"] = balance - data["orderingPrive"] = ordering_private - data["canOrderCargo"] = can_buy_via_budget - return data + //get final order cost + var/current_cost = 0 + if(!isnull(current_order)) + current_cost = current_order.get_final_cost() + + //pack data + .["catastrophe"] = market_crashing + .["materials"] = material_data + .["creditBalance"] = balance + .["orderBalance"] = current_cost + .["orderingPrive"] = ordering_private + .["canOrderCargo"] = can_buy_via_budget /obj/machinery/materials_market/ui_act(action, params, datum/tgui/ui, datum/ui_state/state) . = ..() @@ -141,12 +206,19 @@ if(isnull(used_id_card)) say("No ID Found") return + var/can_buy_via_budget = (ACCESS_CARGO in used_id_card?.GetAccess()) + + //if multiple users open the UI some of them may not have the required access so we recheck + var/is_ordering_private = ordering_private + if(!can_buy_via_budget) //no cargo access then force private purchase + is_ordering_private = TRUE switch(action) if("buy") var/material_str = params["material"] var/quantity = text2num(params["quantity"]) + //find material from it's name var/datum/material/material_bought var/obj/item/stack/sheet/sheet_to_buy for(var/datum/material/mat as anything in SSstock_market.materials_prices) @@ -155,12 +227,11 @@ break if(!material_bought) CRASH("Invalid material name passed to materials market!") + sheet_to_buy = initial(material_bought.sheet_type) + if(!sheet_to_buy) + CRASH("Material with no sheet type being sold on materials market!") - //if multiple users open the UI some of them may not have the required access so we recheck - var/is_ordering_private = ordering_private - if(!(ACCESS_CARGO in used_id_card.GetAccess())) //no cargo access then force private purchase - is_ordering_private = TRUE - + //get available bank account for purchasing var/datum/bank_account/account_payable if(is_ordering_private) account_payable = used_id_card.registered_account @@ -170,59 +241,81 @@ say("No bank account detected!") return - sheet_to_buy = initial(material_bought.sheet_type) - if(!sheet_to_buy) - CRASH("Material with no sheet type being sold on materials market!") + //sanity checks for available quantity & budget + if(quantity > SSstock_market.materials_quantity[material_bought]) + say("Not enough materials on the market to purchase!") + return + var/cost = SSstock_market.materials_prices[material_bought] * quantity if(cost > account_payable.account_balance) - to_chat(living_user, span_warning("You don't have enough money to buy that!")) + say("Not enough money to start purchase!") return var/list/things_to_order = list() things_to_order += (sheet_to_buy) things_to_order[sheet_to_buy] = quantity + // We want to count how many stacks of all sheets we're ordering to make sure they don't exceed the limit of 10 // If we already have a custom order on SSshuttle, we should add the things to order to that order - for(var/datum/supply_order/order in SSshuttle.shopping_list) - // Must be a Galactic Materials Market order and payed by the null account(if ordered via cargo budget) or by correct user for private purchase - if(order.orderer_rank == "Galactic Materials Market" && ( \ - (!is_ordering_private && order.paying_account == null) || \ - (is_ordering_private && order.paying_account != null && order.orderer == living_user) \ - )) - // Check if this order exceeded its limit - var/prior_stacks = 0 - for(var/obj/item/stack/sheet/sheet as anything in order.pack.contains) - prior_stacks += ROUND_UP(order.pack.contains[sheet] / 50) - if(prior_stacks >= 10) - to_chat(usr, span_notice("You already have 10 stacks of sheets on order! Please wait for them to arrive before ordering more.")) - playsound(usr, 'sound/machines/synth_no.ogg', 35, FALSE) - return - // Append to this order - order.append_order(things_to_order, cost) + var/datum/supply_order/current_order = find_order(living_user, is_ordering_private) + if(!isnull(current_order)) + // Check if this order exceeded the market limit + var/prior_sheets = current_order.pack.contains[sheet_to_buy] + if(prior_sheets + quantity > SSstock_market.materials_quantity[material_bought] ) + say("There aren't enough sheets on the market! Please wait for more sheets to be traded before adding more.") + playsound(usr, 'sound/machines/synth_no.ogg', 35, FALSE) + return + + // Check if the order exceeded the purchase limit + var/prior_stacks = ROUND_UP(prior_sheets / MAX_STACK_SIZE) + if(prior_stacks >= MAX_STACK_LIMIT) + say("There are already 10 stacks of sheets on order! Please wait for them to arrive before ordering more.") + playsound(usr, 'sound/machines/synth_no.ogg', 35, FALSE) + return + + // Prevents you from ordering more than the available budget + var/datum/bank_account/paying_account = account_payable + if(!isnull(current_order.paying_account)) //order is already being paid by another account + paying_account = current_order.paying_account + if(current_order.get_final_cost() + cost > paying_account.account_balance) + say("Order exceeds available budget! Please send it before purchasing more.") return - //Now we need to add a cargo order for quantity sheets of material_bought.sheet_type + // Finally Append to this order + current_order.append_order(things_to_order, cost) + return TRUE + + //Place a new order var/datum/supply_pack/custom/minerals/mineral_pack = new( purchaser = is_ordering_private ? living_user : "Cargo", \ cost = cost, \ contains = things_to_order, \ ) - var/datum/supply_order/new_order = new( + var/datum/supply_order/materials/new_order = new( pack = mineral_pack, orderer = living_user, - orderer_rank = "Galactic Materials Market", + orderer_rank = GALATIC_MATERIAL_ORDER, orderer_ckey = living_user.ckey, paying_account = is_ordering_private ? account_payable : null, - cost_type = "credit", + cost_type = "cr", can_be_cancelled = FALSE ) say("Thank you for your purchase! It will arrive on the next cargo shuttle!") SSshuttle.shopping_list += new_order - return + return TRUE + if("toggle_budget") if(!can_buy_via_budget) return ordering_private = !ordering_private + return TRUE + + if("clear") + var/datum/supply_order/current_order = find_order(living_user, is_ordering_private) + if(!isnull(current_order)) + SSshuttle.shopping_list -= current_order + qdel(current_order) + return TRUE /obj/item/stock_block name = "stock block" @@ -238,6 +331,11 @@ /// Is this stock block currently updating it's value with the market (aka fluid)? var/fluid = FALSE +/obj/item/stock_block/Initialize(mapload) + . = ..() + addtimer(CALLBACK(src, PROC_REF(value_warning)), 2.5 MINUTES, TIMER_DELETE_ME) + addtimer(CALLBACK(src, PROC_REF(update_value)), 5 MINUTES, TIMER_DELETE_ME) + /obj/item/stock_block/examine(mob/user) . = ..() . += span_notice("\The [src] is worth [export_value] cr, from selling [quantity] sheets of [initial(export_mat?.name)].") @@ -246,11 +344,6 @@ else . += span_notice("\The [src]'s value is still [span_boldnotice("locked in")]. [span_boldnotice("Sell it")] before it's value becomes liquid!") -/obj/item/stock_block/Initialize(mapload) - . = ..() - addtimer(CALLBACK(src, PROC_REF(value_warning)), 2.5 MINUTES) - addtimer(CALLBACK(src, PROC_REF(update_value)), 5 MINUTES) - /obj/item/stock_block/proc/value_warning() visible_message(span_warning("\The [src] is starting to become liquid!")) icon_state = "stock_block_fluid" @@ -266,3 +359,5 @@ update_appearance(UPDATE_ICON_STATE) visible_message(span_warning("\The [src] becomes liquid!")) +#undef MAX_STACK_LIMIT +#undef GALATIC_MATERIAL_ORDER diff --git a/code/modules/cargo/order.dm b/code/modules/cargo/order.dm index 9217446d9b5..58c13afbe59 100644 --- a/code/modules/cargo/order.dm +++ b/code/modules/cargo/order.dm @@ -89,10 +89,10 @@ /datum/supply_order/proc/get_final_cost() var/cost = pack.get_cost() if(applied_coupon) //apply discount price - cost -= (cost * applied_coupon.discount_pct_off) - if(!isnull(paying_account)) //privately purchased means 1.1x the cost + cost *= (1 - applied_coupon.discount_pct_off) + if(paying_account && !pack.goody) //privately purchased and not a goody means 1.1x the cost cost *= 1.1 - return cost + return round(cost) /datum/supply_order/proc/generateRequisition(turf/T) var/obj/item/paper/requisition_paper = new(T) @@ -201,6 +201,12 @@ pack.contains[i] = new_contents[i] pack.cost += cost_increase +//To append cargo crate value to final order cost +/datum/supply_order/materials + +/datum/supply_order/materials/get_final_cost() + return (..() + CARGO_CRATE_VALUE) + #undef MANIFEST_ERROR_CHANCE #undef MANIFEST_ERROR_NAME #undef MANIFEST_ERROR_CONTENTS diff --git a/code/modules/cargo/orderconsole.dm b/code/modules/cargo/orderconsole.dm index b59cee9def7..fa1ace3461f 100644 --- a/code/modules/cargo/orderconsole.dm +++ b/code/modules/cargo/orderconsole.dm @@ -259,7 +259,16 @@ applied_coupon = coupon_check break - var/datum/supply_order/order = new(pack = pack ,orderer = name, orderer_rank = rank, orderer_ckey = ckey, reason = reason, paying_account = account, coupon = applied_coupon, charge_on_purchase = TRUE) //SKYRAT EDIT CHANGE - ORIGINAL: var/datum/supply_order/order = new(pack = pack ,orderer = name, orderer_rank = rank, orderer_ckey = ckey, reason = reason, paying_account = account, coupon = applied_coupon) + var/datum/supply_order/order = new( + pack = pack , + orderer = name, + orderer_rank = rank, + orderer_ckey = ckey, + reason = reason, + paying_account = account, + coupon = applied_coupon, + charge_on_purchase = TRUE, // SKYRAT EDIT ADDITION + ) working_list += order if(self_paid) @@ -280,13 +289,14 @@ continue if(order.department_destination) say("Only the department that ordered this item may cancel it.") - return + return FALSE if(order.applied_coupon) say("Coupon refunded.") order.applied_coupon.forceMove(get_turf(src)) SSshuttle.shopping_list -= order - . = TRUE - break + qdel(order) + return TRUE + return FALSE /** * maps the ordename displayed on the ui to its supply pack id * * order_name - the name of the order diff --git a/code/modules/shuttle/supply.dm b/code/modules/shuttle/supply.dm index cdabfdc4926..8e1cb80adda 100644 --- a/code/modules/shuttle/supply.dm +++ b/code/modules/shuttle/supply.dm @@ -157,42 +157,41 @@ GLOBAL_LIST_INIT(blacklisted_cargo_types, typecacheof(list( var/value = 0 var/purchases = 0 + var/price + var/pack_cost var/list/goodies_by_buyer = list() // if someone orders more than GOODY_FREE_SHIPPING_MAX goodies, we upcharge to a normal crate so they can't carry around 20 combat shotties - var/list/forced_briefcases = list() //SKYRAT EDIT + var/list/rejected_orders = list() //list of all orders that exceeded the available budget and are uncancelable + var/list/forced_briefcases = list() // SKYRAT EDIT ADDITION for(var/datum/supply_order/spawning_order in SSshuttle.shopping_list) if(!empty_turfs.len) break - var/price = spawning_order.pack.get_cost() - if(spawning_order.applied_coupon) - price *= (1 - spawning_order.applied_coupon.discount_pct_off) - - var/datum/bank_account/paying_for_this + price = spawning_order.get_final_cost() //department orders EARN money for cargo, not the other way around - //Skyrat Edit Add + var/datum/bank_account/paying_for_this if(!spawning_order.department_destination && spawning_order.charge_on_purchase) - //Skyrat Edit End if(spawning_order.paying_account) //Someone paid out of pocket paying_for_this = spawning_order.paying_account - var/list/current_buyer_orders = goodies_by_buyer[spawning_order.paying_account] // so we can access the length a few lines down - if(!spawning_order.pack.goody) - price *= 1.1 //TODO make this customizable by the quartermaster - // note this is before we increment, so this is the GOODY_FREE_SHIPPING_MAX + 1th goody to ship. also note we only increment off this step if they successfully pay the fee, so there's no way around it - else if(LAZYLEN(current_buyer_orders) == GOODY_FREE_SHIPPING_MAX) - price += CRATE_TAX - paying_for_this.bank_card_talk("Goody order size exceeds free shipping limit: Assessing [CRATE_TAX] credit S&H fee.") + if(spawning_order.pack.goody) + var/list/current_buyer_orders = goodies_by_buyer[spawning_order.paying_account] + if(LAZYLEN(current_buyer_orders) == GOODY_FREE_SHIPPING_MAX) + price = round(price + CRATE_TAX) + paying_for_this.bank_card_talk("Goody order size exceeds free shipping limit: Assessing [CRATE_TAX] credit S&H fee.") else paying_for_this = SSeconomy.get_dep_account(ACCOUNT_CAR) + if(paying_for_this) if(!paying_for_this.adjust_money(-price, "Cargo: [spawning_order.pack.name]")) if(spawning_order.paying_account) paying_for_this.bank_card_talk("Cargo order #[spawning_order.id] rejected due to lack of funds. Credits required: [price]") + if(!spawning_order.can_be_cancelled) //only if it absolutly cannot be canceled by the player do we cancel it for them + rejected_orders += spawning_order continue - //Skyrat Edit Add - if(spawning_order.paying_account && spawning_order.charge_on_purchase) - //Skyrat Edit End + + pack_cost = spawning_order.pack.get_cost() + if(spawning_order.paying_account && spawning_order.charge_on_purchase) // SKYRAT EDIT CHANGE - ORIGINAL: if(spawning_order.paying_account) paying_for_this = spawning_order.paying_account if(spawning_order.pack.goody) LAZYADD(goodies_by_buyer[spawning_order.paying_account], spawning_order) @@ -202,13 +201,13 @@ GLOBAL_LIST_INIT(blacklisted_cargo_types, typecacheof(list( paying_for_this.bank_card_talk(reciever_message) SSeconomy.track_purchase(paying_for_this, price, spawning_order.pack.name) var/datum/bank_account/department/cargo = SSeconomy.get_dep_account(ACCOUNT_CAR) - cargo.adjust_money(price - spawning_order.pack.get_cost()) //Cargo gets the handling fee - value += spawning_order.pack.get_cost() + cargo.adjust_money(price - pack_cost) //Cargo gets the handling fee + value += pack_cost SSshuttle.shopping_list -= spawning_order SSshuttle.order_history += spawning_order QDEL_NULL(spawning_order.applied_coupon) - if(!spawning_order.pack.goody && !(spawning_order?.paying_account in forced_briefcases)) //we handle goody crates below //SKYRAT EDIT + if(!spawning_order.pack.goody && !(spawning_order?.paying_account in forced_briefcases)) // SKYRAT EDIT CHANGE - ORIGINAL : if(!spawning_order.pack.goody) var/obj/structure/closet/crate = spawning_order.generate(pick_n_take(empty_turfs)) crate.name += " - #[spawning_order.id]" @@ -221,6 +220,11 @@ GLOBAL_LIST_INIT(blacklisted_cargo_types, typecacheof(list( message_admins("\A [spawning_order.pack.name] ordered by [ADMIN_LOOKUPFLW(spawning_order.orderer_ckey)], paid by [from_whom] has shipped.") purchases++ + //clear out all rejected uncancellable orders + for(var/datum/supply_order/rejected_order in rejected_orders) + SSshuttle.shopping_list -= rejected_order + qdel(rejected_order) + // we handle packing all the goodies last, since the type of crate we use depends on how many goodies they ordered. If it's more than GOODY_FREE_SHIPPING_MAX // then we send it in a crate (including the CRATE_TAX cost), otherwise send it in a free shipping case for(var/buyer_key in goodies_by_buyer) diff --git a/tgui/packages/tgui/interfaces/MatMarket.tsx b/tgui/packages/tgui/interfaces/MatMarket.tsx index da9f4535911..38ecfec2c18 100644 --- a/tgui/packages/tgui/interfaces/MatMarket.tsx +++ b/tgui/packages/tgui/interfaces/MatMarket.tsx @@ -3,22 +3,24 @@ import { Section, Stack, Button, Modal } from '../components'; import { Window } from '../layouts'; import { BooleanLike } from 'common/react'; import { toTitleCase } from 'common/string'; - -type Data = { - orderingPrive: BooleanLike; // you will need to import this - canOrderCargo: BooleanLike; - creditBalance: number; - materials: Material[]; - catastrophe: BooleanLike; -}; +import { formatMoney } from '../format'; type Material = { name: string; quantity: number; - id: string; // correct this if its a number trend: string; price: number; color: string; + requested: number; +}; + +type Data = { + orderingPrive: BooleanLike; + canOrderCargo: BooleanLike; + creditBalance: number; + orderBalance: number; + materials: Material[]; + catastrophe: BooleanLike; }; export const MatMarket = (props, context) => { @@ -28,11 +30,12 @@ export const MatMarket = (props, context) => { orderingPrive, canOrderCargo, creditBalance, + orderBalance, materials = [], catastrophe, - } = data; // better to destructure here (style nit) + } = data; return ( - + {!!catastrophe && }
{ All new purchases will include the cost of the shipped crate, which may be recycled afterwards.
- Current credit balance: {creditBalance || 'zero'} cr. + + + Current Credit Balance: {formatMoney(creditBalance)} cr. + + + Current Order Cost: {formatMoney(orderBalance)} cr. + + +
- {materials.map((material) => ( -
+ {materials.map((material, i) => ( +
@@ -80,12 +99,13 @@ export const MatMarket = (props, context) => { - Trading at {material.price} cr. + Trading at {formatMoney(material.price)} cr. - - {material.quantity} sheets of {material.name}{' '} - trading. + + {material.quantity || 'zero'} sheets of{' '} + {material.name} trading.{' '} + {material.requested || 'zero'} sheets ordered. { + } + /> + } + /> + } + /> + } + /> + } + /> + {material.requested > 0 && ( + x {material.requested} + )}
))} @@ -167,7 +215,6 @@ export const MatMarket = (props, context) => { }; const MarketCrashModal = (props, context) => { - const { act, data } = useBackend(context); return ( ATTENTION! THE MARKET HAS CRASHED