code:
// ********************************************************
// Here's all the seeds (plants) that can be used in hydro
// ********************************************************

/obj/item/seeds
	icon = 'icons/obj/hydroponics/seeds.dmi'
	icon_state = "seed" // Unknown plant seed - these shouldn't exist in-game.
	worn_icon_state = "seed"
	w_class = WEIGHT_CLASS_TINY
	resistance_flags = FLAMMABLE
	/// Name of plant when planted.
	var/plantname = "Plants"
	/// A type path. The thing that is created when the plant is harvested.
	var/obj/item/product
	///Describes the product on the product path.
	var/productdesc
	/// Used to update icons. Should match the name in the sprites unless all icon_* are overridden.
	var/species = ""
	///the file that stores the sprites of the growing plant from this seed.
	var/growing_icon = 'icons/obj/hydroponics/growing.dmi'
	/// Used to override grow icon (default is `"[species]-grow"`). You can use one grow icon for multiple closely related plants with it.
	var/icon_grow
	/// Used to override dead icon (default is `"[species]-dead"`). You can use one dead icon for multiple closely related plants with it.
	var/icon_dead
	/// Used to override harvest icon (default is `"[species]-harvest"`). If null, plant will use `[icon_grow][growthstages]`.
	var/icon_harvest
	/// How long before the plant begins to take damage from age.
	var/lifespan = 25
	/// Amount of health the plant has.
	var/endurance = 15
	/// Used to determine which sprite to switch to when growing.
	var/maturation = 6
	/// Changes the amount of time needed for a plant to become harvestable.
	var/production = 6
	/// Amount of growns created per harvest. If is -1, the plant/shroom/weed is never meant to be harvested.
	var/yield = 3
	/// The 'power' of a plant. Generally effects the amount of reagent in a plant, also used in other ways.
	var/potency = 10
	/// Amount of growth sprites the plant has.
	var/growthstages = 6
	// Chance that a plant will mutate in each stage of it's life.
	var/instability = 5
	/// How rare the plant is. Used for giving points to cargo when shipping off to CentCom.
	var/rarity = 0
	/// The type of plants that this plant can mutate into.
	var/list/mutatelist
	/// Starts as a list of paths, is converted to a list of types on init. Plant gene datums are stored here, see plant_genes.dm for more info.
	var/list/genes = list()
	/// A list of reagents to add to product.
	var/list/reagents_add
	// Format: "reagent_id" = potency multiplier
	// Stronger reagents must always come first to avoid being displaced by weaker ones.
	// Total amount of any reagent in plant is calculated by formula: max(round(potency * multiplier), 1)
	///If the chance below passes, then this many weeds sprout during growth
	var/weed_rate = 1
	///Percentage chance per tray update to grow weeds
	var/weed_chance = 5
	///Determines if the plant has had a graft removed or not.
	var/grafted = FALSE
	///Type-path of trait to be applied when grafting a plant.
	var/graft_gene
	///Determines if the plant should be allowed to mutate early at 30+ instability.
	var/seed_flags = MUTATE_EARLY

/obj/item/seeds/Initialize(mapload, nogenes = FALSE)
	. = ..()
	pixel_x = base_pixel_x + rand(-8, 8)
	pixel_y = base_pixel_y + rand(-8, 8)

	if(!icon_grow)
		icon_grow = "[species]-grow"

	if(!icon_dead)
		icon_dead = "[species]-dead"

	if(!icon_harvest && !get_gene(/datum/plant_gene/trait/plant_type/fungal_metabolism) && yield != -1)
		icon_harvest = "[species]-harvest"

	if(!nogenes)
		for(var/plant_gene in genes)
			if(ispath(plant_gene))
				genes -= plant_gene
				genes += new plant_gene

		// Go through all traits in their genes and call on_new_seed from them.
		for(var/datum/plant_gene/trait/traits in genes)
			traits.on_new_seed(src)

		for(var/reag_id in reagents_add)
			genes += new /datum/plant_gene/reagent(reag_id, reagents_add[reag_id])
		reagents_from_genes() //quality coding

/obj/item/seeds/Destroy()
	// No AS ANYTHING here, because the list/genes could have typepaths in it.
	for(var/datum/plant_gene/gene in genes)
		gene.on_removed(src)
		qdel(gene)

	genes.Cut()
	return ..()

/obj/item/seeds/examine(mob/user)
	. = ..()
	. += span_notice("Use a pen on it to rename it or change its description.")
	if(reagents_add && user.can_see_reagents())
		. += span_notice("- Plant Reagents -")
		for(var/datum/plant_gene/reagent/reagent_gene in genes)
			. += span_notice("- [reagent_gene.get_name()] -")

/// Copy all the variables from one seed to a new instance of the same seed and return it.
/obj/item/seeds/proc/Copy()
	var/obj/item/seeds/copy_seed = new type(null, TRUE)
	// Copy all the stats
	copy_seed.lifespan = lifespan
	copy_seed.endurance = endurance
	copy_seed.maturation = maturation
	copy_seed.production = production
	copy_seed.yield = yield
	copy_seed.potency = potency
	copy_seed.instability = instability
	copy_seed.weed_rate = weed_rate
	copy_seed.weed_chance = weed_chance
	copy_seed.name = name
	copy_seed.plantname = plantname
	copy_seed.desc = desc
	copy_seed.productdesc = productdesc
	copy_seed.genes = list()
	for(var/datum/plant_gene/gene in genes)
		var/datum/plant_gene/copied_gene = gene.Copy()
		copy_seed.genes += copied_gene
		copied_gene.on_new_seed(copy_seed)

	copy_seed.reagents_add = reagents_add.Copy() // Faster than grabbing the list from genes.
	return copy_seed

/obj/item/seeds/proc/get_gene(typepath)
	return (locate(typepath) in genes)

/obj/item/seeds/proc/reagents_from_genes()
	reagents_add = list()
	for(var/datum/plant_gene/reagent/R in genes)
		reagents_add[R.reagent_id] = R.rate

/obj/item/seeds/proc/mutate(lifemut = 2, endmut = 5, productmut = 1, yieldmut = 2, potmut = 25, wrmut = 2, wcmut = 5, traitmut = 0, stabmut = 3)
	adjust_lifespan(rand(-lifemut,lifemut))
	adjust_endurance(rand(-endmut,endmut))
	adjust_production(rand(-productmut,productmut))
	adjust_yield(rand(-yieldmut,yieldmut))
	adjust_potency(rand(-potmut,potmut))
	adjust_instability(rand(-stabmut,stabmut))
	adjust_weed_rate(rand(-wrmut, wrmut))
	adjust_weed_chance(rand(-wcmut, wcmut))
	if(prob(traitmut))
		if(prob(50))
			add_random_traits(1, 1)
		else
			add_random_reagents(1, 1)

/obj/item/seeds/bullet_act(obj/projectile/Proj) //Works with the Somatoray to modify plant variables.
	if(istype(Proj, /obj/projectile/energy/florayield))
		var/rating = 1
		if(istype(loc, /obj/machinery/hydroponics))
			var/obj/machinery/hydroponics/H = loc
			rating = H.rating

		if(yield == 0)//Oh god don't divide by zero you'll doom us all.
			adjust_yield(1 * rating)
		else if(prob(1/(yield * yield) * 100))//This formula gives you diminishing returns based on yield. 100% with 1 yield, decreasing to 25%, 11%, 6, 4, 2...
			adjust_yield(1 * rating)
	else
		return ..()

// Harvest procs
/obj/item/seeds/proc/getYield()
	var/return_yield = yield

	var/obj/machinery/hydroponics/parent = loc
	if(istype(loc, /obj/machinery/hydroponics))
		if(parent.yieldmod == 0)
			return_yield = min(return_yield, 1)//1 if above zero, 0 otherwise
		else
			return_yield *= (parent.yieldmod)

	return return_yield

/obj/item/seeds/proc/harvest(mob/user)
	///Reference to the tray/soil the seeds are planted in.
	var/obj/machinery/hydroponics/parent = loc //for ease of access
	///Count used for creating the correct amount of results to the harvest.
	var/t_amount = 0
	///List of plants all harvested from the same batch.
	var/list/result = list()
	///Tile of the harvester to deposit the growables.
	var/output_loc = parent.Adjacent(user) ? user.loc : parent.loc //needed for TK
	///Name of the grown products.
	var/product_name
	///The Number of products produced by the plant, typically the yield. Modified by certain traits.
	var/product_count = getYield()

	while(t_amount < product_count)
		var/obj/item/food/grown/t_prod
		if(instability >= 30 && (seed_flags & MUTATE_EARLY) && LAZYLEN(mutatelist) && prob(instability/3))
			var/obj/item/seeds/mutated_seed = pick(mutatelist)
			t_prod = initial(mutated_seed.product)
			if(!t_prod)
				continue
			mutated_seed = new mutated_seed
			for(var/datum/plant_gene/trait/trait in parent.myseed.genes)
				if((trait.mutability_flags & PLANT_GENE_MUTATABLE) && trait.can_add(mutated_seed))
					mutated_seed.genes += trait
			t_prod = new t_prod(output_loc, mutated_seed)
			t_prod.transform = initial(t_prod.transform)
			t_prod.transform *= TRANSFORM_USING_VARIABLE(t_prod.seed.potency, 100) + 0.5
			ADD_TRAIT(t_prod, TRAIT_PLANT_WILDMUTATE, user)
			t_amount++
			if(t_prod.seed)
				t_prod.seed.set_instability(round(instability * 0.5))
			continue
		else
			t_prod = new product(output_loc, src)
		if(parent.myseed.plantname != initial(parent.myseed.plantname))
			t_prod.name = lowertext(parent.myseed.plantname)
		if(productdesc)
			t_prod.desc = productdesc
		t_prod.seed.name = parent.myseed.name
		t_prod.seed.desc = parent.myseed.desc
		t_prod.seed.plantname = parent.myseed.plantname
		result.Add(t_prod) // User gets a consumable
		if(!t_prod)
			return
		t_amount++
		product_name = parent.myseed.plantname
	if(product_count >= 1)
		SSblackbox.record_feedback("tally", "food_harvested", product_count, product_name)
	parent.update_tray(user)

	return result

/**
 * This is where plant chemical products are handled.
 *
 * Individually, the formula for individual amounts of chemicals is Potency * the chemical production %, rounded to the fullest 1.
 * Specific chem handling is also handled here, like bloodtype, food taste within nutriment, and the auto-distilling/autojuicing traits.
 * This is where chemical reactions can occur, and the heating / cooling traits effect the reagent container.
 */
/obj/item/seeds/proc/prepare_result(obj/item/T)
	if(!T.reagents)
		CRASH("[T] has no reagents.")
	var/reagent_max = 0
	for(var/rid in reagents_add)
		reagent_max += reagents_add[rid]
	if(IS_EDIBLE(T) || istype(T, /obj/item/grown))
		var/obj/item/food/grown/grown_edible = T
		for(var/rid in reagents_add)
			var/reagent_overflow_mod = reagents_add[rid]
			if(reagent_max > 1)
				reagent_overflow_mod = (reagents_add[rid]/ reagent_max)
			var/edible_vol = grown_edible.reagents ? grown_edible.reagents.maximum_volume : 0
			var/amount = max(1, round((edible_vol)*(potency/100) * reagent_overflow_mod, 1)) //the plant will always have at least 1u of each of the reagents in its reagent production traits
			var/list/data
			if(rid == /datum/reagent/blood) // Hack to make blood in plants always O-
				data = list("blood_type" = "O-")
			if(istype(grown_edible) && (rid == /datum/reagent/consumable/nutriment || rid == /datum/reagent/consumable/nutriment/vitamin))
				data = grown_edible.tastes // apple tastes of apple.
				//Handles the distillary trait, swaps nutriment and vitamins for that species brewable if it exists.
				if(get_gene(/datum/plant_gene/trait/brewing) && grown_edible.distill_reagent)
					T.reagents.add_reagent(grown_edible.distill_reagent, amount/2)
					continue

			T.reagents.add_reagent(rid, amount, data)

		//Handles the juicing trait, swaps nutriment and vitamins for that species various juices if they exist. Mutually exclusive with distilling.
		if(get_gene(/datum/plant_gene/trait/juicing) && grown_edible.juice_results)
			grown_edible.on_juice()
			grown_edible.reagents.add_reagent_list(grown_edible.juice_results)

		/// The number of nutriments we have inside of our plant, for use in our heating / cooling genes
		var/num_nutriment = T.reagents.get_reagent_amount(/datum/reagent/consumable/nutriment)

		// Heats up the plant's contents by 25 kelvin per 1 unit of nutriment. Mutually exclusive with cooling.
		if(get_gene(/datum/plant_gene/trait/chem_heating))
			T.visible_message(span_notice("[T] releases freezing air, consuming its nutriments to heat its contents."))
			T.reagents.remove_all_type(/datum/reagent/consumable/nutriment, num_nutriment, strict = TRUE)
			T.reagents.chem_temp = min(1000, (T.reagents.chem_temp + num_nutriment * 25))
			T.reagents.handle_reactions()
			playsound(T.loc, 'sound/effects/wounds/sizzle2.ogg', 5)
		// Cools down the plant's contents by 5 kelvin per 1 unit of nutriment. Mutually exclusive with heating.
		else if(get_gene(/datum/plant_gene/trait/chem_cooling))
			T.visible_message(span_notice("[T] releases a blast of hot air, consuming its nutriments to cool its contents."))
			T.reagents.remove_all_type(/datum/reagent/consumable/nutriment, num_nutriment, strict = TRUE)
			T.reagents.chem_temp = max(3, (T.reagents.chem_temp + num_nutriment * -5))
			T.reagents.handle_reactions()
			playsound(T.loc, 'sound/effects/space_wind.ogg', 50)

/// Setters procs ///

/**
 * Adjusts seed yield up or down according to adjustamt. (Max 10)
 */
/obj/item/seeds/proc/adjust_yield(adjustamt)
	if(yield == -1) // Unharvestable shouldn't suddenly turn harvestable
		return

	var/max_yield = MAX_PLANT_YIELD
	var/min_yield = 0
	for(var/datum/plant_gene/trait/trait in genes)
		if(trait.trait_flags & TRAIT_HALVES_YIELD)
			max_yield = round(max_yield/2)
			break
	if(get_gene(/datum/plant_gene/trait/plant_type/fungal_metabolism))
		min_yield = FUNGAL_METAB_YIELD_MIN

	yield = clamp(yield + adjustamt, min_yield, max_yield)

