Files
Terrarum/src/net/torvald/terrarum/modulebasegame/ui/UICrafting.kt
2024-01-27 22:05:49 +09:00

591 lines
27 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package net.torvald.terrarum.modulebasegame.ui
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.graphics.OrthographicCamera
import com.badlogic.gdx.graphics.g2d.SpriteBatch
import net.torvald.terrarum.*
import net.torvald.terrarum.App.*
import net.torvald.terrarum.ui.UIItemCatBar.Companion.CAT_ALL
import net.torvald.terrarum.gameactors.AVKey
import net.torvald.terrarum.gameitems.GameItem
import net.torvald.terrarum.gameitems.ItemID
import net.torvald.terrarum.gameitems.isWall
import net.torvald.terrarum.itemproperties.CraftingCodex
import net.torvald.terrarum.langpack.Lang
import net.torvald.terrarum.modulebasegame.gameactors.CraftingStation
import net.torvald.terrarum.modulebasegame.gameactors.FixtureInventory
import net.torvald.terrarum.modulebasegame.gameactors.InventoryPair
import net.torvald.terrarum.modulebasegame.ui.UIItemInventoryItemGrid.Companion.listGap
import net.torvald.terrarum.ui.*
import net.torvald.terrarum.ui.UIItemCatBar.Companion.FILTER_CAT_ALL
import net.torvald.unicode.getKeycapPC
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.min
/**
* This UI has inventory, but it's just there to display all craftable items and should not be serialised.
*
* Created by minjaesong on 2022-03-10.
*/
class UICrafting(val full: UIInventoryFull?) : UICanvas(
toggleKeyLiteral = if (full == null) "control_key_inventory" else null,
toggleButtonLiteral = if (full == null) "control_gamepad_start" else null
), HasInventory {
private val catBarx: UIItemCatBar?
get() = full?.catBar
override var width = App.scr.width
override var height = App.scr.height
private val playerThings = UITemplateHalfInventory(this, false).also {
it.itemListTouchDownFun = { gameItem, _, _, _, _ -> recipeClicked?.let { recipe -> gameItem?.let { gameItem ->
val itemID = gameItem.dynamicID
// don't rely on highlightedness of the button to determine the item on the button is the selected
// ingredient (because I don't fully trust my code lol)
val targetItemToAlter = recipe.ingredients.filter { (key, mode) -> // altering recipe doesn't make sense if player selected a recipe that requires no tag-ingredients
val tags = key.split(',')
val wantsWall = tags.contains("WALL")
(mode == CraftingCodex.CraftingItemKeyMode.TAG && gameItem.hasAllTags(tags) && (wantsWall == gameItem.originalID.isWall())) // true if (wants wall and is wall) or (wants no wall and is not wall)
}.let {
if (it.size > 1)
println("[UICrafting] Your recipe seems to have two similar ingredients defined\n" +
"affected ingredients: ${it.joinToString()}\n" +
"the recipe: ${recipe}")
it.firstOrNull()
}
targetItemToAlter?.let { (key, mode) ->
val oldItem = _getItemListIngredients().getInventory().first { (itm, qty) ->
val tags = key.split(',')
val wantsWall = tags.contains("WALL")
(mode == CraftingCodex.CraftingItemKeyMode.TAG && ItemCodex[itm]!!.hasAllTags(tags) && (wantsWall == itm.isWall())) // true if (wants wall and is wall) or (wants no wall and is not wall)
}
changeIngredient(recipe, oldItem, itemID)
refreshCraftButtonStatus()
}
} } }
}
private val itemListCraftable: UIItemCraftingCandidateGrid // might be changed to something else
private val itemListIngredients: UIItemInventoryItemGrid // this one is definitely not to be changed
private val buttonCraft: UIItemTextButton
private val spinnerCraftCount: UIItemSpinner
private val craftables = FixtureInventory() // might be changed to something else
private val ingredients = FixtureInventory() // this one is definitely not to be changed
private val negotiator = object : InventoryTransactionNegotiator() {
override fun accept(player: FixtureInventory, fixture: FixtureInventory, item: GameItem, amount: Long) {
// TODO()
}
override fun refund(fixture: FixtureInventory, player: FixtureInventory, item: GameItem, amount: Long) {
// TODO()
}
}
override fun getNegotiator() = negotiator
override fun getFixtureInventory(): FixtureInventory = craftables
override fun getPlayerInventory(): FixtureInventory = INGAME.actorNowPlaying!!.inventory
private val halfSlotOffset = (UIItemInventoryElemSimple.height + listGap) / 2
private val thisOffsetX = UIInventoryFull.INVENTORY_CELLS_OFFSET_X() + UIItemInventoryElemSimple.height + listGap - halfSlotOffset
private val thisOffsetX2 = thisOffsetX + (listGap + UIItemInventoryElemWide.height) * 7
private val thisXend = thisOffsetX + (listGap + UIItemInventoryElemWide.height) * 13 - listGap
private val thisOffsetY = UIInventoryFull.INVENTORY_CELLS_OFFSET_Y()
private val cellsWidth = (listGap + UIItemInventoryElemWide.height) * 6 - listGap
private val TEXT_GAP = 26
private val LAST_LINE_IN_GRID = ((UIItemInventoryElemWide.height + listGap) * (UIInventoryFull.CELLS_VRT - 2)) + 22//359 // TEMPORARY VALUE!
private var recipeClicked: CraftingCodex.CraftingRecipe? = null
private val controlHelp: String
get() = if (App.environment == RunningEnvironment.PC)
"${getKeycapPC(ControlPresets.getKey("control_key_inventory"))} ${Lang["GAME_ACTION_CLOSE"]}"
else
"$gamepadLabelStart ${Lang["GAME_ACTION_CLOSE"]}\u3000 " +
"$gamepadLabelLEFTRIGHT ${Lang["GAME_OBJECTIVE_MULTIPLIER"]}\u3000 " +
"${App.gamepadLabelWest} ${Lang["GAME_ACTION_CRAFT"]}"
private val oldSelectedItems = ArrayList<ItemID>()
private val craftMult
get() = spinnerCraftCount.value.toLong()
private fun _getItemListPlayer() = playerThings.itemList
private fun _getItemListIngredients() = itemListIngredients
private fun _getItemListCraftables() = itemListCraftable
init {
val craftButtonsY = thisOffsetY + 23 + (UIItemInventoryElemWide.height + listGap) * (UIInventoryFull.CELLS_VRT - 1)
val buttonWidth = (UIItemInventoryElemWide.height + listGap) * 3 - listGap - 2
// ingredient list
itemListIngredients = UIItemInventoryItemGrid(
this,
{ ingredients },
thisOffsetX,
thisOffsetY + LAST_LINE_IN_GRID,
6, 1,
drawScrollOnRightside = false,
drawWallet = false,
hideSidebar = true,
colourTheme = UIItemInventoryCellCommonRes.defaultInventoryCellTheme.copy(
cellHighlightSubCol = Toolkit.Theme.COL_INACTIVE
),
keyDownFun = { _, _, _, _, _ -> },
touchDownFun = { gameItem, amount, _, _, _ -> gameItem?.let { gameItem ->
// if the item is craftable one, load its recipe instead
CraftingRecipeCodex.getRecipesFor(gameItem.originalID)?.let { recipes ->
// select most viable recipe (completely greedy search)
val player = getPlayerInventory()
// list of [Score, Ingredients, Recipe]
recipes.map { recipe ->
// list of (Item, How many player has, How many the recipe requires)
val items = recipeToIngredientRecord(player, recipe, nearbyCraftingStations)
val score = items.fold(1L) { acc, item ->
(item.howManyPlayerHas).times(16L) + 1L
}
listOf(score, items, recipe)
}.maxByOrNull { it[0] as Long }?.let { (_, items, recipe) ->
val items = items as List<List<*>>
val recipe = recipe as CraftingCodex.CraftingRecipe
// change selected recipe to mostViableRecipe then update the UIs accordingly
// FIXME recipe highlighting will not change correctly!
val selectedItems = ArrayList<ItemID>()
// auto-dial the spinner so that player would just have to click the Craft! button (for the most time, that is)
val howManyRequired = craftMult * amount
val howManyPlayerHas = player.searchByID(gameItem.dynamicID)?.qty ?: 0
val howManyPlayerMightNeed = ceil((howManyRequired - howManyPlayerHas).toDouble() / recipe.moq).toLong()
resetSpinner(howManyPlayerMightNeed.coerceIn(1L, App.getConfigInt("basegame:gameplay_max_crafting").toLong()))
ingredients.clear()
recipeClicked = recipe
items.forEach {
val itm = it[0] as ItemID
val qty = it[2] as Long
selectedItems.add(itm)
ingredients.add(itm, qty)
}
_getItemListPlayer().let {
it.removeFromForceHighlightList(oldSelectedItems)
filterPlayerListUsing(recipeClicked)
it.addToForceHighlightList(selectedItems)
it.rebuild(FILTER_CAT_ALL)
}
_getItemListIngredients().rebuild(FILTER_CAT_ALL)
// highlighting CraftingCandidateButton by searching for the buttons that has the recipe
_getItemListCraftables().let {
// turn the highlights off
it.items.forEach { it.forceHighlighted = false }
// search for the recipe
// also need to find what "page" the recipe might be in
// use it.isCompactMode to find out the current mode
var ord = 0
while (ord < it.craftingRecipes.indices.last) {
if (recipeClicked == it.craftingRecipes[ord]) break
ord += 1
}
val itemSize = it.items.size
it.itemPage = ord / itemSize
it.items[ord % itemSize].forceHighlighted = true
}
oldSelectedItems.clear()
oldSelectedItems.addAll(selectedItems)
refreshCraftButtonStatus()
}
}
} }
)
// make sure grid buttons for ingredients do nothing (even if they are hidden!)
itemListIngredients.navRemoCon.listButtonListener = { _,_, -> }
itemListIngredients.navRemoCon.gridButtonListener = { _,_, -> }
itemListIngredients.isCompactMode = true
itemListIngredients.setCustomHighlightRuleSub {
it.item?.let { ingredient ->
return@setCustomHighlightRuleSub getPlayerInventory().searchByID(ingredient.dynamicID)?.let { itemOnPlayer ->
itemOnPlayer.qty * craftMult >= it.amount * craftMult
} == true
}
false
}
// crafting list to the left
itemListCraftable = UIItemCraftingCandidateGrid(
this,
thisOffsetX,
thisOffsetY,
6, UIInventoryFull.CELLS_VRT - 2, // decrease the internal height so that craft/cancel button would fit in
keyDownFun = { _, _, _, _, _ -> },
touchDownFun = { gameItem, amount, _, recipe0, button ->
(recipe0 as? CraftingCodex.CraftingRecipe).let { recipe ->
val selectedItems = ArrayList<ItemID>()
val playerInventory = getPlayerInventory()
ingredients.clear()
recipeClicked = recipe
// printdbg(this, "Recipe selected: $recipe")
recipe?.ingredients?.forEach { ingredient ->
val selectedItem = getItemForIngredient(playerInventory, ingredient)
selectedItems.add(selectedItem)
ingredients.add(selectedItem, ingredient.qty)
}
_getItemListPlayer().removeFromForceHighlightList(oldSelectedItems)
_getItemListPlayer().addToForceHighlightList(selectedItems)
_getItemListPlayer().itemPage = 0
filterPlayerListUsing(recipeClicked)
_getItemListIngredients().rebuild(FILTER_CAT_ALL)
highlightCraftingCandidateButton(recipe)
oldSelectedItems.clear()
oldSelectedItems.addAll(selectedItems)
refreshCraftButtonStatus()
}
}
)
buttonCraft = UIItemTextButton(this,
{ Lang["GAME_ACTION_CRAFT"] }, thisOffsetX + 3 + buttonWidth + listGap, craftButtonsY, buttonWidth, alignment = UIItemTextButton.Companion.Alignment.CENTRE, hasBorder = true)
spinnerCraftCount = UIItemSpinner(this, thisOffsetX + 1, craftButtonsY, 1, 1, App.getConfigInt("basegame:gameplay_max_crafting"), 1, buttonWidth, numberToTextFunction = {"×\u200A${it.toInt()}"})
spinnerCraftCount.selectionChangeListener = {
itemListIngredients.numberMultiplier = it.toLong()
itemListIngredients.rebuild(FILTER_CAT_ALL)
itemListCraftable.numberMultiplier = it.toLong()
itemListCraftable.rebuild(FILTER_CAT_ALL)
refreshCraftButtonStatus()
}
buttonCraft.clickOnceListener = { _,_ ->
getPlayerInventory().let { player -> recipeClicked?.let { recipe ->
// check if player has enough amount of ingredients
val itemCraftable = itemListIngredients.getInventory().all { (itm, qty) ->
(player.searchByID(itm)?.qty ?: -1) >= qty * craftMult
}
if (itemCraftable) {
itemListIngredients.getInventory().forEach { (itm, qty) ->
player.remove(itm, qty * craftMult)
}
player.add(recipe.product, recipe.moq * craftMult)
// reset selection status after a crafting to hide the possible artefact where no-longer-craftable items are still displayed due to ingredient depletion
resetUI() // also clears forcehighlightlist
playerThings.rebuild(FILTER_CAT_ALL)
itemListCraftable.rebuild(FILTER_CAT_ALL)
}
} }
refreshCraftButtonStatus()
}
// make grid mode buttons work together
// itemListCraftable.gridModeButtons[0].clickOnceListener = { _,_ -> setCompact(false) }
// itemListCraftable.gridModeButtons[1].clickOnceListener = { _,_ -> setCompact(true) }
handler.allowESCtoClose = true
addUIitem(itemListCraftable)
addUIitem(itemListIngredients)
addUIitem(playerThings)
addUIitem(spinnerCraftCount)
addUIitem(buttonCraft)
}
private fun filterPlayerListUsing(recipe: CraftingCodex.CraftingRecipe?) {
if (recipe == null)
playerThings.rebuild(FILTER_CAT_ALL)
else {
val items = recipe.ingredients.flatMap {
getItemCandidatesForIngredient(getPlayerInventory(), it).map { it.itm }
}.sorted()
val filterFun = { pair: InventoryPair ->
items.binarySearch(pair.itm) >= 0
}
playerThings.rebuild(filterFun, recipe.product)
}
}
var nearbyCraftingStations = emptyList<String>(); protected set
fun getCraftingStationsWithinReach(): List<String> {
val reach = INGAME.actorNowPlaying!!.actorValue.getAsDouble(AVKey.REACH)!! * (INGAME.actorNowPlaying!!.actorValue.getAsDouble(AVKey.REACHBUFF) ?: 1.0) * INGAME.actorNowPlaying!!.scale
val nearbyCraftingStations = INGAME.findKNearestActors(INGAME.actorNowPlaying!!, 256) {
it is CraftingStation && (distBetweenActors(it, INGAME.actorNowPlaying!!) < reach)
}
return nearbyCraftingStations.flatMap { (it.get() as CraftingStation).tags }
}
private fun changeIngredient(recipe: CraftingCodex.CraftingRecipe?, old: InventoryPair, new: ItemID) {
playerThings.itemList.removeFromForceHighlightList(oldSelectedItems)
oldSelectedItems.remove(old.itm)
oldSelectedItems.add(new)
playerThings.itemList.addToForceHighlightList(oldSelectedItems)
playerThings.itemList.itemPage = 0
filterPlayerListUsing(recipe)
// change highlight status of itemListIngredients
itemListIngredients.getInventory().let {
val amount = old.qty
it.remove(old.itm, amount)
it.add(new, amount)
}
itemListIngredients.rebuild(FILTER_CAT_ALL)
}
private fun highlightCraftingCandidateButton(recipe: CraftingCodex.CraftingRecipe?) { // a proxy function
itemListCraftable.highlightRecipe(recipe)
itemListCraftable.rebuild(FILTER_CAT_ALL)
}
/**
* Updates Craft! button so that the button is correctly highlighted
*/
fun refreshCraftButtonStatus() {
val itemCraftable = if (itemListIngredients.getInventory().totalUniqueCount < 1) false
else getPlayerInventory().let { player ->
// check if player has enough amount of ingredients
itemListIngredients.getInventory().all { (itm, qty) ->
(player.searchByID(itm)?.qty ?: -1) >= qty * craftMult
}
}
buttonCraft.isEnabled = itemCraftable
}
// reset whatever player has selected to null and bring UI to its initial state
fun resetUI() {
// reset spinner
resetSpinner()
// reset selected recipe status
recipeClicked = null
filterPlayerListUsing(recipeClicked)
highlightCraftingCandidateButton(null)
ingredients.clear()
playerThings.removeFromForceHighlightList(oldSelectedItems)
itemListIngredients.rebuild(FILTER_CAT_ALL)
refreshCraftButtonStatus()
}
private fun resetSpinner(value: Long = 1L) {
spinnerCraftCount.resetToSmallest()
itemListIngredients.numberMultiplier = value
itemListCraftable.numberMultiplier = value
}
private var openingClickLatched = false
override fun show() {
nearbyCraftingStations = getCraftingStationsWithinReach()
// printdbg(this, "Nearby crafting stations: $nearbyCraftingStations")
playerThings.setGetInventoryFun { INGAME.actorNowPlaying!!.inventory }
itemListUpdate()
openingClickLatched = Terrarum.mouseDown
UIItemInventoryItemGrid.tooltipShowing.clear()
INGAME.setTooltipMessage(null)
resetUI()
}
private var encumbrancePerc = 0f
private fun itemListUpdate() {
// let itemlists be sorted
itemListCraftable.rebuild(FILTER_CAT_ALL)
playerThings.rebuild(FILTER_CAT_ALL)
encumbrancePerc = getPlayerInventory().let {
it.capacity.toFloat() / it.maxCapacity
}
}
override fun touchDown(screenX: Int, screenY: Int, pointer: Int, button: Int): Boolean {
if (!openingClickLatched) {
return super.touchDown(screenX, screenY, pointer, button)
}
return false
}
override fun updateUI(delta: Float) {
// NO super.update due to an infinite recursion
this.uiItems.forEach { it.update(delta) }
if (openingClickLatched && !Terrarum.mouseDown) openingClickLatched = false
}
override fun renderUI(frameDelta: Float, batch: SpriteBatch, camera: OrthographicCamera) {
// NO super.render due to an infinite recursion
this.uiItems.forEach { it.render(frameDelta, batch, camera) }
batch.color = Color.WHITE
// text label for two inventory grids
val craftingLabel = Lang["GAME_CRAFTING"]
val ingredientsLabel = Lang["GAME_INVENTORY_INGREDIENTS"]
val playerName = INGAME.actorNowPlaying!!.actorValue.getAsString(AVKey.NAME).orEmpty().let { it.ifBlank { Lang["GAME_INVENTORY"] } }
App.fontGame.draw(batch, craftingLabel, thisOffsetX + (cellsWidth - App.fontGame.getWidth(craftingLabel)) / 2, thisOffsetY - TEXT_GAP)
App.fontGame.draw(batch, ingredientsLabel, thisOffsetX + (cellsWidth - App.fontGame.getWidth(ingredientsLabel)) / 2, thisOffsetY + LAST_LINE_IN_GRID - TEXT_GAP)
App.fontGame.draw(batch, playerName, thisOffsetX2 + (cellsWidth - App.fontGame.getWidth(playerName)) / 2, thisOffsetY - TEXT_GAP)
// control hints
val controlHintXPos = thisOffsetX + 2f
blendNormalStraightAlpha(batch)
App.fontGame.draw(batch, controlHelp, controlHintXPos, UIInventoryFull.yEnd - 20)
if (full != null) {
//draw player encumb
// encumbrance meter
val encumbranceText = Lang["GAME_INVENTORY_ENCUMBRANCE"]
// encumbrance bar will go one row down if control help message is too long
val encumbBarXPos = thisXend - UIInventoryCells.weightBarWidth + 36
val encumbBarTextXPos = encumbBarXPos - 6 - App.fontGame.getWidth(encumbranceText)
val encumbBarYPos = UIInventoryFull.yEnd - 20 + 3f +
if (App.fontGame.getWidth(full.listControlHelp) + 2 + controlHintXPos >= encumbBarTextXPos)
App.fontGame.lineHeight
else 0f
App.fontGame.draw(batch, encumbranceText, encumbBarTextXPos, encumbBarYPos - 3f)
// encumbrance bar background
blendNormalStraightAlpha(batch)
val encumbCol = UIItemInventoryCellCommonRes.getHealthMeterColour(1f - encumbrancePerc, 0f, 1f)
val encumbBack = encumbCol mul UIItemInventoryCellCommonRes.meterBackDarkening
batch.color = encumbBack
Toolkit.fillArea(
batch,
encumbBarXPos, encumbBarYPos,
UIInventoryCells.weightBarWidth, UIInventoryFull.controlHelpHeight - 6f
)
// encumbrance bar
batch.color = encumbCol
Toolkit.fillArea(
batch,
encumbBarXPos, encumbBarYPos,
if (full.actor.inventory.capacityMode == FixtureInventory.CAPACITY_MODE_NO_ENCUMBER)
1f
else // make sure 1px is always be seen
min(UIInventoryCells.weightBarWidth, max(1f, UIInventoryCells.weightBarWidth * encumbrancePerc)),
UIInventoryFull.controlHelpHeight - 6f
)
// debug text
batch.color = Color.LIGHT_GRAY
if (App.IS_DEVELOPMENT_BUILD) {
App.fontSmallNumbers.draw(
batch,
"${full.actor.inventory.capacity}/${full.actor.inventory.maxCapacity}",
encumbBarTextXPos,
encumbBarYPos + UIInventoryFull.controlHelpHeight - 4f
)
}
}
blendNormalStraightAlpha(batch)
}
override fun doOpening(delta: Float) {
super.doOpening(delta)
INGAME.setTooltipMessage(null)
}
override fun doClosing(delta: Float) {
super.doClosing(delta)
INGAME.setTooltipMessage(null)
}
override fun endOpening(delta: Float) {
super.endOpening(delta)
UIItemInventoryItemGrid.tooltipShowing.clear()
INGAME.setTooltipMessage(null) // required!
}
override fun endClosing(delta: Float) {
super.endClosing(delta)
resetUI()
UIItemInventoryItemGrid.tooltipShowing.clear()
INGAME.setTooltipMessage(null) // required!
}
override fun dispose() {
}
companion object {
data class RecipeIngredientRecord(
val selectedItem: ItemID,
val howManyPlayerHas: Long,
val howManyRecipeWants: Long,
val craftingStationAvailable: Boolean,
)
fun getItemCandidatesForIngredient(inventory: FixtureInventory, ingredient: CraftingCodex.CraftingIngredients): List<InventoryPair> {
return if (ingredient.keyMode == CraftingCodex.CraftingItemKeyMode.TAG) {
val tags = ingredient.key.split(',')
val wantsWall = tags.contains("WALL")
// If the player has the required item, use it; otherwise, will take an item from the ItemCodex
inventory.filter { (itm, qty) ->
ItemCodex[itm]?.hasAllTags(tags) == true && qty >= ingredient.qty && (wantsWall == itm.isWall()) // true if (wants wall and is wall) or (wants no wall and is not wall)
}
}
else {
listOf(InventoryPair(ingredient.key, -1))
}
}
fun getItemForIngredient(inventory: FixtureInventory, ingredient: CraftingCodex.CraftingIngredients): ItemID {
val candidate = getItemCandidatesForIngredient(inventory, ingredient)
return if (ingredient.keyMode == CraftingCodex.CraftingItemKeyMode.TAG) {
candidate.maxByOrNull { it.qty }?.itm ?: (
(ItemCodex.itemCodex.firstNotNullOfOrNull { if (it.value.hasTag(ingredient.key)) it.key else null }) ?:
throw NullPointerException("Item with tag '${ingredient.key}' not found. Possible cause: game or a module not updated or installed")
)
}
else {
ingredient.key
}
}
/**
* For each ingredient of the recipe, returns list of (ingredient, how many the player has the ingredient, how many the recipe wants)
*/
fun recipeToIngredientRecord(inventory: FixtureInventory, recipe: CraftingCodex.CraftingRecipe, nearbyCraftingStations: List<String>): List<RecipeIngredientRecord> {
val hasStation = if (recipe.workbench.isBlank()) true else nearbyCraftingStations.containsAll(recipe.workbench.split(','))
return recipe.ingredients.map { ingredient ->
val selectedItem = getItemForIngredient(inventory, ingredient)
val howManyPlayerHas = inventory.searchByID(selectedItem)?.qty ?: 0L
val howManyTheRecipeWants = ingredient.qty
RecipeIngredientRecord(selectedItem, howManyPlayerHas, howManyTheRecipeWants, hasStation)
}
}
}
}