[全流程完成]

This commit is contained in:
1820982382
2026-08-17 15:15:04 +08:00
parent 286fc2d2c3
commit 13b981a916
47 changed files with 920 additions and 333 deletions
@@ -33,9 +33,11 @@ import appeng.me.storage.LinkStatusRespectingInventory;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import thaumcraft.api.aspects.Aspect;
import thaumcraft.api.aspects.AspectList;
import thaumcraft.api.wands.WandCap;
import net.minecraft.world.item.crafting.Ingredient;
import thaumcraft.common.items.TCFunctionalItems;
import thaumcraft.common.lib.crafting.ThaumcraftCraftingManager;
import thaumcraft.common.vis.TCVisHelper;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.container.slot.ArcaneCraftingResultSlot;
import thaumicenergistics.common.parts.ArcaneCraftingTerminalPart;
@@ -55,6 +57,34 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
*/
private boolean isCraftingResultBeingTaken = false;
/** vis 恢复检测节流:每 10 tick(0.5 秒)检查一次,避免持续遍历配方。 */
private static final int VIS_RECOVERY_CHECK_INTERVAL = 10;
private int visRecoveryTickCounter = 0;
@Override
public void broadcastChanges() {
super.broadcastChanges();
// vis 充能恢复后自动重算结果槽:结果槽空 + 奥术配方有效 + vis 已足够 → 重新显示产物。
// 节流:仅"待恢复"状态每 10 tick 检查一次(findMatchingArcaneRecipe 只在满足时执行)
var player = getPlayerInventory().player;
if (player.level().isClientSide() || !hasValidRecipe || !resultSlot.getItem().isEmpty()) {
return;
}
if (++visRecoveryTickCounter < VIS_RECOVERY_CHECK_INTERVAL) {
return;
}
visRecoveryTickCounter = 0;
var craftContainer = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_CRAFTING).toContainer();
ItemStack arcaneResult = ThaumcraftCraftingManager.findMatchingArcaneRecipe(craftContainer, player);
if (!arcaneResult.isEmpty()) {
AspectList aspects = ThaumcraftCraftingManager.findMatchingArcaneRecipeAspects(craftContainer, player);
if (aspects != null && hasEnoughVis(aspects)) {
resultSlot.setRequiredAspects(aspects);
resultSlot.setDisplayedCraftingOutput(arcaneResult);
}
}
}
private final ISegmentedInventory craftingInventoryHost;
private final CraftingMatrixSlot[] craftingSlots = new CraftingMatrixSlot[9];
private final ArcaneCraftingResultSlot resultSlot;
@@ -76,9 +106,9 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
}
var linkStatusInventory = new LinkStatusRespectingInventory(host.getInventory(), this::getLinkStatus);
this.resultSlot = new ArcaneCraftingResultSlot(getPlayerInventory().player, craftingGridInv);
this.resultSlot.setRefreshCallback(() -> updateCurrentRecipeAndOutput(false));
this.resultSlot.setContainer(this);
this.resultSlot = new ArcaneCraftingResultSlot(
getPlayerInventory().player, this, craftingGridInv, craftingGridInv,
linkStatusInventory, this.getEnergySource(), this.getActionSource());
this.addSlot(this.resultSlot, SlotSemantics.CRAFTING_RESULT);
var wandInv = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_WAND);
@@ -88,7 +118,6 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
return 1;
}
@Override
public boolean mayPlace(ItemStack stack) {
//只允许 TCFunctionalItems.WandCastingItem 类型的物品放入
@@ -145,6 +174,26 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
private static final int CENTIVIS = 100;
/**
* 复刻 TC port {@code WandCastingItem.consumptionModifier}:法杖 cap 折扣 − 玩家装备/饰品折扣 − 法杖折扣。
* 仅用于显示(GUI 所需 vis),实际扣费由 consumeVisCost 内部同公式处理。
*/
private float getConsumptionModifier(Aspect aspect, boolean crafting) {
var wandInv = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_WAND);
var wandStack = wandInv.getStackInSlot(0);
if (wandStack.isEmpty() || !(wandStack.getItem() instanceof TCFunctionalItems.WandCastingItem wand)) {
return 1.0f;
}
float modifier = wand.consumptionModifier(wandStack, getPlayerInventory().player, aspect, crafting);
return modifier;
}
/** 折扣后的 vis 成本(GUI 显示用)。 */
private int getDiscountedVisCost(Aspect aspect, int rawCost) {
if (rawCost <= 0 || aspect == null) return 0;
return Math.max(0, Math.round(rawCost * getConsumptionModifier(aspect, true)));
}
private boolean hasEnoughVis(AspectList cost) {
ThaumicEnergistics.LOG.debug("[V] hasEnoughVis: cost={}", cost);
if (cost == null || cost.isEmpty()) {
@@ -172,7 +221,6 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
}
private void updateCurrentRecipeAndOutput(boolean forceUpdate) {
ThaumicEnergistics.LOG.debug("[R] === updateCurrentRecipeAndOutput forceUpdate={} ===", forceUpdate);
var player = getPlayerInventory().player;
var level = player.level();
@@ -183,27 +231,20 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
var craftContainer = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_CRAFTING).toContainer();
ItemStack arcaneResult = ThaumcraftCraftingManager.findMatchingArcaneRecipe(craftContainer, player);
ThaumicEnergistics.LOG.debug("[R] findMatchingArcaneRecipe result={}", arcaneResult);
if (!arcaneResult.isEmpty()) {
requiredAspects = ThaumcraftCraftingManager.findMatchingArcaneRecipeAspects(craftContainer, player);
ThaumicEnergistics.LOG.debug("[R] ARCANE MATCH! aspects={}, result={}", requiredAspects, arcaneResult);
foundArcane = true;
if (hasEnoughVis(requiredAspects)) {
ThaumicEnergistics.LOG.debug("[R] Vis OK → showing result");
resultSlot.setDisplayedCraftingOutput(arcaneResult);
} else {
ThaumicEnergistics.LOG.debug("[R] Vis INSUFFICIENT → result empty, but recipe recognized");
resultSlot.setDisplayedCraftingOutput(ItemStack.EMPTY);
}
} else {
ThaumicEnergistics.LOG.debug("[R] No arcane recipe match");
}
}
if (!foundArcane) {
ThaumicEnergistics.LOG.debug("[R] No arcane match, trying vanilla...");
var testItems = new ArrayList<ItemStack>(this.craftingSlots.length);
for (var craftingSlot : this.craftingSlots) {
testItems.add(craftingSlot.getItem().copy());
@@ -212,10 +253,8 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
var recipeHolder = level.getRecipeManager().getRecipeFor(RecipeType.CRAFTING, testInput, level);
if (recipeHolder.isPresent()) {
ItemStack result = recipeHolder.get().value().assemble(testInput, level.registryAccess());
ThaumicEnergistics.LOG.debug("[R] Vanilla match: {}, setting result slot", result);
resultSlot.setDisplayedCraftingOutput(result);
} else {
ThaumicEnergistics.LOG.debug("[R] No vanilla match either, clearing result slot");
// 客户端不清空产物槽,@GuiSync 会同步服务端的正确状态
if (!level.isClientSide()) {
resultSlot.setDisplayedCraftingOutput(ItemStack.EMPTY);
@@ -225,9 +264,7 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
if (foundArcane) {
// 服务端设置 @GuiSync 字段;客户端不应覆盖来自服务端的同步值
ThaumicEnergistics.LOG.debug("[R] foundArcane branch: isClientSide={}", level.isClientSide());
if (level.isClientSide()) {
ThaumicEnergistics.LOG.debug("[R] Client: keeping server-synced values, not overriding");
return;
}
this.hasValidRecipe = true;
@@ -235,32 +272,27 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
var wandInv = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_WAND);
var ws = wandInv.getStackInSlot(0);
resultSlot.setWandStack(ws);
ThaumicEnergistics.LOG.debug("[R] SET state: hasValidRecipe=true, wandStack={}", ws);
if (requiredAspects != null && requiredAspects.size() > 0) {
StringBuilder sb = new StringBuilder();
for (Map.Entry<Aspect, Integer> entry : requiredAspects.aspects.entrySet()) {
if (!sb.isEmpty()) sb.append(",");
sb.append(entry.getKey().getTag()).append("=").append(entry.getValue());
// 存折扣后数值(GUI 显示实际所需 vis,含法杖 + 装备 + 饰品减免)
sb.append(entry.getKey().getTag()).append("=")
.append(getDiscountedVisCost(entry.getKey(), entry.getValue()));
}
this.requiredAspectsData = sb.toString();
ThaumicEnergistics.LOG.debug("[R] aspectsData={}", this.requiredAspectsData);
} else {
this.requiredAspectsData = "";
ThaumicEnergistics.LOG.debug("[R] aspectsData=empty (no aspects)");
}
} else {
ThaumicEnergistics.LOG.debug("[R] else branch: isClientSide={}, levelClass={}",
level.isClientSide(), level.getClass().getSimpleName());
if (level.isClientSide()) {
ThaumicEnergistics.LOG.debug("[R] Client: not overriding synced values (no arcane recipe on client)");
return;
}
this.requiredAspectsData = "";
this.hasValidRecipe = false;
resultSlot.setRequiredAspects(null);
resultSlot.setWandStack(ItemStack.EMPTY);
ThaumicEnergistics.LOG.debug("[R] SET state: hasValidRecipe=false (no arcane recipe)");
}
}