/**
 * Adjusts seed lifespan up or down according to adjustamt. (Max 100)
 */
/obj/item/seeds/proc/adjust_lifespan(adjustamt)
	lifespan = clamp(lifespan + adjustamt, 10, MAX_PLANT_LIFESPAN)

/**
 * Adjusts seed endurance up or down according to adjustamt. (Max 100)
 */
/obj/item/seeds/proc/adjust_endurance(adjustamt)
	endurance = clamp(endurance + adjustamt, MIN_PLANT_ENDURANCE, MAX_PLANT_ENDURANCE)

/**
 * Adjusts seed production seed up or down according to adjustamt. (Max 10)
 */
/obj/item/seeds/proc/adjust_production(adjustamt)
	if(yield == -1)
		return
	production = clamp(production + adjustamt, 1, MAX_PLANT_PRODUCTION)

/**
 * Adjusts seed potency up or down according to adjustamt. (Max 100)
 */
/obj/item/seeds/proc/adjust_potency(adjustamt)
	if(potency == -1)
		return
	potency = clamp(potency + adjustamt, 0, MAX_PLANT_POTENCY)

/**
 * Adjusts seed instability up or down according to adjustamt. (Max 100)
 */
/obj/item/seeds/proc/adjust_instability(adjustamt)
	if(instability == -1)
		return
	instability = clamp(instability + adjustamt, 0, MAX_PLANT_INSTABILITY)

/**
 * Adjusts seed weed grwoth speed up or down according to adjustamt. (Max 10)
 */
/obj/item/seeds/proc/adjust_weed_rate(adjustamt)
	weed_rate = clamp(weed_rate + adjustamt, 0, MAX_PLANT_WEEDRATE)

/**
 * Adjusts seed weed chance up or down according to adjustamt. (Max 67%)
 */
/obj/item/seeds/proc/adjust_weed_chance(adjustamt)
	weed_chance = clamp(weed_chance + adjustamt, 0, MAX_PLANT_WEEDCHANCE)

//Directly setting stats

/**
 * Sets the plant's yield stat to the value of adjustamt. (Max 10, or 5 with some traits)
 */
/obj/item/seeds/proc/set_yield(adjustamt)
	if(yield == -1) // Unharvestable shouldn't suddenly turn harvestable
		return

	var/max_yield = MAX_PLANT_YIELD
	var/min_yield = 0
	for(var/datum/plant_gene/trait/trait in genes)
		if(trait.trait_flags & TRAIT_HALVES_YIELD)
			max_yield = round(max_yield/2)
			break
	if(get_gene(/datum/plant_gene/trait/plant_type/fungal_metabolism))
		min_yield = FUNGAL_METAB_YIELD_MIN

	yield = clamp(adjustamt, min_yield, max_yield)

/**
 * Sets the plant's lifespan stat to the value of adjustamt. (Max 100)
 */
/obj/item/seeds/proc/set_lifespan(adjustamt)
	lifespan = clamp(adjustamt, 10, MAX_PLANT_LIFESPAN)

/**
 * Sets the plant's endurance stat to the value of adjustamt. (Max 100)
 */
/obj/item/seeds/proc/set_endurance(adjustamt)
	endurance = clamp(adjustamt, MIN_PLANT_ENDURANCE, MAX_PLANT_ENDURANCE)

/**
 * Sets the plant's production stat to the value of adjustamt. (Max 10)
 */
/obj/item/seeds/proc/set_production(adjustamt)
	if(yield == -1)
		return
	production = clamp(adjustamt, 1, MAX_PLANT_PRODUCTION)

/**
 * Sets the plant's potency stat to the value of adjustamt. (Max 100)
 */
/obj/item/seeds/proc/set_potency(adjustamt)
	if(potency == -1)
		return
	potency = clamp(adjustamt, 0, MAX_PLANT_POTENCY)

/**
 * Sets the plant's instability stat to the value of adjustamt. (Max 100)
 */
/obj/item/seeds/proc/set_instability(adjustamt)
	if(instability == -1)
		return
	instability = clamp(adjustamt, 0, MAX_PLANT_INSTABILITY)

/**
 * Sets the plant's weed production rate to the value of adjustamt. (Max 10)
 */
/obj/item/seeds/proc/set_weed_rate(adjustamt)
	weed_rate = clamp(adjustamt, 0, MAX_PLANT_WEEDRATE)

/**
 * Sets the plant's weed growth percentage to the value of adjustamt. (Max 67%)
 */
/obj/item/seeds/proc/set_weed_chance(adjustamt)
	weed_chance = clamp(adjustamt, 0, MAX_PLANT_WEEDCHANCE)

/**
 * Override for seeds with unique text for their analyzer. (No newlines at the start or end of unique text!)
 * Returns null if no unique text, or a string of text if there is.
 */
/obj/item/seeds/proc/get_unique_analyzer_text()
	return null

/**
 * Override for seeds with special chem reactions.
 */
/obj/item/seeds/proc/on_chem_reaction(datum/reagents/reagents)
	return

/obj/item/seeds/attackby(obj/item/O, mob/user, params)
	if(istype(O, /obj/item/pen))
		var/choice = tgui_input_list(usr, "What would you like to change?",, list("Plant Name", "Seed Description", "Product Description", "Cancel"))
		if(!user.canUseTopic(src, BE_CLOSE))
			return
		switch(choice)
			if("Plant Name")
				var/newplantname = reject_bad_text(stripped_input(user, "Write a new plant name:", name, plantname))
				if(!user.canUseTopic(src, BE_CLOSE))
					return
				if (length(newplantname) > 20)
					to_chat(user, span_warning("That name is too long!"))
					return
				if(!newplantname)
					to_chat(user, span_warning("That name is invalid."))
					return
				else
					name = "[lowertext(newplantname)]"
					plantname = newplantname
			if("Seed Description")
				var/newdesc = stripped_input(user, "Write a new description:", name, desc)
				if(!user.canUseTopic(src, BE_CLOSE))
					return
				if (length(newdesc) > 180)
					to_chat(user, span_warning("That description is too long!"))
					return
				if(!newdesc)
					to_chat(user, span_warning("That description is invalid."))
					return
				else
					desc = newdesc
			if("Product Description")
				if(product && !productdesc)
					productdesc = initial(product.desc)
				var/newproductdesc = stripped_input(user, "Write a new description:", name, productdesc)
				if(!user.canUseTopic(src, BE_CLOSE))
					return
				if (length(newproductdesc) > 180)
					to_chat(user, span_warning("That description is too long!"))
					return
				if(!newproductdesc)
					to_chat(user, span_warning("That description is invalid."))
					return
				else
					productdesc = newproductdesc
			else
				return

	..() // Fallthrough to item/attackby() so that bags can pick seeds up

/obj/item/seeds/proc/randomize_stats()
	set_lifespan(rand(25, 60))
	set_endurance(rand(15, 35))
	set_production(rand(2, 10))
	set_yield(rand(1, 10))
	set_potency(rand(10, 35))
	set_weed_rate(rand(1, 10))
	set_weed_chance(rand(5, 100))
	maturation = rand(6, 12)

/obj/item/seeds/proc/add_random_reagents(lower = 0, upper = 2)
	var/amount_random_reagents = rand(lower, upper)
	for(var/i in 1 to amount_random_reagents)
		var/random_amount = rand(4, 15) * 0.01 // this must be multiplied by 0.01, otherwise, it will not properly associate
		var/datum/plant_gene/reagent/R = new(get_random_reagent_id(), random_amount)
		if(R.can_add(src))
			if(!R.try_upgrade_gene(src))
				genes += R
		else
			qdel(R)
	reagents_from_genes()

/obj/item/seeds/proc/add_random_traits(lower = 0, upper = 2)
	var/amount_random_traits = rand(lower, upper)
	for(var/i in 1 to amount_random_traits)
		var/random_trait = pick(subtypesof(/datum/plant_gene/trait))
		var/datum/plant_gene/trait/picked_random_trait = new random_trait
		if((picked_random_trait.mutability_flags & PLANT_GENE_MUTATABLE) && picked_random_trait.can_add(src))
			genes += picked_random_trait
		else
			qdel(picked_random_trait)

/obj/item/seeds/proc/add_random_plant_type(normal_plant_chance = 75)
	if(prob(normal_plant_chance))
		var/random_plant_type = pick(subtypesof(/datum/plant_gene/trait/plant_type))
		var/datum/plant_gene/trait/plant_type/P = new random_plant_type
		if(P.can_add(src))
			genes += P
		else
			qdel(P)

/obj/item/seeds/proc/remove_random_reagents(lower = 0, upper = 2)
	var/amount_random_reagents = rand(lower, upper)
	for(var/i in 1 to amount_random_reagents)
		var/datum/reagent/chemical = pick(reagents_add)
		qdel(chemical)

/**
 * Creates a graft from this plant.
 *
 * Creates a new graft from this plant.
 * Sets the grafts trait to this plants graftable trait.
 * Gives the graft a reference to this plant.
 * Copies all the relevant stats from this plant to the graft.
 * Returns the created graft.
 */
/obj/item/seeds/proc/create_graft()
	var/obj/item/graft/snip = new(loc, graft_gene)
	snip.parent_name = plantname
	snip.name += "([plantname])"

	// Copy over stats so the graft can outlive its parent.
	snip.lifespan = lifespan
	snip.endurance = endurance
	snip.production = production
	snip.weed_rate = weed_rate
	snip.weed_chance = weed_chance
	snip.yield = yield

	return snip

/**
 * Applies a graft to this plant.
 *
 * Adds the graft trait to this plant if possible.
 * Increases plant stats by 2/3 of the grafts stats to a maximum of 100 (10 for yield).
 * Returns TRUE if the graft could apply its trait successfully, FALSE if it fails to apply the trait.
 * NOTE even if the graft fails to apply the trait it still adjusts the plant's stats and reagents.
 *
 * Arguments:
 * - [snip][/obj/item/graft]: The graft being used applied to this plant.
 */
/obj/item/seeds/proc/apply_graft(obj/item/graft/snip)
	. = TRUE
	var/datum/plant_gene/new_trait = snip.stored_trait
	if(new_trait?.can_add(src))
		genes += new_trait.Copy()
	else
		. = FALSE

	// Adjust stats based on graft stats
	set_lifespan(round(max(lifespan, (lifespan + (2/3)*(snip.lifespan - lifespan)))))
	set_endurance(round(max(endurance, (endurance + (2/3)*(snip.endurance - endurance)))))
	set_production(round(max(production, (production + (2/3)*(snip.production - production)))))
	set_weed_rate(round(max(weed_rate, (weed_rate + (2/3)*(snip.weed_rate - weed_rate)))))
	set_weed_chance(round(max(weed_chance, (weed_chance+ (2/3)*(snip.weed_chance - weed_chance)))))
	set_yield(round(max(yield, (yield + (2/3)*(snip.yield - yield)))))

	// Add in any reagents, too.
	reagents_from_genes()

	return

/*
 * Both `/item/food/grown` and `/item/grown` implement a seed variable which tracks
 * plant statistics, genes, traits, etc. This proc gets the seed for either grown food or
 * grown inedibles and returns it, or returns null if it's not a plant.
 *
 * Returns an `/obj/item/seeds` ref for grown foods or grown inedibles.
 *  - returned seed CAN be null in weird cases but in all applications it SHOULD NOT be.
 * Returns null if it is not a plant.
 */
/obj/item/proc/get_plant_seed()
	return null

/obj/item/food/grown/get_plant_seed()
	return seed

/obj/item/grown/get_plant_seed()
	return seed


code:
// --- Below here are special, unique plant traits that only belong to certain plants. ---
// They are un-removable and cannot be mutated randomly, and should never be graftable.

/// Holymelon's anti-magic trait. Charges based on potency.
/datum/plant_gene/trait/anti_magic
	name = "Anti-Magic Vacuoles"
	/// The amount of anti-magic blocking uses we have.
	var/shield_uses = 1

/datum/plant_gene/trait/anti_magic/on_new_plant(obj/item/our_plant, newloc)
	. = ..()
	if(!.)
		return
	var/obj/item/seeds/our_seed = our_plant.get_plant_seed()
	shield_uses = round(our_seed.potency / 20)
	our_plant.AddComponent(/datum/component/anti_magic, TRUE, TRUE, FALSE, ITEM_SLOT_HANDS, shield_uses, TRUE, CALLBACK(src, .proc/block_magic), CALLBACK(src, .proc/expire)) //deliver us from evil o melon god

/*
 * The proc called when the holymelon successfully blocks a spell.
 *
 * user - the mob who's using the melon
 * major - whether the spell was 'major' or not
 * our_plant - our plant, who's eating the magic spell
 */
/datum/plant_gene/trait/anti_magic/proc/block_magic(mob/user, major, obj/item/our_plant)
	if(major)
		to_chat(user, "[our_plant] hums slightly, and seems to decay a bit.")

/*
 * The proc called when the holymelon uses up all of its anti-magic charges.
 *
 * user - the mob who's using the melon
 * major - whether the spell was 'major' or not
 * our_plant - our plant, who tragically melted protecting us from magics
 */
/datum/plant_gene/trait/anti_magic/proc/expire(mob/user, obj/item/our_plant)
	to_chat(user, "[our_plant] rapidly turns into ash!")
	new /obj/effect/decal/cleanable/ash(our_plant.drop_location())
	qdel(our_plant)

/// Traits that turn a plant into a weapon, giving them force and effects on attack.
/datum/plant_gene/trait/attack
	name = "On Attack Trait"
	/// The multiplier we apply to the potency to calculate force. Set to 0 to not affect the force.
	var/force_multiplier = 0

/datum/plant_gene/trait/attack/on_new_plant(obj/item/our_plant, newloc)
	. = ..()
	if(!.)
		return

	if(force_multiplier)
		var/obj/item/seeds/our_seed = our_plant.get_plant_seed()
		our_plant.force = round((5 + our_seed.potency * force_multiplier), 1)
	RegisterSignal(our_plant, COMSIG_ITEM_ATTACK, .proc/on_plant_attack)
	RegisterSignal(our_plant, COMSIG_ITEM_AFTERATTACK, .proc/after_plant_attack)

/*
 * Plant effects ON attack.
 *
 * our_plant - our plant, that we're attacking with
 * user - the person who is attacking with the plant
 * target - the person who is attacked by the plant
 */
/datum/plant_gene/trait/attack/proc/on_plant_attack(obj/item/our_plant, mob/living/target, mob/living/user)
	SIGNAL_HANDLER

/*
 * Plant effects AFTER attack.
 *
 * our_plant - our plant, that we're attacking with
 * user - the person who is attacking with the plant
 * target - the atom which is attacked by the plant
 */
/datum/plant_gene/trait/attack/proc/after_plant_attack(obj/item/our_plant, atom/target, mob/user)
	SIGNAL_HANDLER

/// Novaflower's attack effects (sets people on fire) + degradation on attack
/datum/plant_gene/trait/attack/novaflower_attack
	name = "Heated Petals"
	force_multiplier = 0.2

/datum/plant_gene/trait/attack/novaflower_attack/on_plant_attack(obj/item/our_plant, mob/living/target, mob/living/user)
	. = ..()

	var/obj/item/seeds/our_seed = our_plant.get_plant_seed()
	to_chat(target, "You are lit on fire from the intense heat of [our_plant]!")
	target.adjust_fire_stacks(our_seed.potency / 20)
	if(target.IgniteMob())
		message_admins("[ADMIN_LOOKUPFLW(user)] set [ADMIN_LOOKUPFLW(target)] on fire with [our_plant] at [AREACOORD(user)]")
		log_game("[key_name(user)] set [key_name(target)] on fire with [our_plant] at [AREACOORD(user)]")
	our_plant.investigate_log("was used by [key_name(user)] to burn [key_name(target)] at [AREACOORD(user)]", INVESTIGATE_BOTANY)

/datum/plant_gene/trait/attack/novaflower_attack/after_plant_attack(obj/item/our_plant, atom/target, mob/user)
	. = ..()

	if(!ismovable(target))
		return
	if(our_plant.force > 0)
		our_plant.force -= rand(1, (our_plant.force / 3) + 1)
	else
		to_chat(user, "All the petals have fallen off [our_plant] from violent whacking!")
		qdel(our_plant)

/// Sunflower's attack effect (shows cute text)
/datum/plant_gene/trait/attack/sunflower_attack
	name = "Bright Petals"

/datum/plant_gene/trait/attack/sunflower_attack/after_plant_attack(obj/item/our_plant, atom/target, mob/living/user)
	. = ..()

	if(!ismob(target))
		return
	var/mob/target_mob = target
	user.visible_message("[user] smacks [target_mob] with [user.p_their()] [our_plant.name]! FLOWER POWER!", ignored_mobs = list(target_mob, user))
	if(target_mob != user)
		to_chat(target_mob, "[user] smacks you with [our_plant]!FLOWER POWER!")
	to_chat(user, "Your [our_plant.name]'s FLOWER POWER strikes [target_mob]!")

/// Normal nettle's force + degradation on attack
/datum/plant_gene/trait/attack/nettle_attack
	name = "Sharpened Leaves"
	force_multiplier = 0.2

/datum/plant_gene/trait/attack/nettle_attack/after_plant_attack(obj/item/our_plant, atom/target, mob/user)
	. = ..()

	if(!ismovable(target))
		return
	if(our_plant.force > 0)
		our_plant.force -= rand(1, (our_plant.force / 3) + 1)
	else
		to_chat(user, "All the leaves have fallen off [our_plant] from violent whacking!")
		qdel(our_plant)

/// Deathnettle force + degradation on attack
/datum/plant_gene/trait/attack/nettle_attack/death
	name = "Aggressive Sharpened Leaves"
	force_multiplier = 0.4

/// Traits for plants with backfire effects. These are negative effects that occur when a plant is handled without gloves/unsafely.
/datum/plant_gene/trait/backfire
	name = "Backfire Trait"
	/// Whether our actions are cancelled when the backfire triggers.
	var/cancel_action_on_backfire = FALSE
	/// A list of extra traits to check to be considered safe.
	var/traits_to_check
	/// A list of extra genes to check to be considered safe.
	var/genes_to_check

/datum/plant_gene/trait/backfire/on_new_plant(obj/item/our_plant, newloc)
	. = ..()
	if(!.)
		return

	our_plant.AddElement(/datum/element/plant_backfire, cancel_action_on_backfire, traits_to_check, genes_to_check)
	RegisterSignal(our_plant, COMSIG_PLANT_ON_BACKFIRE, .proc/backfire_effect)

/*
 * The backfire effect. Override with plant-specific effects.
 *
 * user - the person who is carrying the plant
 * our_plant - our plant
 */
/datum/plant_gene/trait/backfire/proc/backfire_effect(obj/item/our_plant, mob/living/carbon/user)
	SIGNAL_HANDLER

/// Rose's prick on backfire
/datum/plant_gene/trait/backfire/rose_thorns
	name = "Rose Thorns"
	traits_to_check = list(TRAIT_PIERCEIMMUNE)

/datum/plant_gene/trait/backfire/rose_thorns/backfire_effect(obj/item/our_plant, mob/living/carbon/user)
	. = ..()

	var/obj/item/seeds/our_seed = our_plant.get_plant_seed()
	if(!our_seed.get_gene(/datum/plant_gene/trait/sticky) && prob(66))
		to_chat(user, "[our_plant]'s thorns nearly prick your hand. Best be careful.")
		return

	to_chat(user, "[our_plant]'s thorns prick your hand. Ouch.")
	our_plant.investigate_log("rose-pricked [key_name(user)] at [AREACOORD(user)]", INVESTIGATE_BOTANY)
	var/obj/item/bodypart/affecting = user.get_active_hand()
	if(affecting?.receive_damage(2))
		user.update_damage_overlays()

/// Novaflower's hand burn on backfire
/datum/plant_gene/trait/backfire/novaflower_heat
	name = "Burning Stem"
	cancel_action_on_backfire = TRUE

/datum/plant_gene/trait/backfire/novaflower_heat/backfire_effect(obj/item/our_plant, mob/living/carbon/user)
	. = ..()

	to_chat(user, "[our_plant] singes your bare hand!")
	our_plant.investigate_log("self-burned [key_name(user)] for [our_plant.force] at [AREACOORD(user)]", INVESTIGATE_BOTANY)
	var/obj/item/bodypart/affecting = user.get_active_hand()
	if(affecting?.receive_damage(0, our_plant.force, wound_bonus = CANT_WOUND))
		user.update_damage_overlays()

/// Normal Nettle hannd burn on backfire
/datum/plant_gene/trait/backfire/nettle_burn
	name = "Stinging Stem"

/datum/plant_gene/trait/backfire/nettle_burn/backfire_effect(obj/item/our_plant, mob/living/carbon/user)
	. = ..()

	to_chat(user, "[our_plant] burns your bare hand!")
	our_plant.investigate_log("self-burned [key_name(user)] for [our_plant.force] at [AREACOORD(user)]", INVESTIGATE_BOTANY)
	var/obj/item/bodypart/affecting = user.get_active_hand()
	if(affecting?.receive_damage(0, our_plant.force, wound_bonus = CANT_WOUND))
		user.update_damage_overlays()

/// Deathnettle hand burn + stun on backfire
/datum/plant_gene/trait/backfire/nettle_burn/death
	name = "Aggressive Stinging Stem"
	cancel_action_on_backfire = TRUE

/datum/plant_gene/trait/backfire/nettle_burn/death/backfire_effect(obj/item/our_plant, mob/living/carbon/user)
	. = ..()

	if(prob(50))
		user.Paralyze(100)
		to_chat(user, "You are stunned by the powerful acids of [our_plant]!")

/// Ghost-Chili heating up on backfire
/datum/plant_gene/trait/backfire/chili_heat
	name = "Active Capsicum Glands"
	genes_to_check = list(/datum/plant_gene/trait/chem_heating)
	/// The mob currently holding the chili.
	var/datum/weakref/held_mob
	/// The chili this gene is tied to, to track it for processing.
	var/datum/weakref/our_chili

/datum/plant_gene/trait/backfire/chili_heat/on_new_plant(obj/item/our_plant, newloc)
	. = ..()
	if(!.)
		return

	our_chili = WEAKREF(our_plant)
	RegisterSignal(our_plant, list(COMSIG_PARENT_QDELETING, COMSIG_ITEM_DROPPED), .proc/stop_backfire_effect)

/*
 * Begin processing the trait on backfire.
 *
 * our_plant - our source plant, which is backfiring
 * user - the mob holding our plant
 */
/datum/plant_gene/trait/backfire/chili_heat/backfire_effect(obj/item/our_plant, mob/living/carbon/user)
	. = ..()

	held_mob = WEAKREF(user)
	START_PROCESSING(SSobj, src)

/*
 * Stop processing the trait when we're dropped or deleted.
 *
 * our_plant - our source plant
 */
/datum/plant_gene/trait/backfire/chili_heat/proc/stop_backfire_effect(datum/source)
	SIGNAL_HANDLER

	held_mob = null
	STOP_PROCESSING(SSobj, src)

/*
 * The processing of our trait. Heats up the mob ([held_mob]) currently holding the source plant ([our_chili]).
 * Stops processing if we're no longer being held by [held mob].
 */
/datum/plant_gene/trait/backfire/chili_heat/process(delta_time)
	var/mob/living/carbon/our_mob = held_mob?.resolve()
	var/obj/item/our_plant = our_chili?.resolve()

	// If our weakrefs don't resolve, or if our mob is not holding our plant, stop processing.
	if(!our_mob || !our_plant || !our_mob.is_holding(our_plant))
		stop_backfire_effect()
		return

	our_mob.adjust_bodytemperature(7.5 * TEMPERATURE_DAMAGE_COEFFICIENT * delta_time)
	if(DT_PROB(5, delta_time))
		to_chat(our_mob, span_warning("Your hand holding [our_plant] burns!"))

/// Bluespace Tomato squashing on the user on backfire
/datum/plant_gene/trait/backfire/bluespace
	name = "Bluespace Volatility"
	cancel_action_on_backfire = TRUE
	genes_to_check = list(/datum/plant_gene/trait/squash)

/datum/plant_gene/trait/backfire/bluespace/backfire_effect(obj/item/our_plant, mob/living/carbon/user)
	. = ..()

	if(prob(50))
		to_chat(user, "[our_plant] slips out of your hand!")
		INVOKE_ASYNC(our_plant, /obj/item/.proc/attack_self, user)

/// Traits for plants that can be activated to turn into a mob.
/datum/plant_gene/trait/mob_transformation
	name = "Dormat Ferocity"
	trait_ids = ATTACK_SELF_ID
	/// Whether mobs spawned by this trait are dangerous or not.
	var/dangerous = FALSE
	/// The typepath to what mob spawns from this plant.
	var/killer_plant
	/// Whether our attatched plant is currently waking up or not.
	var/awakening = FALSE
	/// Spawned mob's health = this multiplier * seed endurance.
	var/mob_health_multiplier = 1
	/// Spawned mob's melee damage = this multiplier * seed potency.
	var/mob_melee_multiplier = 1
	/// Spawned mob's move delay = this multiplier * seed potency.
	var/mob_speed_multiplier = 1

/datum/plant_gene/trait/mob_transformation/on_new_plant(obj/item/our_plant, newloc)
	. = ..()
	if(!.)
		return

	if(dangerous)
		our_plant.AddElement(/datum/element/plant_backfire, TRUE)
		RegisterSignal(our_plant, COMSIG_PLANT_ON_BACKFIRE, .proc/early_awakening)
	RegisterSignal(our_plant, COMSIG_ITEM_ATTACK_SELF, .proc/manual_awakening)
	RegisterSignal(our_plant, COMSIG_ITEM_PRE_ATTACK, .proc/pre_consumption_check)

/*
 * Before we can eat our plant, check to see if it's waking up. Don't eat it if it is.
 *
 * our_plant - signal source, the plant we're eating
 * target - the mob eating the plant
 * user - the mob feeding someone the plant (generally, target == user)
 */
/datum/plant_gene/trait/mob_transformation/proc/pre_consumption_check(obj/item/our_plant, atom/target, mob/user)
	SIGNAL_HANDLER

	if(!awakening)
		return

	if(!ismob(target))
		return

	if(target != user)
		to_chat(user, "[our_plant] is twitching and shaking, preventing you from feeding it to [target].")
	to_chat(target, "[our_plant] is twitching and shaking, preventing you from eating it.")
	return COMPONENT_CANCEL_ATTACK_CHAIN

/*
 * Called when a user manually activates the plant.
 * Checks if the critera is met to spawn it, and spawns it in 3 seconds if it is.
 *
 * our_plant - our plant that we're waking up
 * user - the mob activating the plant
 */
/datum/plant_gene/trait/mob_transformation/proc/manual_awakening(obj/item/our_plant, mob/user)
	SIGNAL_HANDLER

	if(awakening || isspaceturf(user.loc))
		return

	if(dangerous && HAS_TRAIT(user, TRAIT_PACIFISM))
		to_chat(user, "You decide not to awaken [our_plant]. It may be very dangerous!")
		return

	to_chat(user, "You begin to awaken [our_plant]...")
	begin_awaken(our_plant, 3 SECONDS)
	log_game("[key_name(user)] awakened a [our_plant] at [AREACOORD(user)].")

/*
 * Called when a user accidentally activates the plant via backfire effect.
 *
 * our_plant - our plant, which is waking up
 * user - the mob handling the plant
 */
/datum/plant_gene/trait/mob_transformation/proc/early_awakening(obj/item/our_plant, mob/living/carbon/user)
	SIGNAL_HANDLER

	if(!awakening && !isspaceturf(user.loc) && prob(25))
		to_chat(user, "[our_plant] begins to growl and shake!")
		begin_awaken(our_plant, 1 SECONDS)

/*
 * Actually begin the process of awakening the plant.
 *
 * awaken_time - the time, in seconds, it will take for the plant to spawn.
 */
/datum/plant_gene/trait/mob_transformation/proc/begin_awaken(obj/item/our_plant, awaken_time)
	awakening = TRUE
	addtimer(CALLBACK(src, .proc/awaken, our_plant), awaken_time)

/*
 * Actually awaken the plant, spawning the mob designated by the [killer_plant] typepath.
 *
 * our_plant - the plant that's waking up
 */
/datum/plant_gene/trait/mob_transformation/proc/awaken(obj/item/our_plant)
	if(QDELETED(our_plant))
		return
	if(!ispath(killer_plant))
		return

	var/obj/item/seeds/our_seed = our_plant.get_plant_seed()
	var/mob/living/spawned_mob = new killer_plant(our_plant.drop_location())
	spawned_mob.maxHealth += round(our_seed.endurance * mob_health_multiplier)
	spawned_mob.health = spawned_mob.maxHealth
	if(ishostile(spawned_mob))
		var/mob/living/simple_animal/hostile/spawned_simplemob = spawned_mob
		spawned_simplemob.melee_damage_lower += round(our_seed.potency * mob_melee_multiplier)
		spawned_simplemob.melee_damage_upper += round(our_seed.potency * mob_melee_multiplier)
		spawned_simplemob.move_to_delay -= round(our_seed.production * mob_speed_multiplier)
	our_plant.forceMove(our_plant.drop_location())
	spawned_mob.visible_message("[our_plant] growls as it suddenly awakens!")
	qdel(our_plant)

/// Killer Tomato's transformation gene.
/datum/plant_gene/trait/mob_transformation/tomato
	dangerous = TRUE
	killer_plant = /mob/living/simple_animal/hostile/killertomato
	mob_health_multiplier = 0.33
	mob_melee_multiplier = 0.1
	mob_speed_multiplier = 0.02

/// Walking Mushroom's transformation gene
/datum/plant_gene/trait/mob_transformation/shroom
	killer_plant = /mob/living/simple_animal/hostile/mushroom
	mob_health_multiplier = 0.25
	mob_melee_multiplier = 0.05
	mob_speed_multiplier = 0.02

/// Traiit for plants eaten in 1 bite.
/datum/plant_gene/trait/one_bite
	name = "Large Bites"

/datum/plant_gene/trait/one_bite/on_new_plant(obj/item/our_plant, newloc)
	. = ..()
	if(!.)
		return

	var/obj/item/food/grown/grown_plant = our_plant
	if(istype(grown_plant))
		grown_plant.bite_consumption_mod = 100

/// Traits for plants with a different base max_volume.
/datum/plant_gene/trait/modified_volume
	name = "Deep Vesicles"
	/// The new number we set the plant's max_volume to.
	var/new_capcity = 100

/datum/plant_gene/trait/modified_volume/on_new_plant(obj/item/our_plant, newloc)
	. = ..()
	if(!.)
		return

	var/obj/item/food/grown/grown_plant = our_plant
	if(istype(grown_plant))
		grown_plant.max_volume = new_capcity

/// Omegaweed's funny 420 max volume gene
/datum/plant_gene/trait/modified_volume/omega_weed
	name = "Dank Vesicles"
	new_capcity = 420

/// Cherry Bomb's increased max volume gene
/datum/plant_gene/trait/modified_volume/cherry_bomb
	name = "Powder-Filled Bulbs"
	new_capcity = 125

/// Plants that explode when used (based on their reagent contents)
/datum/plant_gene/trait/bomb_plant
	name = "Explosive Contents"
	trait_ids = ATTACK_SELF_ID

/datum/plant_gene/trait/bomb_plant/on_new_plant(obj/item/our_plant, newloc)
	. = ..()
	if(!.)
		return

	our_plant.max_integrity = 40 // Max_integrity is lowered so they explode better, or something like that.
	RegisterSignal(our_plant, COMSIG_ITEM_ATTACK_SELF, .proc/trigger_detonation)
	RegisterSignal(our_plant, COMSIG_ATOM_EX_ACT, .proc/explosion_reaction)
	RegisterSignal(our_plant, COMSIG_OBJ_DECONSTRUCT, .proc/deconstruct_reaction)

/*
 * Trigger our plant's detonation.
 *
 * our_plant - the plant that's exploding
 * user - the mob detonating the plant
 */
/datum/plant_gene/trait/bomb_plant/proc/trigger_detonation(obj/item/our_plant, mob/living/user)
	SIGNAL_HANDLER

	var/obj/item/seeds/our_seed = our_plant.get_plant_seed()
	var/obj/item/food/grown/grown_plant = our_plant
	// If we have an alt icon, use that to show our plant is exploding.
	if(istype(our_plant) && grown_plant.alt_icon)
		our_plant.icon_state = grown_plant.alt_icon
	else
		our_plant.color = COLOR_RED

	playsound(our_plant, 'sound/effects/fuse.ogg', our_seed.potency, FALSE)
	user.visible_message("[user] plucks the stem from [our_plant]!", "You pluck the stem from [our_plant], which begins to hiss loudly!")
	log_bomber(user, "primed a", our_plant, "for detonation")
	detonate(our_plant)

/*
 * Reacting to when the plant is deconstructed.
 * When is a plant ever deconstructed? Apparently, when it burns.
 *
 * our_plant - the plant that's 'deconstructed'
 * disassembled - if it was disassembled when it was deconstructed.
 */
/datum/plant_gene/trait/bomb_plant/proc/deconstruct_reaction(obj/item/our_plant, disassembled)
	SIGNAL_HANDLER

	if(!disassembled)
		detonate(our_plant)
	if(!QDELETED(our_plant))
		qdel(our_plant)

/*
 * React to explosions that hit the plant.
 * Ensures that the plant id deleted by its own explosion.
 * Also prevents mass chain reaction with piles plants.
 *
 * our_plant - the plant that's exploded on
 * severity - severity of the explosion
 */
/datum/plant_gene/trait/bomb_plant/proc/explosion_reaction(obj/item/our_plant, severity)
	SIGNAL_HANDLER

	qdel(our_plant)

/*
 * RActually blow up the plant.
 *
 * our_plant - the plant that's exploding for real
 */
/datum/plant_gene/trait/bomb_plant/proc/detonate(obj/item/our_plant)
	our_plant.reagents.chem_temp = 1000 //Sets off the gunpowder
	our_plant.reagents.handle_reactions()

/// A subtype of bomb plants that have their boom sized based on potency instead of reagent contents.
/datum/plant_gene/trait/bomb_plant/potency_based
	name = "Explosive Nature"

/datum/plant_gene/trait/bomb_plant/potency_based/trigger_detonation(obj/item/our_plant, mob/living/user)
	user.visible_message("[user] primes [our_plant]!", "You prime [our_plant]!")
	log_bomber(user, "primed a", our_plant, "for detonation")

	var/obj/item/food/grown/grown_plant = our_plant
	if(istype(our_plant) && grown_plant.alt_icon)
		our_plant.icon_state = grown_plant.alt_icon
	else
		our_plant.color = COLOR_RED

	playsound(our_plant.drop_location(), 'sound/weapons/armbomb.ogg', 75, TRUE, -3)
	addtimer(CALLBACK(src, .proc/detonate, our_plant), rand(1 SECONDS, 6 SECONDS))

/datum/plant_gene/trait/bomb_plant/potency_based/detonate(obj/item/our_plant)
	var/obj/item/seeds/our_seed = our_plant.get_plant_seed()
	var/flame_reach = clamp(round(our_seed.potency / 20), 1, 5) //Like IEDs - their flame range can get up to 5, but their real boom is small

	our_plant.forceMove(our_plant.drop_location())
	explosion(our_plant, devastation_range = -1, heavy_impact_range = -1, light_impact_range = 2, flame_range = flame_reach)
	qdel(our_plant)

/// Corpseflower's miasma production.
/// Can be generalized in the future to spawn any gas, but I don't think that's necessarily a good idea.
/datum/plant_gene/trait/gas_production
	name = "Miasma Gas Production"
	/// The location of our tray, if we have one.
	var/datum/weakref/home_tray
	/// The seed emitting gas.
	var/datum/weakref/stinky_seed

/datum/plant_gene/trait/gas_production/on_new_seed(obj/item/seeds/new_seed)
	RegisterSignal(new_seed, COMSIG_SEED_ON_PLANTED, .proc/set_home_tray)
	RegisterSignal(new_seed, COMSIG_SEED_ON_GROW, .proc/try_release_gas)
	RegisterSignal(new_seed, COMSIG_PARENT_QDELETING, .proc/stop_gas)
	stinky_seed = WEAKREF(new_seed)

/datum/plant_gene/trait/gas_production/on_removed(obj/item/seeds/old_seed)
	UnregisterSignal(old_seed, list(COMSIG_PARENT_QDELETING, COMSIG_SEED_ON_PLANTED, COMSIG_SEED_ON_GROW))
	stop_gas()

/*
 * Whenever we're planted, set a new home tray.
 *
 * our_seed - the seed growing
 * grown_tray - the tray we were planted in
 */
/datum/plant_gene/trait/gas_production/proc/set_home_tray(obj/item/seeds/our_seed, obj/machinery/hydroponics/grown_tray)
	SIGNAL_HANDLER

	home_tray = WEAKREF(grown_tray)

/*
 * Whenever the plant starts to grow in a tray, check if we can release gas.
 *
 * our_seed - the seed growing
 * grown_tray - the tray, we're currently growing within
 */
/datum/plant_gene/trait/gas_production/proc/try_release_gas(obj/item/seeds/our_seed, obj/machinery/hydroponics/grown_tray)
	SIGNAL_HANDLER

	if(grown_tray.age < our_seed.maturation) // Start a little before it blooms
		return

	START_PROCESSING(SSobj, src)

/*
 * Stop the seed from releasing gas.
 */
/datum/plant_gene/trait/gas_production/proc/stop_gas(datum/source)
	SIGNAL_HANDLER

	STOP_PROCESSING(SSobj, src)

/*
 * If the conditions are acceptable and the potency is high enough, release miasma into the air.
 */
/datum/plant_gene/trait/gas_production/process(delta_time)
	var/obj/item/seeds/seed = stinky_seed?.resolve()
	var/obj/machinery/hydroponics/tray = home_tray?.resolve()

	// If our weakrefs don't resolve, or if our seed is /somehow/ not in the tray it was planted in, stop processing.
	if(!seed || !tray || seed.loc != tray)
		stop_gas()
		return

	var/turf/open/tray_turf = get_turf(tray)
	if(abs(ONE_ATMOSPHERE - tray_turf.return_air().return_pressure()) > (seed.potency / 10 + 10)) // clouds can begin showing at around 50-60 potency in standard atmos
		return

	var/datum/gas_mixture/stank = new
	ADD_GAS(/datum/gas/miasma, stank.gases)
	stank.gases[/datum/gas/miasma][MOLES] = (seed.yield + 6) * 3.5 * MIASMA_CORPSE_MOLES * delta_time // this process is only being called about 2/7 as much as corpses so this is 12-32 times a corpses
	stank.temperature = T20C // without this the room would eventually freeze and miasma mining would be easier
	tray_turf.assume_air(stank)

/// Starthistle's essential invasive spreading
/datum/plant_gene/trait/invasive/galaxythistle
	mutability_flags = PLANT_GENE_GRAFTABLE

/// Jupitercup's essential carnivory
/datum/plant_gene/trait/carnivory/jupitercup
	mutability_flags = PLANT_GENE_GRAFTABLE

/// Preset plant reagent genes that are unremovable from a plant.
/datum/plant_gene/reagent/preset
	mutability_flags = PLANT_GENE_GRAFTABLE

/datum/plant_gene/reagent/preset/New(new_reagent_id, new_reagent_rate = 0.04)
	. = ..()
	set_reagent(reagent_id)

/// Spaceman's Trumpet fragile Polypyrylium Oligomers
/datum/plant_gene/reagent/preset/polypyr
	reagent_id = /datum/reagent/medicine/polypyr
	rate = 0.15

/// Jupitercup's fragile Liquid Electricity
/datum/plant_gene/reagent/preset/liquidelectricity
	reagent_id = /datum/reagent/consumable/liquidelectricity/enriched
	rate = 0.1

/// Carbon Roses's fragile Carbon
/datum/plant_gene/reagent/preset/carbon
	reagent_id = /datum/reagent/carbon
	rate = 0.1

repo_name: AnalWerewolf/sundown

license: mit


code:
/**
 * Get all non admin_only instruments as a list of text ids.
 */
/proc/get_allowed_instrument_ids()
	. = list()
	for(var/id in SSinstruments.instrument_data)
		var/datum/instrument/I = SSinstruments.instrument_data[id]
		if(!I.admin_only)
			. += I.id

/**
 * # Instrument Datums
 *
 * Instrument datums hold the data for any given instrument, as well as data on how to play it and what bounds there are to playing it.
 *
 * The datums themselves are kept in SSinstruments in a list by their unique ID. The reason it uses ID instead of typepath is to support the runtime creation of instruments.
 * Since songs cache them while playing, there isn't realistic issues regarding performance from accessing.
 */
/datum/instrument
	/// Name of the instrument
	var/name = "Generic instrument"
	/// Uniquely identifies this instrument so runtime changes are possible as opposed to paths. If this is unset, things will use path instead.
	var/id
	/// Category
	var/category = "Unsorted"
	/// Used for categorization subtypes
	var/abstract_type = /datum/instrument
	/// Write here however many samples, follow this syntax: "%note num%"='%sample file%' eg. "27"='synthesizer/e2.ogg'. Key must never be lower than 0 and higher than 127
	var/list/real_samples
	/// assoc list key = /datum/instrument_key. do not fill this yourself!
	var/list/samples
	/// See __DEFINES/flags/instruments.dm
	var/instrument_flags = NONE
	/// For legacy instruments, the path to our notes
	var/legacy_instrument_path
	/// For legacy instruments, our file extension
	var/legacy_instrument_ext
	/// What songs are using us
	var/list/datum/song/songs_using = list()
	/// Don't touch this
	var/static/HIGHEST_KEY = 127
	/// Don't touch this x2
	var/static/LOWEST_KEY = 0
	/// Oh no - For truly troll instruments.
	var/admin_only = FALSE
	/// Volume multiplier. Synthesized instruments are quite loud and I don't like to cut off potential detail via editing. (someone correct me if this isn't a thing)
	var/volume_multiplier = 0.33

/datum/instrument/New()
	if(isnull(id))
		id = "[type]"

/**
 * Initializes the instrument, calculating its samples if necessary.
 */
/datum/instrument/proc/Initialize()
	if(instrument_flags & (INSTRUMENT_LEGACY | INSTRUMENT_DO_NOT_AUTOSAMPLE))
		return
	calculate_samples()

/**
 * Checks if this instrument is ready to play.
 */
/datum/instrument/proc/ready()
	if(instrument_flags & INSTRUMENT_LEGACY)
		return legacy_instrument_path && legacy_instrument_ext
	else if(instrument_flags & INSTRUMENT_DO_NOT_AUTOSAMPLE)
		return length(samples)
	return (length(samples) >= 128)

/datum/instrument/Destroy()
	SSinstruments.instrument_data -= id
	for(var/i in songs_using)
		var/datum/song/S = i
		S.set_instrument(null)
	real_samples = null
	samples = null
	songs_using = null
	return ..()

/**
 * For synthesized instruments, this is how the instrument generates the "keys" that a [/datum/song] uses to play notes.
 * Calculating them on the fly would be unperformant, so we do it during init and keep it all cached in a list.
 */
/datum/instrument/proc/calculate_samples()
	if(!length(real_samples))
		CRASH("No real samples defined for [id] [type] on calculate_samples() call.")
	var/list/real_keys = list()
	samples = list()
	for(var/key in real_samples)
		real_keys += text2num(key)
	sortTim(real_keys, /proc/cmp_numeric_asc, associative = FALSE)

	for(var/i in 1 to (length(real_keys) - 1))
		var/from_key = real_keys[i]
		var/to_key = real_keys[i+1]
		var/sample1 = real_samples[num2text(from_key)]
		var/sample2 = real_samples[num2text(to_key)]
		var/pivot = FLOOR((from_key + to_key) / 2, 1) //original code was a round but I replaced it because that's effectively a floor, thanks Baystation! who knows what was intended.
		for(var/key in from_key to pivot)
			samples[num2text(key)] = new /datum/instrument_key(sample1, key, key - from_key)
		for(var/key in (pivot + 1) to to_key)
			samples[num2text(key)] = new /datum/instrument_key(sample2, key, key - to_key)

	// Fill in 0 to first key and last key to 127
	var/first_key = real_keys[1]
	var/last_key = real_keys[length(real_keys)]
	var/first_sample = real_samples[num2text(first_key)]
	var/last_sample = real_samples[num2text(last_key)]
	for(var/key in LOWEST_KEY to (first_key - 1))
		samples[num2text(key)] = new /datum/instrument_key(first_sample, key, key - first_key)
	for(var/key in last_key to HIGHEST_KEY)
		samples[num2text(key)] = new /datum/instrument_key(last_sample, key, key - last_key)


code:
/**
 * Instrument key datums contain everything needed to know how to play a specific
 * note of an instrument.*
 */
/datum/instrument_key
	/// The numerical key of what this is, from 1 to 127 on a standard piano keyboard.
	var/key
	/// The actual sample file that will be loaded when playing.
	var/sample
	/// The frequency to play the sample to get our desired note.
	var/frequency
	/// Deviation up/down from the pivot point that uses its sample. Used to calculate frequency.
	var/deviation

/datum/instrument_key/New(sample = src.sample, key = src.key, deviation = src.deviation, frequency = src.frequency)
	src.sample = sample
	src.key = key
	src.deviation = deviation
	src.frequency = frequency
	if(!frequency && deviation)
		calculate()

/**
 * Calculates and stores our deviation.
 */
/datum/instrument_key/proc/calculate()
	if(!deviation)
		CRASH("Invalid calculate call: No deviation or sample in instrument_key")
	#define KEY_TWELTH (1/12)
	frequency = 2 ** (KEY_TWELTH * deviation)
	#undef KEY_TWELTH


code:
/datum/instrument/brass
	name = "Generic brass instrument"
	category = "Brass"
	abstract_type = /datum/instrument/brass

/datum/instrument/brass/crisis_section
	name = "Crisis Brass Section"
	id = "crbrass"
	real_samples = list("36"='sound/runtime/instruments/synthesis_samples/brass/crisis_brass/c2.ogg',
				   "48"='sound/runtime/instruments/synthesis_samples/brass/crisis_brass/c3.ogg',
				   "60"='sound/runtime/instruments/synthesis_samples/brass/crisis_brass/c4.ogg',
				   "72"='sound/runtime/instruments/synthesis_samples/brass/crisis_brass/c5.ogg')

/datum/instrument/brass/crisis_trombone
	name = "Crisis Trombone"
	id = "crtrombone"
	real_samples = list("36"='sound/runtime/instruments/synthesis_samples/brass/crisis_trombone/c2.ogg',
				   "48"='sound/runtime/instruments/synthesis_samples/brass/crisis_trombone/c3.ogg',
				   "60"='sound/runtime/instruments/synthesis_samples/brass/crisis_trombone/c4.ogg',
				   "72"='sound/runtime/instruments/synthesis_samples/brass/crisis_trombone/c5.ogg')

/datum/instrument/brass/crisis_trumpet
	name = "Crisis Trumpet"
	id = "crtrumpet"
	real_samples = list("60"='sound/runtime/instruments/synthesis_samples/brass/crisis_trumpet/c4.ogg',
				   "72"='sound/runtime/instruments/synthesis_samples/brass/crisis_trumpet/c5.ogg')


code:
/datum/instrument/chromatic
	name = "Generic chromatic percussion instrument"
	category = "Chromatic percussion"
	abstract_type = /datum/instrument/chromatic

/datum/instrument/chromatic/vibraphone1
	name = "Crisis Vibraphone"
	id = "crvibr"
	real_samples = list("36"='sound/runtime/instruments/synthesis_samples/chromatic/vibraphone1/c2.ogg',
				   "48"='sound/runtime/instruments/synthesis_samples/chromatic/vibraphone1/c3.ogg',
				   "60"='sound/runtime/instruments/synthesis_samples/chromatic/vibraphone1/c4.ogg',
				   "72"='sound/runtime/instruments/synthesis_samples/chromatic/vibraphone1/c5.ogg')

/datum/instrument/chromatic/musicbox1
	name = "SGM Music Box"
	id = "sgmmbox"
	real_samples = list("36"='sound/runtime/instruments/synthesis_samples/chromatic/sgmbox/c2.ogg',
				   "48"='sound/runtime/instruments/synthesis_samples/chromatic/sgmbox/c3.ogg',
				   "60"='sound/runtime/instruments/synthesis_samples/chromatic/sgmbox/c4.ogg',
				   "72"='sound/runtime/instruments/synthesis_samples/chromatic/sgmbox/c5.ogg')

/datum/instrument/chromatic/fluid_celeste
	name = "FluidR3 Celeste"
	id = "r3celeste"
	real_samples = list("36"='sound/runtime/instruments/synthesis_samples/chromatic/fluid_celeste/c2.ogg',
				   "48"='sound/runtime/instruments/synthesis_samples/chromatic/fluid_celeste/c3.ogg',
				   "60"='sound/runtime/instruments/synthesis_samples/chromatic/fluid_celeste/c4.ogg',
				   "72"='sound/runtime/instruments/synthesis_samples/chromatic/fluid_celeste/c5.ogg',
				   "84"='sound/runtime/instruments/synthesis_samples/chromatic/fluid_celeste/c6.ogg',
				   "96"='sound/runtime/instruments/synthesis_samples/chromatic/fluid_celeste/c7.ogg',
				   "108"='sound/runtime/instruments/synthesis_samples/chromatic/fluid_celeste/c8.ogg')


code:
/datum/instrument/fun
	name = "Generic Fun Instrument"
	category = "Fun"
	abstract_type = /datum/instrument/fun

/datum/instrument/fun/honk
	name = "!!HONK!!"
	id = "honk"
	real_samples = list("74"='sound/items/bikehorn.ogg') // Cluwne Heaven

/datum/instrument/fun/signal
	name = "Ping"
	id = "ping"
	real_samples = list("79"='sound/machines/ping.ogg')

/datum/instrument/fun/chime
	name = "Chime"
	id = "chime"
	real_samples = list("79"='sound/machines/chime.ogg')

/datum/instrument/fun/mothscream
	name = "Moth Scream"
	id = "mothscream"
	real_samples = list("60"='sound/voice/moth/scream_moth.ogg')
	admin_only = TRUE


code:
/datum/instrument/guitar
	name = "Generic guitar-like instrument"
	category = "Guitar"
	abstract_type = /datum/instrument/guitar

/datum/instrument/guitar/steel_crisis
	name = "Crisis Steel String Guitar"
	id = "csteelgt"
	real_samples = list("36"='sound/runtime/instruments/synthesis_samples/guitar/crisis_steel/c2.ogg',
				   "48"='sound/runtime/instruments/synthesis_samples/guitar/crisis_steel/c3.ogg',
				   "60"='sound/runtime/instruments/synthesis_samples/guitar/crisis_steel/c4.ogg',
				   "72"='sound/runtime/instruments/synthesis_samples/guitar/crisis_steel/c5.ogg')

/datum/instrument/guitar/nylon_crisis
	name = "Crisis Nylon String Guitar"
	id = "cnylongt"
	real_samples = list("36"='sound/runtime/instruments/synthesis_samples/guitar/crisis_nylon/c2.ogg',
				   "48"='sound/runtime/instruments/synthesis_samples/guitar/crisis_nylon/c3.ogg',
				   "60"='sound/runtime/instruments/synthesis_samples/guitar/crisis_nylon/c4.ogg',
				   "72"='sound/runtime/instruments/synthesis_samples/guitar/crisis_nylon/c5.ogg')

/datum/instrument/guitar/clean_crisis
	name = "Crisis Clean Guitar"
	id = "ccleangt"
	real_samples = list("36"='sound/runtime/instruments/synthesis_samples/guitar/crisis_clean/c2.ogg',
				   "48"='sound/runtime/instruments/synthesis_samples/guitar/crisis_clean/c3.ogg',
				   "60"='sound/runtime/instruments/synthesis_samples/guitar/crisis_clean/c4.ogg',
				   "72"='sound/runtime/instruments/synthesis_samples/guitar/crisis_clean/c5.ogg')

/datum/instrument/guitar/muted_crisis
	name = "Crisis Muted Guitar"
	id = "cmutedgt"
	real_samples = list("36"='sound/runtime/instruments/synthesis_samples/guitar/crisis_muted/c2.ogg',
				   "48"='sound/runtime/instruments/synthesis_samples/guitar/crisis_muted/c3.ogg',
				   "60"='sound/runtime/instruments/synthesis_samples/guitar/crisis_muted/c4.ogg',
				   "72"='sound/runtime/instruments/synthesis_samples/guitar/crisis_muted/c5.ogg')


code:
//THESE ARE HARDCODED INSTRUMENT SAMPLES.
//SONGS WILL BE AUTOMATICALLY SWITCHED TO LEGACY MODE IF THEY USE THIS KIND OF INSTRUMENT!
//I'd prefer these stayed. They sound different from the mechanical synthesis of synthed instruments, and I quite like them that way. It's not legacy, it's hardcoded, old style. - kevinz000
/datum/instrument/hardcoded
	abstract_type = /datum/instrument/hardcoded
	category = "Non-Synthesized"
	instrument_flags = INSTRUMENT_LEGACY
	volume_multiplier = 1 //not as loud as synth'd

/datum/instrument/hardcoded/accordion
	name = "Accordion"
	id = "accordion"
	legacy_instrument_ext = "mid"
	legacy_instrument_path = "accordion"

/datum/instrument/hardcoded/bikehorn
	name = "Bike Horn"
	id = "bikehorn"
	legacy_instrument_ext = "ogg"
	legacy_instrument_path = "bikehorn"

/datum/instrument/hardcoded/eguitar
	name = "Electric Guitar"
	id = "eguitar"
	legacy_instrument_ext = "ogg"
	legacy_instrument_path = "eguitar"

/datum/instrument/hardcoded/glockenspiel
	name = "Glockenspiel"
	id = "glockenspiel"
	legacy_instrument_ext = "mid"
	legacy_instrument_path = "glockenspiel"

/datum/instrument/hardcoded/guitar
	name = "Guitar"
	id = "guitar"
	legacy_instrument_ext = "ogg"
	legacy_instrument_path = "guitar"

/datum/instrument/hardcoded/harmonica
	name = "Harmonica"
	id = "harmonica"
	legacy_instrument_ext = "mid"
	legacy_instrument_path = "harmonica"

/datum/instrument/hardcoded/piano
	name = "Piano"
	id = "piano"
	legacy_instrument_ext = "ogg"
	legacy_instrument_path = "piano"

/datum/instrument/hardcoded/recorder
	name = "Recorder"
	id = "recorder"
	legacy_instrument_ext = "mid"
	legacy_instrument_path = "recorder"

/datum/instrument/hardcoded/saxophone
	name = "Saxophone"
	id = "saxophone"
	legacy_instrument_ext = "mid"
	legacy_instrument_path = "saxophone"

/datum/instrument/hardcoded/trombone
	name = "Trombone"
	id = "trombone"
	legacy_instrument_ext = "mid"
	legacy_instrument_path = "trombone"

/datum/instrument/hardcoded/violin
	name = "Violin"
	id = "violin"
	legacy_instrument_ext = "mid"
	legacy_instrument_path = "violin"

/datum/instrument/hardcoded/xylophone
	name = "Xylophone"
	id = "xylophone"
	legacy_instrument_ext = "mid"
	legacy_instrument_path = "xylophone"

/datum/instrument/hardcoded/banjo
	name = "Banjo"
	id = "banjo"
	legacy_instrument_ext = "ogg"
	legacy_instrument_path = "banjo"


code:
/datum/instrument/organ
	name = "Generic organ"
	category = "Organ"
	abstract_type = /datum/instrument/organ

/datum/instrument/organ/crisis_church
	name = "Crisis Church Organ"
	id = "crichugan"
	real_samples = list("36"='sound/runtime/instruments/synthesis_samples/organ/crisis_church/c2.ogg',
				   "48"='sound/runtime/instruments/synthesis_samples/organ/crisis_church/c3.ogg',
				   "60"='sound/runtime/instruments/synthesis_samples/organ/crisis_church/c4.ogg',
				   "72"='sound/runtime/instruments/synthesis_samples/organ/crisis_church/c5.ogg')

/datum/instrument/organ/crisis_hammond
	name = "Crisis Hammond Organ"
	id = "crihamgan"
	real_samples = list("36"='sound/runtime/instruments/synthesis_samples/organ/crisis_hammond/c2.ogg',
				   "48"='sound/runtime/instruments/synthesis_samples/organ/crisis_hammond/c3.ogg',
				   "60"='sound/runtime/instruments/synthesis_samples/organ/crisis_hammond/c4.ogg',
				   "72"='sound/runtime/instruments/synthesis_samples/organ/crisis_hammond/c5.ogg')

/datum/instrument/organ/crisis_accordian
	name = "Crisis Accordian"
	id = "crack"
	real_samples = list("36"='sound/runtime/instruments/synthesis_samples/organ/crisis_accordian/c2.ogg',
				   "48"='sound/runtime/instruments/synthesis_samples/organ/crisis_accordian/c3.ogg',
				   "60"='sound/runtime/instruments/synthesis_samples/organ/crisis_accordian/c4.ogg',
				   "72"='sound/runtime/instruments/synthesis_samples/organ/crisis_accordian/c5.ogg')

/datum/instrument/organ/crisis_harmonica
	name = "Crisis Harmonica"
	id = "crharmony"
	real_samples = list("48"='sound/runtime/instruments/synthesis_samples/organ/crisis_harmonica/c3.ogg',
				   "60"='sound/runtime/instruments/synthesis_samples/organ/crisis_harmonica/c4.ogg',
				   "72"='sound/runtime/instruments/synthesis_samples/organ/crisis_harmonica/c5.ogg')

/datum/instrument/organ/crisis_tango_accordian
	name = "Crisis Tango Accordian"
	id = "crtango"
	real_samples = list("36"='sound/runtime/instruments/synthesis_samples/organ/crisis_tangaccordian/c2.ogg',
				   "48"='sound/runtime/instruments/synthesis_samples/organ/crisis_tangaccordian/c3.ogg',
				   "60"='sound/runtime/instruments/synthesis_samples/organ/crisis_tangaccordian/c4.ogg',
				   "72"='sound/runtime/instruments/synthesis_samples/organ/crisis_tangaccordian/c5.ogg')


code:
/datum/instrument/piano
	name = "Generic piano"
	category = "Piano"
	abstract_type = /datum/instrument/piano

/datum/instrument/piano/fluid_piano
	name = "FluidR3 Grand Piano"
	id = "r3grand"
	real_samples = list("36"='sound/runtime/instruments/synthesis_samples/piano/fluid_piano/c2.ogg',
				   "48"='sound/runtime/instruments/synthesis_samples/piano/fluid_piano/c3.ogg',
				   "60"='sound/runtime/instruments/synthesis_samples/piano/fluid_piano/c4.ogg',
				   "72"='sound/runtime/instruments/synthesis_samples/piano/fluid_piano/c5.ogg',
				   "84"='sound/runtime/instruments/synthesis_samples/piano/fluid_piano/c6.ogg',
				   "96"='sound/runtime/instruments/synthesis_samples/piano/fluid_piano/c7.ogg',
				   "108"='sound/runtime/instruments/synthesis_samples/piano/fluid_piano/c8.ogg')

/datum/instrument/piano/fluid_harpsichord
	name = "FluidR3 Harpsichord"
	id = "r3harpsi"
	real_samples = list("36"='sound/runtime/instruments/synthesis_samples/piano/fluid_harpsi/c2.ogg',
				   "48"='sound/runtime/instruments/synthesis_samples/piano/fluid_harpsi/c3.ogg',
				   "60"='sound/runtime/instruments/synthesis_samples/piano/fluid_harpsi/c4.ogg',
				   "72"='sound/runtime/instruments/synthesis_samples/piano/fluid_harpsi/c5.ogg',
				   "84"='sound/runtime/instruments/synthesis_samples/piano/fluid_harpsi/c6.ogg',
				   "96"='sound/runtime/instruments/synthesis_samples/piano/fluid_harpsi/c7.ogg',
				   "108"='sound/runtime/instruments/synthesis_samples/piano/fluid_harpsi/c8.ogg')

/datum/instrument/piano/crisis_harpsichord
	name = "Crisis Harpsichord"
	id = "crharpsi"
	real_samples = list("36"='sound/runtime/instruments/synthesis_samples/piano/crisis_harpsichord/c2.ogg',
				   "48"='sound/runtime/instruments/synthesis_samples/piano/crisis_harpsichord/c3.ogg',
				   "60"='sound/runtime/instruments/synthesis_samples/piano/crisis_harpsichord/c4.ogg',
				   "72"='sound/runtime/instruments/synthesis_samples/piano/crisis_harpsichord/c5.ogg')

/datum/instrument/piano/crisis_grandpiano_uni
	name = "Crisis Grand Piano One"
	id = "crgrand1"
	real_samples = list("36"='sound/runtime/instruments/synthesis_samples/piano/crisis_grand_piano/c2.ogg',
				   "48"='sound/runtime/instruments/synthesis_samples/piano/crisis_grand_piano/c3.ogg',
				   "60"='sound/runtime/instruments/synthesis_samples/piano/crisis_grand_piano/c4.ogg',
				   "72"='sound/runtime/instruments/synthesis_samples/piano/crisis_grand_piano/c5.ogg',
				   "84"='sound/runtime/instruments/synthesis_samples/piano/crisis_grand_piano/c6.ogg',
				   "96"='sound/runtime/instruments/synthesis_samples/piano/crisis_grand_piano/c7.ogg',
				   "108"='sound/runtime/instruments/synthesis_samples/piano/crisis_grand_piano/c8.ogg')

/datum/instrument/piano/crisis_brightpiano_uni
	name = "Crisis Bright Piano One"
	id = "crbright1"
	real_samples = list("36"='sound/runtime/instruments/synthesis_samples/piano/crisis_bright_piano/c2.ogg',
				   "48"='sound/runtime/instruments/synthesis_samples/piano/crisis_bright_piano/c3.ogg',
				   "60"='sound/runtime/instruments/synthesis_samples/piano/crisis_bright_piano/c4.ogg',
				   "72"='sound/runtime/instruments/synthesis_samples/piano/crisis_bright_piano/c5.ogg',
				   "84"='sound/runtime/instruments/synthesis_samples/piano/crisis_bright_piano/c6.ogg',
				   "96"='sound/runtime/instruments/synthesis_samples/piano/crisis_bright_piano/c7.ogg',
				   "108"='sound/runtime/instruments/synthesis_samples/piano/crisis_bright_piano/c8.ogg')


code:
/datum/instrument/tones
	name = "Ideal tone"
	category = "Tones"
	abstract_type = /datum/instrument/tones

/datum/instrument/tones/square_wave
	name = "Ideal square wave"
	id = "square"
	real_samples = list("81"='sound/runtime/instruments/synthesis_samples/tones/Square.ogg')

/datum/instrument/tones/sine_wave
	name = "Ideal sine wave"
	id = "sine"
	real_samples = list("81"='sound/runtime/instruments/synthesis_samples/tones/Sine.ogg')

/datum/instrument/tones/saw_wave
	name = "Ideal sawtooth wave"
	id = "saw"
	real_samples = list("81"='sound/runtime/instruments/synthesis_samples/tones/Sawtooth.ogg')


code:
//copy pasta of the space piano, don't hurt me -Pete
/obj/item/instrument
	name = "generic instrument"
	force = 10
	max_integrity = 100
	resistance_flags = FLAMMABLE
	icon = 'icons/obj/musician.dmi'
	lefthand_file = 'icons/mob/inhands/equipment/instruments_lefthand.dmi'
	righthand_file = 'icons/mob/inhands/equipment/instruments_righthand.dmi'
	/// Our song datum.
	var/datum/song/handheld/song
	/// Our allowed list of instrument ids. This is nulled on initialize.
	var/list/allowed_instrument_ids
	/// How far away our song datum can be heard.
	var/instrument_range = 15

/obj/item/instrument/Initialize(mapload)
	. = ..()
	song = new(src, allowed_instrument_ids, instrument_range)
	allowed_instrument_ids = null //We don't need this clogging memory after it's used.

/obj/item/instrument/Destroy()
	QDEL_NULL(song)
	return ..()

/obj/item/instrument/proc/should_stop_playing(mob/user)
	return user.incapacitated() || !((loc == user) || (isturf(loc) && Adjacent(user))) // sorry, no more TK playing.

/obj/item/instrument/suicide_act(mob/user)
	user.visible_message(span_suicide("[user] begins to play 'Gloomy Sunday'! It looks like [user.p_theyre()] trying to commit suicide!"))
	return (BRUTELOSS)

/obj/item/instrument/attack_self(mob/user)
	if(!ISADVANCEDTOOLUSER(user))
		to_chat(user, span_warning("You don't have the dexterity to do this!"))
		return TRUE
	interact(user)

/obj/item/instrument/interact(mob/user)
	ui_interact(user)

/obj/item/instrument/ui_interact(mob/living/user)
	if(!isliving(user) || user.stat != CONSCIOUS || (HAS_TRAIT(user, TRAIT_HANDS_BLOCKED) && !ispAI(user)))
		return

	user.set_machine(src)
	song.ui_interact(user)

/obj/item/instrument/violin
	name = "space violin"
	desc = "A wooden musical instrument with four strings and a bow. \"The devil went down to space, he was looking for an assistant to grief.\""
	icon_state = "violin"
	inhand_icon_state = "violin"
	hitsound = "swing_hit"
	allowed_instrument_ids = "violin"

/obj/item/instrument/violin/golden
	name = "golden violin"
	desc = "A golden musical instrument with four strings and a bow. \"The devil went down to space, he was looking for an assistant to grief.\""
	icon_state = "golden_violin"
	inhand_icon_state = "golden_violin"
	resistance_flags = LAVA_PROOF | FIRE_PROOF | ACID_PROOF

/obj/item/instrument/piano_synth
	name = "synthesizer"
	desc = "An advanced electronic synthesizer that can be used as various instruments."
	icon_state = "synth"
	inhand_icon_state = "synth"
	allowed_instrument_ids = "piano"

/obj/item/instrument/piano_synth/Initialize(mapload)
	. = ..()
	song.allowed_instrument_ids = SSinstruments.synthesizer_instrument_ids

/obj/item/instrument/piano_synth/headphones
	name = "headphones"
	desc = "Unce unce unce unce. Boop!"
	icon = 'icons/obj/clothing/accessories.dmi'
	lefthand_file = 'icons/mob/inhands/clothing_lefthand.dmi'
	righthand_file = 'icons/mob/inhands/clothing_righthand.dmi'
	icon_state = "headphones"
	inhand_icon_state = "headphones"
	slot_flags = ITEM_SLOT_EARS | ITEM_SLOT_HEAD
	force = 0
	w_class = WEIGHT_CLASS_SMALL
	custom_price = PAYCHECK_ASSISTANT * 2.5
	instrument_range = 1

/obj/item/instrument/piano_synth/headphones/ComponentInitialize()
	. = ..()
	AddElement(/datum/element/update_icon_updates_onmob)
	RegisterSignal(src, COMSIG_SONG_START, .proc/start_playing)
	RegisterSignal(src, COMSIG_SONG_END, .proc/stop_playing)

/**
 * Called by a component signal when our song starts playing.
 */
/obj/item/instrument/piano_synth/headphones/proc/start_playing()
	SIGNAL_HANDLER
	icon_state = "[initial(icon_state)]_on"
	update_appearance()

/**
 * Called by a component signal when our song stops playing.
 */
/obj/item/instrument/piano_synth/headphones/proc/stop_playing()
	SIGNAL_HANDLER
	icon_state = "[initial(icon_state)]"
	update_appearance()

/obj/item/instrument/piano_synth/headphones/spacepods
	name = "\improper Nanotrasen space pods"
	desc = "Flex your money, AND ignore what everyone else says, all at once!"
	icon_state = "spacepods"
	inhand_icon_state = "spacepods"
	slot_flags = ITEM_SLOT_EARS
	strip_delay = 100 //air pods don't fall out
	instrument_range = 0 //you're paying for quality here
	custom_premium_price = PAYCHECK_ASSISTANT * 36 //Save up 5 shifts worth of pay just to lose it down a drainpipe on the sidewalk

/obj/item/instrument/banjo
	name = "banjo"
	desc = "A 'Mura' brand banjo. It's pretty much just a drum with a neck and strings."
	icon_state = "banjo"
	inhand_icon_state = "banjo"
	attack_verb_continuous = list("scruggs-styles", "hum-diggitys", "shin-digs", "clawhammers")
	attack_verb_simple = list("scruggs-style", "hum-diggity", "shin-dig", "clawhammer")
	hitsound = 'sound/weapons/banjoslap.ogg'
	allowed_instrument_ids = "banjo"

/obj/item/instrument/guitar
	name = "guitar"
	desc = "It's made of wood and has bronze strings."
	icon_state = "guitar"
	inhand_icon_state = "guitar"
	attack_verb_continuous = list("plays metal on", "serenades", "crashes", "smashes")
	attack_verb_simple = list("play metal on", "serenade", "crash", "smash")
	hitsound = 'sound/weapons/stringsmash.ogg'
	allowed_instrument_ids = list("guitar","csteelgt","cnylongt", "ccleangt", "cmutedgt")

/obj/item/instrument/eguitar
	name = "electric guitar"
	desc = "Makes all your shredding needs possible."
	icon_state = "eguitar"
	inhand_icon_state = "eguitar"
	force = 12
	attack_verb_continuous = list("plays metal on", "shreds", "crashes", "smashes")
	attack_verb_simple = list("play metal on", "shred", "crash", "smash")
	hitsound = 'sound/weapons/stringsmash.ogg'
	allowed_instrument_ids = "eguitar"

/obj/item/instrument/glockenspiel
	name = "glockenspiel"
	desc = "Smooth metal bars perfect for any marching band."
	icon_state = "glockenspiel"
	allowed_instrument_ids = list("glockenspiel","crvibr", "sgmmbox", "r3celeste")
	inhand_icon_state = "glockenspiel"

/obj/item/instrument/accordion
	name = "accordion"
	desc = "Pun-Pun not included."
	icon_state = "accordion"
	allowed_instrument_ids = list("crack", "crtango", "accordion")
	inhand_icon_state = "accordion"

/obj/item/instrument/trumpet
	name = "trumpet"
	desc = "To announce the arrival of the king!"
	icon_state = "trumpet"
	allowed_instrument_ids = "crtrumpet"
	inhand_icon_state = "trumpet"

/obj/item/instrument/trumpet/spectral
	name = "spectral trumpet"
	desc = "Things are about to get spooky!"
	icon_state = "spectral_trumpet"
	inhand_icon_state = "spectral_trumpet"
	force = 0
	attack_verb_continuous = list("plays", "jazzes", "trumpets", "mourns", "doots", "spooks")
	attack_verb_simple = list("play", "jazz", "trumpet", "mourn", "doot", "spook")

/obj/item/instrument/trumpet/spectral/Initialize(mapload)
	. = ..()
	AddElement(/datum/element/spooky)

/obj/item/instrument/trumpet/spectral/attack(mob/living/carbon/C, mob/user)
	playsound (src, 'sound/runtime/instruments/trombone/En4.mid', 100,1,-1)
	..()

/obj/item/instrument/saxophone
	name = "saxophone"
	desc = "This soothing sound will be sure to leave your audience in tears."
	icon_state = "saxophone"
	allowed_instrument_ids = "saxophone"
	inhand_icon_state = "saxophone"

/obj/item/instrument/saxophone/spectral
	name = "spectral saxophone"
	desc = "This spooky sound will be sure to leave mortals in bones."
	icon_state = "saxophone"
	inhand_icon_state = "saxophone"
	force = 0
	attack_verb_continuous = list("plays", "jazzes", "saxxes", "mourns", "doots", "spooks")
	attack_verb_simple = list("play", "jazz", "sax", "mourn", "doot", "spook")

/obj/item/instrument/saxophone/spectral/Initialize(mapload)
	. = ..()
	AddElement(/datum/element/spooky)

/obj/item/instrument/saxophone/spectral/attack(mob/living/carbon/C, mob/user)
	playsound (src, 'sound/runtime/instruments/saxophone/En4.mid', 100,1,-1)
	..()

/obj/item/instrument/trombone
	name = "trombone"
	desc = "How can any pool table ever hope to compete?"
	icon_state = "trombone"
	allowed_instrument_ids = list("crtrombone", "crbrass", "trombone")
	inhand_icon_state = "trombone"

/obj/item/instrument/trombone/spectral
	name = "spectral trombone"
	desc = "A skeleton's favorite instrument. Apply directly on the mortals."
	icon_state = "trombone"
	inhand_icon_state = "trombone"
	force = 0
	attack_verb_continuous = list("plays", "jazzes", "trombones", "mourns", "doots", "spooks")
	attack_verb_simple = list("play", "jazz", "trombone", "mourn", "doot", "spook")

/obj/item/instrument/trombone/spectral/Initialize(mapload)
	. = ..()
	AddElement(/datum/element/spooky)

/obj/item/instrument/trombone/spectral/attack(mob/living/carbon/C, mob/user)
	playsound (src, 'sound/runtime/instruments/trombone/Cn4.mid', 100,1,-1)
	..()

/obj/item/instrument/recorder
	name = "recorder"
	desc = "Just like in school, playing ability and all."
	force = 5
	icon_state = "recorder"
	allowed_instrument_ids = "recorder"
	inhand_icon_state = "recorder"

/obj/item/instrument/harmonica
	name = "harmonica"
	desc = "For when you get a bad case of the space blues."
	icon_state = "harmonica"
	allowed_instrument_ids = list("crharmony", "harmonica")
	inhand_icon_state = "harmonica"
	slot_flags = ITEM_SLOT_MASK
	force = 5
	w_class = WEIGHT_CLASS_SMALL
	actions_types = list(/datum/action/item_action/instrument)

/obj/item/instrument/harmonica/proc/handle_speech(datum/source, list/speech_args)
	SIGNAL_HANDLER
	if(song.playing && ismob(loc))
		to_chat(loc, span_warning("You stop playing the harmonica to talk..."))
		song.playing = FALSE

/obj/item/instrument/harmonica/equipped(mob/M, slot)
	. = ..()
	RegisterSignal(M, COMSIG_MOB_SAY, .proc/handle_speech)

/obj/item/instrument/harmonica/dropped(mob/M)
	. = ..()
	UnregisterSignal(M, COMSIG_MOB_SAY)

/obj/item/instrument/bikehorn
	name = "gilded bike horn"
	desc = "An exquisitely decorated bike horn, capable of honking in a variety of notes."
	icon_state = "bike_horn"
	inhand_icon_state = "bike_horn"
	lefthand_file = 'icons/mob/inhands/equipment/horns_lefthand.dmi'
	righthand_file = 'icons/mob/inhands/equipment/horns_righthand.dmi'
	allowed_instrument_ids = list("bikehorn", "honk")
	attack_verb_continuous = list("beautifully honks")
	attack_verb_simple = list("beautifully honk")
	w_class = WEIGHT_CLASS_TINY
	force = 0
	throw_speed = 3
	throw_range = 15
	hitsound = 'sound/items/bikehorn.ogg'

/obj/item/choice_beacon/music
	name = "instrument delivery beacon"
	desc = "Summon your tool of art."
	icon_state = "gangtool-red"

/obj/item/choice_beacon/music/generate_display_names()
	var/static/list/instruments
	if(!instruments)
		instruments = list()
		var/list/templist = list(/obj/item/instrument/violin,
							/obj/item/instrument/piano_synth,
							/obj/item/instrument/banjo,
							/obj/item/instrument/guitar,
							/obj/item/instrument/eguitar,
							/obj/item/instrument/glockenspiel,
							/obj/item/instrument/accordion,
							/obj/item/instrument/trumpet,
							/obj/item/instrument/saxophone,
							/obj/item/instrument/trombone,
							/obj/item/instrument/recorder,
							/obj/item/instrument/harmonica,
							/obj/item/instrument/piano_synth/headphones
							)
		for(var/V in templist)
			var/atom/A = V
			instruments[initial(A.name)] = A
	return instruments

/obj/item/instrument/musicalmoth
	name = "musical moth"
	desc = "Despite its popularity, this controversial musical toy was eventually banned due to its unethically sampled sounds of moths screaming in agony."
	icon_state = "mothsician"
	allowed_instrument_ids = "mothscream"
	attack_verb_continuous = list("flutters", "flaps")
	attack_verb_simple = list("flutter", "flap")
	w_class = WEIGHT_CLASS_TINY
	force = 0
	hitsound = 'sound/voice/moth/scream_moth.ogg'
	custom_price = PAYCHECK_HARD * 2.37
	custom_premium_price = PAYCHECK_HARD * 2.37


code:
/**
 * # Song datum
 *
 * These are the actual backend behind instruments.
 * They attach to an atom and provide the editor + playback functionality.
 */
/datum/song
	/// Name of the song
	var/name = "Untitled"

	/// The atom we're attached to/playing from
	var/atom/parent

	/// Our song lines
	var/list/lines

	/// delay between notes in deciseconds
	var/tempo = 5

	/// How far we can be heard
	var/instrument_range = 15

	/// Are we currently playing?
	var/playing = FALSE

	/// Are we currently editing?
	var/editing = TRUE
	/// Is the help screen open?
	var/help = FALSE

	/// Repeats left
	var/repeat = 0
	/// Maximum times we can repeat
	var/max_repeats = 10

	/// Our volume
	var/volume = 35
	/// Max volume
	var/max_volume = 75
	/// Min volume - This is so someone doesn't decide it's funny to set it to 0 and play invisible songs.
	var/min_volume = 1

	/// What instruments our built in picker can use. The picker won't show unless this is longer than one.
	var/list/allowed_instrument_ids = list("r3grand")

	//////////// Cached instrument variables /////////////
	/// Instrument we are currently using
	var/datum/instrument/using_instrument
	/// Cached legacy ext for legacy instruments
	var/cached_legacy_ext
	/// Cached legacy dir for legacy instruments
	var/cached_legacy_dir
	/// Cached list of samples, referenced directly from the instrument for synthesized instruments
	var/list/cached_samples
	/// Are we operating in legacy mode (so if the instrument is a legacy instrument)
	var/legacy = FALSE
	//////////////////////////////////////////////////////

	/////////////////// Playing variables ////////////////
	/**
	  * Build by compile_chords()
	  * Must be rebuilt on instrument switch.
	  * Compilation happens when we start playing and is cleared after we finish playing.
	  * Format: list of chord lists, with chordlists having (key1, key2, key3, tempodiv)
	  */
	var/list/compiled_chords
	/// Current section of a long chord we're on, so we don't need to make a billion chords, one for every unit ticklag.
	var/elapsed_delay
	/// Amount of delay to wait before playing the next chord
	var/delay_by
	/// Current chord we're on.
	var/current_chord
	/// Channel as text = current volume percentage but it's 0 to 100 instead of 0 to 1.
	var/list/channels_playing = list()
	/// List of channels that aren't being used, as text. This is to prevent unnecessary freeing and reallocations from SSsounds/SSinstruments.
	var/list/channels_idle = list()
	/// Person playing us
	var/mob/user_playing
	//////////////////////////////////////////////////////

	/// Last world.time we checked for who can hear us
	var/last_hearcheck = 0
	/// The list of mobs that can hear us
	var/list/hearing_mobs
	/// If this is enabled, some things won't be strictly cleared when they usually are (liked compiled_chords on play stop)
	var/debug_mode = FALSE
	/// Max sound channels to occupy
	var/max_sound_channels = CHANNELS_PER_INSTRUMENT
	/// Current channels, so we can save a length() call.
	var/using_sound_channels = 0
	/// Last channel to play. text.
	var/last_channel_played
	/// Should we not decay our last played note?
	var/full_sustain_held_note = TRUE

	/////////////////////// DO NOT TOUCH THESE ///////////////////
	var/octave_min = INSTRUMENT_MIN_OCTAVE
	var/octave_max = INSTRUMENT_MAX_OCTAVE
	var/key_min = INSTRUMENT_MIN_KEY
	var/key_max = INSTRUMENT_MAX_KEY
	var/static/list/note_offset_lookup = list(9, 11, 0, 2, 4, 5, 7)
	var/static/list/accent_lookup = list("b" = -1, "s" = 1, "#" = 1, "n" = 0)
	//////////////////////////////////////////////////////////////

	///////////// !!FUN!! - Only works in synthesized mode! /////////////////
	/// Note numbers to shift.
	var/note_shift = 0
	var/note_shift_min = -100
	var/note_shift_max = 100
	var/can_noteshift = TRUE
	/// The kind of sustain we're using
	var/sustain_mode = SUSTAIN_LINEAR
	/// When a note is considered dead if it is below this in volume
	var/sustain_dropoff_volume = 0
	/// Total duration of linear sustain for 100 volume note to get to SUSTAIN_DROPOFF
	var/sustain_linear_duration = 5
	/// Exponential sustain dropoff rate per decisecond
	var/sustain_exponential_dropoff = 1.4
	////////// DO NOT DIRECTLY SET THESE!
	/// Do not directly set, use update_sustain()
	var/cached_linear_dropoff = 10
	/// Do not directly set, use update_sustain()
	var/cached_exponential_dropoff = 1.045
	/////////////////////////////////////////////////////////////////////////

/datum/song/New(atom/parent, list/instrument_ids, new_range)
	SSinstruments.on_song_new(src)
	lines = list()
	tempo = sanitize_tempo(tempo)
	src.parent = parent
	if(instrument_ids)
		allowed_instrument_ids = islist(instrument_ids)? instrument_ids : list(instrument_ids)
	if(length(allowed_instrument_ids))
		set_instrument(allowed_instrument_ids[1])
	hearing_mobs = list()
	volume = clamp(volume, min_volume, max_volume)
	update_sustain()
	if(new_range)
		instrument_range = new_range

/datum/song/Destroy()
	stop_playing()
	SSinstruments.on_song_del(src)
	lines = null
	if(using_instrument)
		using_instrument.songs_using -= src
		using_instrument = null
	allowed_instrument_ids = null
	parent = null
	return ..()

/**
 * Checks and stores which mobs can hear us. Terminates sounds for mobs that leave our range.
 */
/datum/song/proc/do_hearcheck()
	last_hearcheck = world.time
	var/list/old = hearing_mobs.Copy()
	hearing_mobs.len = 0
	var/turf/source = get_turf(parent)
	for(var/mob/M in get_hearers_in_view(instrument_range, source))
		hearing_mobs[M] = get_dist(M, source)
	var/list/exited = old - hearing_mobs
	for(var/i in exited)
		terminate_sound_mob(i)

/**
 * Sets our instrument, caching anything necessary for faster accessing. Accepts an ID, typepath, or instantiated instrument datum.
 */
/datum/song/proc/set_instrument(datum/instrument/I)
	terminate_all_sounds()
	var/old_legacy
	if(using_instrument)
		using_instrument.songs_using -= src
		old_legacy = (using_instrument.instrument_flags & INSTRUMENT_LEGACY)
	using_instrument = null
	cached_samples = null
	cached_legacy_ext = null
	cached_legacy_dir = null
	legacy = null
	if(istext(I) || ispath(I))
		I = SSinstruments.instrument_data[I]
	if(istype(I))
		using_instrument = I
		I.songs_using += src
		var/instrument_legacy = (I.instrument_flags & INSTRUMENT_LEGACY)
		if(instrument_legacy)
			cached_legacy_ext = I.legacy_instrument_ext
			cached_legacy_dir = I.legacy_instrument_path
			legacy = TRUE
		else
			cached_samples = I.samples
			legacy = FALSE
		if(isnull(old_legacy) || (old_legacy != instrument_legacy))
			if(playing)
				compile_chords()

/**
 * Attempts to start playing our song.
 */
/datum/song/proc/start_playing(mob/user)
	if(playing)
		return
	if(!using_instrument?.ready())
		to_chat(user, span_warning("An error has occured with [src]. Please reset the instrument."))
		return
	compile_chords()
	if(!length(compiled_chords))
		to_chat(user, span_warning("Song is empty."))
		return
	playing = TRUE
	updateDialog(user_playing)
	//we can not afford to runtime, since we are going to be doing sound channel reservations and if we runtime it means we have a channel allocation leak.
	//wrap the rest of the stuff to ensure stop_playing() is called.
	do_hearcheck()
	SEND_SIGNAL(parent, COMSIG_SONG_START)
	elapsed_delay = 0
	delay_by = 0
	current_chord = 1
	user_playing = user
	START_PROCESSING(SSinstruments, src)

/**
 * Stops playing, terminating all sounds if in synthesized mode. Clears hearing_mobs.
 */
/datum/song/proc/stop_playing()
	if(!playing)
		return
	playing = FALSE
	if(!debug_mode)
		compiled_chords = null
	STOP_PROCESSING(SSinstruments, src)
	SEND_SIGNAL(parent, COMSIG_SONG_END)
	terminate_all_sounds(TRUE)
	hearing_mobs.len = 0
	user_playing = null

/**
 * Processes our song.
 */
/datum/song/proc/process_song(wait)
	if(!length(compiled_chords) || should_stop_playing(user_playing))
		stop_playing()
		return
	var/list/chord = compiled_chords[current_chord]
	if(++elapsed_delay >= delay_by)
		play_chord(chord)
		elapsed_delay = 0
		delay_by = tempodiv_to_delay(chord[length(chord)])
		current_chord++
		if(current_chord > length(compiled_chords))
			if(repeat)
				repeat--
				current_chord = 1
				return
			else
				stop_playing()
				return

/**
 * Converts a tempodiv to ticks to elapse before playing the next chord, taking into account our tempo.
 */
/datum/song/proc/tempodiv_to_delay(tempodiv)
	if(!tempodiv)
		tempodiv = 1 // no division by 0. some song converters tend to use 0 for when it wants to have no div, for whatever reason.
	return max(1, round((tempo/tempodiv) / world.tick_lag, 1))

/**
 * Compiles chords.
 */
/datum/song/proc/compile_chords()
	legacy? compile_legacy() : compile_synthesized()

/**
 * Plays a chord.
 */
/datum/song/proc/play_chord(list/chord)
	// last value is timing information
	for(var/i in 1 to (length(chord) - 1))
		legacy? playkey_legacy(chord[i][1], chord[i][2], chord[i][3], user_playing) : playkey_synth(chord[i], user_playing)

/**
 * Checks if we should halt playback.
 */
/datum/song/proc/should_stop_playing(mob/user)
	return QDELETED(parent) || !using_instrument || !playing

/**
 * Sanitizes tempo to a value that makes sense and fits the current world.tick_lag.
 */
/datum/song/proc/sanitize_tempo(new_tempo)
	new_tempo = abs(new_tempo)
	return clamp(round(new_tempo, world.tick_lag), world.tick_lag, 5 SECONDS)

/**
 * Gets our beats per minute based on our tempo.
 */
/datum/song/proc/get_bpm()
	return 600 / tempo

/**
 * Sets our tempo from a beats-per-minute, sanitizing it to a valid number first.
 */
/datum/song/proc/set_bpm(bpm)
	tempo = sanitize_tempo(600 / bpm)

/**
 * Updates the window for our users. Override down the line.
 */
/datum/song/proc/updateDialog(mob/user)
	ui_interact(user)

/datum/song/process(wait)
	if(!playing)
		return PROCESS_KILL
	// it's expected this ticks at every world.tick_lag. if it lags, do not attempt to catch up.
	process_song(world.tick_lag)
	process_decay(world.tick_lag)

/**
 * Updates our cached linear/exponential falloff stuff, saving calculations down the line.
 */
/datum/song/proc/update_sustain()
	// Exponential is easy
	cached_exponential_dropoff = sustain_exponential_dropoff
	// Linear, not so much, since it's a target duration from 100 volume rather than an exponential rate.
	var/target_duration = sustain_linear_duration
	var/volume_diff = max(0, 100 - sustain_dropoff_volume)
	var/volume_decrease_per_decisecond = volume_diff / target_duration
	cached_linear_dropoff = volume_decrease_per_decisecond

/**
 * Setter for setting output volume.
 */
/datum/song/proc/set_volume(volume)
	src.volume = clamp(volume, max(0, min_volume), min(100, max_volume))
	update_sustain()
	updateDialog()

/**
 * Setter for setting how low the volume has to get before a note is considered "dead" and dropped
 */
/datum/song/proc/set_dropoff_volume(volume)
	sustain_dropoff_volume = clamp(volume, INSTRUMENT_MIN_SUSTAIN_DROPOFF, 100)
	update_sustain()
	updateDialog()

/**
 * Setter for setting exponential falloff factor.
 */
/datum/song/proc/set_exponential_drop_rate(drop)
	sustain_exponential_dropoff = clamp(drop, INSTRUMENT_EXP_FALLOFF_MIN, INSTRUMENT_EXP_FALLOFF_MAX)
	update_sustain()
	updateDialog()

/**
 * Setter for setting linear falloff duration.
 */
/datum/song/proc/set_linear_falloff_duration(duration)
	sustain_linear_duration = clamp(duration, 0.1, INSTRUMENT_MAX_TOTAL_SUSTAIN)
	update_sustain()
	updateDialog()

/datum/song/vv_edit_var(var_name, var_value)
	. = ..()
	if(.)
		switch(var_name)
			if(NAMEOF(src, volume))
				set_volume(var_value)
			if(NAMEOF(src, sustain_dropoff_volume))
				set_dropoff_volume(var_value)
			if(NAMEOF(src, sustain_exponential_dropoff))
				set_exponential_drop_rate(var_value)
			if(NAMEOF(src, sustain_linear_duration))
				set_linear_falloff_duration(var_value)

// subtype for handheld instruments, like violin
/datum/song/handheld

/datum/song/handheld/updateDialog(mob/user)
	parent.ui_interact(user || usr)

/datum/song/handheld/should_stop_playing(mob/user)
	. = ..()
	if(.)
		return TRUE
	var/obj/item/instrument/I = parent
	return I.should_stop_playing(user)

// subtype for stationary structures, like pianos
/datum/song/stationary

/datum/song/stationary/updateDialog(mob/user)
	parent.ui_interact(user || usr)

/datum/song/stationary/should_stop_playing(mob/user)
	. = ..()
	if(.)
		return TRUE
	var/obj/structure/musician/M = parent
	return M.should_stop_playing(user)


code:
/**
 * Returns the HTML for the status UI for this song datum.
 */
/datum/song/proc/instrument_status_ui()
	. = list()
	. += "
"
	. += "Current instrument: "
	if(!using_instrument)
		. += "[span_danger("No instrument loaded!")]
"
	else
		. += "[using_instrument.name]
"
	. += "Playback Settings:
"
	if(can_noteshift)
		. += "Note Shift/Note Transpose: [note_shift] keys / [round(note_shift / 12, 0.01)] octaves
"
	var/smt
	var/modetext = ""
	switch(sustain_mode)
		if(SUSTAIN_LINEAR)
			smt = "Linear"
			modetext = "Linear Sustain Duration: [sustain_linear_duration / 10] seconds
"
		if(SUSTAIN_EXPONENTIAL)
			smt = "Exponential"
			modetext = "Exponential Falloff Factor: [sustain_exponential_dropoff]% per decisecond
"
	. += "Sustain Mode: [smt]
"
	. += modetext
	. += using_instrument?.ready()? "Status: Ready
" : "Status: !Instrument Definition Error!
"
	. += "Instrument Type: [legacy? "Legacy" : "Synthesized"]
"
	. += "Volume: [volume]
"
	. += "Volume Dropoff Threshold: [sustain_dropoff_volume]
"
	. += "Sustain indefinitely last held note: [full_sustain_held_note? "Enabled" : "Disabled"].
"
	. += "
"

/datum/song/ui_interact(mob/user)
	var/list/dat = list()

	dat += instrument_status_ui()

	if(lines.len > 0)
		dat += "
Playback
"
		if(!playing)
			dat += "Play Stop

"
			dat += "Repeat Song: "
			dat += repeat > 0 ? "--" : "--"
			dat += " [repeat] times "
			dat += repeat < max_repeats ? "++" : "++"
			dat += "
"
		else
			dat += "Play Stop
"
			dat += "Repeats left: [repeat]
"
	if(!editing)
		dat += "
Show Editor
"
	else
		dat += "
Editing
"
		dat += "Hide Editor"
		dat += " Start a New Song"
		dat += " Import a Song

"
		var/bpm = round(600 / tempo)
		dat += "Tempo: - [bpm] BPM +

"
		var/linecount = 0
		for(var/line in lines)
			linecount += 1
			dat += "Line [linecount]: Edit X [line]
"
		dat += "Add Line

"
		if(help)
			dat += "Hide Help
"
			dat += {"
					Lines are a series of chords, separated by commas (,), each with notes separated by hyphens (-).

					Every note in a chord will play together, with chord timed by the tempo.

					Notes are played by the names of the note, and optionally, the accidental, and/or the octave number.

					By default, every note is natural and in octave 3. Defining otherwise is remembered for each note.

					Example: C,D,E,F,G,A,B will play a C major scale.

					After a note has an accidental placed, it will be remembered: C,C4,C,C3 is C3,C4,C4,C3

					Chords can be played simply by seperating each note with a hyphon: A-C#,Cn-E,E-G#,Gn-B

					A pause may be denoted by an empty chord: C,E,,C,G

					To make a chord be a different time, end it with /x, where the chord length will be length

					defined by tempo / x: C,G/2,E/4

					Combined, an example is: E-E4/4,F#/2,G#/8,B/8,E3-E4/4

					Lines may be up to [MUSIC_MAXLINECHARS] characters.

					A song may only contain up to [MUSIC_MAXLINES] lines.

					"}
		else
			dat += "Show Help
"

	var/datum/browser/popup = new(user, "instrument", parent?.name || "instrument", 700, 500)
	popup.set_content(dat.Join(""))
	popup.open()

/**
 * Parses a song the user has input into lines and stores them.
 */
/datum/song/proc/ParseSong(text)
	set waitfor = FALSE
	//split into lines
	lines = splittext(text, "\n")
	if(lines.len)
		var/bpm_string = "BPM: "
		if(findtext(lines[1], bpm_string, 1, length(bpm_string) + 1))
			var/divisor = text2num(copytext(lines[1], length(bpm_string) + 1)) || 120 // default
			tempo = sanitize_tempo(600 / round(divisor, 1))
			lines.Cut(1, 2)
		else
			tempo = sanitize_tempo(5) // default 120 BPM
		if(lines.len > MUSIC_MAXLINES)
			to_chat(usr, "Too many lines!")
			lines.Cut(MUSIC_MAXLINES + 1)
		var/linenum = 1
		for(var/l in lines)
			if(length_char(l) > MUSIC_MAXLINECHARS)
				to_chat(usr, "Line [linenum] too long!")
				lines.Remove(l)
			else
				linenum++
		updateDialog(usr) // make sure updates when complete

/datum/song/Topic(href, href_list)
	if(!usr.canUseTopic(parent, TRUE, FALSE, FALSE, FALSE))
		usr << browse(null, "window=instrument")
		usr.unset_machine()
		return

	parent.add_fingerprint(usr)

	if(href_list["newsong"])
		lines = new()
		tempo = sanitize_tempo(5) // default 120 BPM
		name = ""

	else if(href_list["import"])
		var/t = ""
		do
			t = html_encode(input(usr, "Please paste the entire song, formatted:", text("[]", name), t)  as message)
			if(!in_range(parent, usr))
				return

			if(length_char(t) >= MUSIC_MAXLINES * MUSIC_MAXLINECHARS)
				var/cont = input(usr, "Your message is too long! Would you like to continue editing it?", "", "yes") in list("yes", "no")
				if(cont == "no")
					break
		while(length_char(t) > MUSIC_MAXLINES * MUSIC_MAXLINECHARS)
		ParseSong(t)

	else if(href_list["help"])
		help = text2num(href_list["help"]) - 1

	else if(href_list["edit"])
		editing = text2num(href_list["edit"]) - 1

	if(href_list["repeat"]) //Changing this from a toggle to a number of repeats to avoid infinite loops.
		if(playing)
			return //So that people cant keep adding to repeat. If the do it intentionally, it could result in the server crashing.
		repeat += round(text2num(href_list["repeat"]))
		if(repeat < 0)
			repeat = 0
		if(repeat > max_repeats)
			repeat = max_repeats

	else if(href_list["tempo"])
		tempo = sanitize_tempo(tempo + text2num(href_list["tempo"]))

	else if(href_list["play"])
		INVOKE_ASYNC(src, .proc/start_playing, usr)

	else if(href_list["newline"])
		var/newline = html_encode(input("Enter your line: ", parent.name) as text|null)
		if(!newline || !in_range(parent, usr))
			return
		if(lines.len > MUSIC_MAXLINES)
			return
		if(length(newline) > MUSIC_MAXLINECHARS)
			newline = copytext(newline, 1, MUSIC_MAXLINECHARS)
		lines.Add(newline)

	else if(href_list["deleteline"])
		var/num = round(text2num(href_list["deleteline"]))
		if(num > lines.len || num < 1)
			return
		lines.Cut(num, num+1)

	else if(href_list["modifyline"])
		var/num = round(text2num(href_list["modifyline"]),1)
		var/content = stripped_input(usr, "Enter your line: ", parent.name, lines[num], MUSIC_MAXLINECHARS)
		if(!content || !in_range(parent, usr))
			return
		if(num > lines.len || num < 1)
			return
		lines[num] = content

	else if(href_list["stop"])
		stop_playing()

	else if(href_list["setlinearfalloff"])
		var/amount = input(usr, "Set linear sustain duration in seconds", "Linear Sustain Duration") as null|num
		if(!isnull(amount))
			set_linear_falloff_duration(round(amount * 10, world.tick_lag))

	else if(href_list["setexpfalloff"])
		var/amount = input(usr, "Set exponential sustain factor", "Exponential sustain factor") as null|num
		if(!isnull(amount))
			set_exponential_drop_rate(round(amount, 0.00001))

	else if(href_list["setvolume"])
		var/amount = input(usr, "Set volume", "Volume") as null|num
		if(!isnull(amount))
			set_volume(round(amount, 1))

	else if(href_list["setdropoffvolume"])
		var/amount = input(usr, "Set dropoff threshold", "Dropoff Threshold Volume") as null|num
		if(!isnull(amount))
			set_dropoff_volume(round(amount, 0.01))

	else if(href_list["switchinstrument"])
		if(!length(allowed_instrument_ids))
			return
		else if(length(allowed_instrument_ids) == 1)
			set_instrument(allowed_instrument_ids[1])
			return
		var/list/categories = list()
		for(var/i in allowed_instrument_ids)
			var/datum/instrument/I = SSinstruments.get_instrument(i)
			if(I)
				LAZYSET(categories[I.category || "ERROR CATEGORY"], I.name, I.id)
		var/cat = input(usr, "Select Category", "Instrument Category") as null|anything in categories
		if(!cat)
			return
		var/list/instruments = categories[cat]
		var/choice = input(usr, "Select Instrument", "Instrument Selection") as null|anything in instruments
		if(!choice)
			return
		choice = instruments[choice] //get id
		if(choice)
			set_instrument(choice)

	else if(href_list["setnoteshift"])
		var/amount = input(usr, "Set note shift", "Note Shift") as null|num
		if(!isnull(amount))
			note_shift = clamp(amount, note_shift_min, note_shift_max)

	else if(href_list["setsustainmode"])
		var/choice = input(usr, "Choose a sustain mode", "Sustain Mode") as null|anything in list("Linear", "Exponential")
		switch(choice)
			if("Linear")
				sustain_mode = SUSTAIN_LINEAR
			if("Exponential")
				sustain_mode = SUSTAIN_EXPONENTIAL

	else if(href_list["togglesustainhold"])
		full_sustain_held_note = !full_sustain_held_note

	updateDialog()