Compare commits

13 Commits

Author SHA1 Message Date
LiuEnder
6b558f34b4 发布 1.0.1
Some checks failed
Build / build (push) Has been cancelled
2026-04-26 21:11:38 +08:00
LiuEnder
0a0333faed 修复服务端加载客户端界面类崩溃
Some checks failed
Build / build (push) Has been cancelled
2026-04-26 20:24:59 +08:00
LiuEnder
a88ac00cde 修正工作流错误
Some checks failed
Build / build (push) Has been cancelled
2026-04-26 17:49:54 +08:00
LiuEnder
8d4903e1f5 refactor: reorganize Config.java with grouped sections and consistent formatting
Some checks failed
Build / build (push) Failing after 6s
Grouped into 8 sections: General/CrossDimension/Teleport/Durability/FoodCost/Effects/RecipeToggles/RecipeOverrides

Unified TOML keys to hierarchical dotted naming (durability.basic, foodCost.basic.foodLevel, etc.)

Standardized comment style and added format example comments
2026-04-26 12:13:13 +08:00
LiuEnder
c2e58fbf0e refactor: remove old team-object filter, split food cost into foodLevel and saturation
Some checks failed
Build / build (push) Has been cancelled
teleportTeamOnly now uses team tag (name) comparison only

foodCost split into foodCost.xxx.foodLevel and foodCost.xxx.saturation, independently configurable

supports percentage and fixed value independently for each
2026-04-26 11:46:51 +08:00
LiuEnder
f8e58cc2f1 feat: configurable food cost and same team tag restriction
Some checks failed
Build / build (push) Has been cancelled
foodCost.xxx replaces halveFood.xxx, supports percentage or fixed value

teleportSameTeamOnly: only target players with matching team name tag
2026-04-26 11:36:40 +08:00
LiuEnder
08cba8216a fix: 修复传送选择界面标题被blur着色器二次模糊
Some checks failed
Build / build (push) Has been cancelled
- super.render()会调用renderBackground施加模糊shader到整个framebuffer

- 改为手动遍历renderables渲染控件,避免触发blur

- 覆盖renderBackground为空操作,防止任何意外调用导致模糊
2026-04-26 11:24:47 +08:00
LiuEnder
b4c09a194e asset: 更新8个魔镜贴图
Some checks failed
Build / build (push) Has been cancelled
2026-04-25 22:09:12 +08:00
LiuEnder
92b9438253 feat: 跨维度/同队伍限制、动态debuff与配方配置、模糊界面修复
Some checks failed
Build / build (push) Has been cancelled
- 选择界面:弃用renderBackground模糊着色器,改为纯色覆盖层,文字清晰

- 跨维度传送:初级/中级默认禁止,高级/永久允许(config可配)

- 传送魔镜:默认仅限同team玩家(teleportTeamOnly可配)

- debuff完全可配置:格式 effect_id,seconds,amplifier;... 每镜独立

- 饱食度减半独立开关(halveFood.xxx)

- 合成配方可通过config覆盖(格式 row1;row2;row3|key=item;...)

- 蓄力满自动触发(onUseTick托管)
2026-04-25 21:22:33 +08:00
LiuEnder
9f85d3c610 修改蓄力使用逻辑;修正选择界面文字 2026-04-25 20:02:39 +08:00
LiuEnder
ceb59bb625 feat: 实现传送魔镜mod完整功能
Some checks failed
Build / build (push) Has been cancelled
- 回城魔镜:传送至出生点(优先床/重生锚,否则世界出生点)

- 传送魔镜:打开玩家选择UI后传送到目标玩家身边

- 4个等级(初级/中级/高级/永久),支持耐久、冷却、副作用配置

- 8个合成配方

- 支持中英文语言

- 修复配置加载顺序问题
2026-04-25 19:02:51 +08:00
LiuEnder
33145ade44 编写readme 2026-04-25 18:23:17 +08:00
LiuEnder
abc303b996 初步设置 2026-04-25 14:45:57 +08:00
42 changed files with 1329 additions and 282 deletions

View File

@@ -19,7 +19,7 @@ jobs:
distribution: 'temurin'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
uses: gradle/actions/setup-gradle@v3
- name: Build with Gradle
run: ./gradlew build

3
.gitignore vendored
View File

@@ -32,6 +32,9 @@ bin/
### Mac OS ###
.DS_Store
### Git Credentials ###
.git-credentials
### Minecraft Modding ###
run/
!**/src/**/run/

View File

@@ -1,25 +1,86 @@
该项目是一个minecraft mod项目。版本1.21.1加载器为neoforge。
mod包名为com.le.teleportmirror 。
目前规划功能如下:
# 一、核心功能分类
魔镜分为两种类型:
Installation information
=======
回城魔镜:使用后快速传送回出生点(优先床/重生锚设置点,否则世界出生点)。
This template repository can be directly cloned to get you started with a new
mod. Simply create a new repository cloned from this one, by following the
instructions provided by [GitHub](https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-repository-from-a-template).
传送魔镜:使用后传送使用者至其他使用者指定的玩家身边。
Once you have your clone, simply open the repository in the IDE of your choice. The usual recommendation for an IDE is either IntelliJ IDEA or Eclipse.
# 二、等级与属性
每种魔镜分多个等级(初级 / 中级 / 高级 / 永久),属性差异包括:
If at any point you are missing libraries in your IDE, or you've run into problems you can
run `gradlew --refresh-dependencies` to refresh the local cache. `gradlew clean` to reset everything
{this does not affect your code} and then start the process again.
属性 说明
耐久度 每次使用消耗1点耗尽后物品消失。初级默认10中级50高级100永久无限可配置
冷却时间 两次使用之间的间隔默认1分钟可配置
副作用 使用后附加负面效果,等级越低副作用越强。
Mapping Names:
============
By default, the MDK is configured to use the official mapping names from Mojang for methods and fields
in the Minecraft codebase. These names are covered by a specific license. All modders should be aware of this
license. For the latest license text, refer to the mapping file itself, or the reference copy here:
https://github.com/NeoForged/NeoForm/blob/main/Mojang.md
# 三、副作用设计(低等级/一次性镜子)
初级反胃10s凋零5s饱食度减半
中级反胃5s凋零3s饱食度减半
高级反胃3s饱食度减半
永久:无
Additional Resources:
==========
Community Documentation: https://docs.neoforged.net/
NeoForged Discord: https://discord.neoforged.net/
四、合成配方(以为例)
## 回城魔镜
### 初级回城魔镜
金锭 指南针 金锭
铁锭 蓝色染色玻璃板 铁锭
铜锭 铁锭 铜锭
### 中级回城魔镜
钻石 末影珍珠 钻石
青金石 初级回城魔镜 青金石
红石 紫水晶碎片 红石
### 高级回城魔镜
钻石 末影之眼 钻石
绿宝石 中级回城魔镜 绿宝石
钻石 烈焰棒 钻石
### 永久回城魔镜
潜影壳 紫颂果 潜影壳
末影珍珠 高级回城魔镜 末影珍珠
潜影壳 烈焰棒 潜影壳
## 传送魔镜
### 初级传送魔镜
金锭 拴绳 金锭
铁锭 橙色染色玻璃板 铁锭
铜锭 铁锭 铜锭
### 中级传送魔镜
钻石 末影珍珠 钻石
青金石 初级传送魔镜 青金石
红石 紫水晶碎片 红石
### 高级传送魔镜
钻石 末影之眼 钻石
绿宝石 中级传送魔镜 绿宝石
钻石 烈焰棒 钻石
### 永久传送魔镜
潜影壳 紫颂果 潜影壳
末影珍珠 高级传送魔镜 末影珍珠
潜影壳 烈焰棒 潜影壳
# 五、特殊机制
待定(保留接口,暂时不实装):一次性魔镜:低成本、低副作用(例如红石+青金石合成),用后即毁。
使用方式:长按右键蓄力使用。传送魔镜需选择目标玩家,给出两种可选的 UI 方案:
长按右键弹出轮盘选择同队伍原版的team的玩家头像。默认方案
右键后输入玩家名字(简单直接)。
# 六、可配置项
各级魔镜耐久度与合成配方。
冷却时间。
副作用类型与强度。
# 七、贴图
创建每个魔镜对应的占位贴图。

View File

@@ -12,28 +12,28 @@ parchment_mappings_version=2024.11.10
# Environment Properties
# You can find the latest versions here: https://projects.neoforged.net/neoforged/neoforge
# The Minecraft version must agree with the Neo version to get a valid artifact
minecraft_version=1.21
minecraft_version=1.21.1
# The Minecraft version range can use any release version of Minecraft as bounds.
# Snapshots, pre-releases, and release candidates are not guaranteed to sort properly
# as they do not follow standard versioning conventions.
minecraft_version_range=[1.21]
minecraft_version_range=[1.21.1]
# The Neo version must agree with the Minecraft version to get a valid artifact
neo_version=21.0.167
neo_version=21.1.227
# The loader version range can only use the major version of FML as bounds
loader_version_range=[1,)
loader_version_range=[2,)
## Mod Properties
# The unique mod identifier for the mod. Must be lowercase in English locale. Must fit the regex [a-z][a-z0-9_]{1,63}
# Must match the String constant located in the main mod class annotated with @Mod.
mod_id=examplemod
mod_id=teleportmirror
# The human-readable display name for the mod.
mod_name=Example Mod
mod_name=Teleport Mirror
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
mod_license=All Rights Reserved
mod_license=MIT
# The mod version. See https://semver.org/
mod_version=1.0.0
mod_version=1.0.1
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
# This should match the base package used for the mod sources.
# See https://maven.apache.org/guides/mini/guide-naming-conventions.html
mod_group_id=com.example.examplemod
mod_group_id=com.le.teleportmirror

View File

@@ -1,42 +0,0 @@
package com.example.examplemod;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Item;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.fml.event.config.ModConfigEvent;
import net.neoforged.neoforge.common.ModConfigSpec;
// An example config class. This is not required, but it's a good idea to have one to keep your config organized.
// Demonstrates how to use Neo's config APIs
public class Config {
private static final ModConfigSpec.Builder BUILDER = new ModConfigSpec.Builder();
public static final ModConfigSpec.BooleanValue LOG_DIRT_BLOCK = BUILDER
.comment("Whether to log the dirt block on common setup")
.define("logDirtBlock", true);
public static final ModConfigSpec.IntValue MAGIC_NUMBER = BUILDER
.comment("A magic number")
.defineInRange("magicNumber", 42, 0, Integer.MAX_VALUE);
public static final ModConfigSpec.ConfigValue<String> MAGIC_NUMBER_INTRODUCTION = BUILDER
.comment("What you want the introduction message to be for the magic number")
.define("magicNumberIntroduction", "The magic number is... ");
// a list of strings that are treated as resource locations for items
public static final ModConfigSpec.ConfigValue<List<? extends String>> ITEM_STRINGS = BUILDER
.comment("A list of items to log on common setup.")
.defineListAllowEmpty("items", List.of("minecraft:iron_ingot"), () -> "", Config::validateItemName);
static final ModConfigSpec SPEC = BUILDER.build();
private static boolean validateItemName(final Object obj) {
return obj instanceof String itemName && BuiltInRegistries.ITEM.containsKey(ResourceLocation.parse(itemName));
}
}

View File

@@ -1,131 +0,0 @@
package com.example.examplemod;
import org.slf4j.Logger;
import com.mojang.logging.LogUtils;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.network.chat.Component;
import net.minecraft.world.food.FoodProperties;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.CreativeModeTab;
import net.minecraft.world.item.CreativeModeTabs;
import net.minecraft.world.item.Item;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.material.MapColor;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.Mod;
import net.neoforged.fml.config.ModConfig;
import net.neoforged.fml.ModContainer;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent;
import net.minecraft.client.Minecraft;
import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent;
import net.neoforged.neoforge.common.NeoForge;
import net.neoforged.neoforge.event.BuildCreativeModeTabContentsEvent;
import net.neoforged.neoforge.event.server.ServerStartingEvent;
import net.neoforged.neoforge.registries.DeferredBlock;
import net.neoforged.neoforge.registries.DeferredHolder;
import net.neoforged.neoforge.registries.DeferredItem;
import net.neoforged.neoforge.registries.DeferredRegister;
// The value here should match an entry in the META-INF/neoforge.mods.toml file
@Mod(ExampleMod.MODID)
public class ExampleMod {
// Define mod id in a common place for everything to reference
public static final String MODID = "examplemod";
// Directly reference a slf4j logger
public static final Logger LOGGER = LogUtils.getLogger();
// Create a Deferred Register to hold Blocks which will all be registered under the "examplemod" namespace
public static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks(MODID);
// Create a Deferred Register to hold Items which will all be registered under the "examplemod" namespace
public static final DeferredRegister.Items ITEMS = DeferredRegister.createItems(MODID);
// Create a Deferred Register to hold CreativeModeTabs which will all be registered under the "examplemod" namespace
public static final DeferredRegister<CreativeModeTab> CREATIVE_MODE_TABS = DeferredRegister.create(Registries.CREATIVE_MODE_TAB, MODID);
// Creates a new Block with the id "examplemod:example_block", combining the namespace and path
public static final DeferredBlock<Block> EXAMPLE_BLOCK = BLOCKS.registerSimpleBlock("example_block", BlockBehaviour.Properties.of().mapColor(MapColor.STONE));
// Creates a new BlockItem with the id "examplemod:example_block", combining the namespace and path
public static final DeferredItem<BlockItem> EXAMPLE_BLOCK_ITEM = ITEMS.registerSimpleBlockItem("example_block", EXAMPLE_BLOCK);
// Creates a new food item with the id "examplemod:example_id", nutrition 1 and saturation 2
public static final DeferredItem<Item> EXAMPLE_ITEM = ITEMS.registerSimpleItem("example_item", new Item.Properties().food(new FoodProperties.Builder()
.alwaysEdible().nutrition(1).saturationModifier(2f).build()));
// Creates a creative tab with the id "examplemod:example_tab" for the example item, that is placed after the combat tab
public static final DeferredHolder<CreativeModeTab, CreativeModeTab> EXAMPLE_TAB = CREATIVE_MODE_TABS.register("example_tab", () -> CreativeModeTab.builder()
.title(Component.translatable("itemGroup.examplemod")) //The language key for the title of your CreativeModeTab
.withTabsBefore(CreativeModeTabs.COMBAT)
.icon(() -> EXAMPLE_ITEM.get().getDefaultInstance())
.displayItems((parameters, output) -> {
output.accept(EXAMPLE_ITEM.get()); // Add the example item to the tab. For your own tabs, this method is preferred over the event
}).build());
// The constructor for the mod class is the first code that is run when your mod is loaded.
// FML will recognize some parameter types like IEventBus or ModContainer and pass them in automatically.
public ExampleMod(IEventBus modEventBus, ModContainer modContainer) {
// Register the commonSetup method for modloading
modEventBus.addListener(this::commonSetup);
// Register the Deferred Register to the mod event bus so blocks get registered
BLOCKS.register(modEventBus);
// Register the Deferred Register to the mod event bus so items get registered
ITEMS.register(modEventBus);
// Register the Deferred Register to the mod event bus so tabs get registered
CREATIVE_MODE_TABS.register(modEventBus);
// Register ourselves for server and other game events we are interested in.
// Note that this is necessary if and only if we want *this* class (ExampleMod) to respond directly to events.
// Do not add this line if there are no @SubscribeEvent-annotated functions in this class, like onServerStarting() below.
NeoForge.EVENT_BUS.register(this);
// Register the item to a creative tab
modEventBus.addListener(this::addCreative);
// Register our mod's ModConfigSpec so that FML can create and load the config file for us
modContainer.registerConfig(ModConfig.Type.COMMON, Config.SPEC);
}
private void commonSetup(FMLCommonSetupEvent event) {
// Some common setup code
LOGGER.info("HELLO FROM COMMON SETUP");
if (Config.LOG_DIRT_BLOCK.getAsBoolean()) {
LOGGER.info("DIRT BLOCK >> {}", BuiltInRegistries.BLOCK.getKey(Blocks.DIRT));
}
LOGGER.info("{}{}", Config.MAGIC_NUMBER_INTRODUCTION.get(), Config.MAGIC_NUMBER.getAsInt());
Config.ITEM_STRINGS.get().forEach((item) -> LOGGER.info("ITEM >> {}", item));
}
// Add the example block item to the building blocks tab
private void addCreative(BuildCreativeModeTabContentsEvent event) {
if (event.getTabKey() == CreativeModeTabs.BUILDING_BLOCKS) {
event.accept(EXAMPLE_BLOCK_ITEM);
}
}
// You can use SubscribeEvent and let the Event Bus discover methods to call
@SubscribeEvent
public void onServerStarting(ServerStartingEvent event) {
// Do something when the server starts
LOGGER.info("HELLO from server starting");
}
// You can use EventBusSubscriber to automatically register all static methods in the class annotated with @SubscribeEvent
@EventBusSubscriber(modid = ExampleMod.MODID, bus = EventBusSubscriber.Bus.MOD, value = Dist.CLIENT)
static class ClientModEvents {
@SubscribeEvent
static void onClientSetup(FMLClientSetupEvent event) {
// Some client setup code
LOGGER.info("HELLO FROM CLIENT SETUP");
LOGGER.info("MINECRAFT NAME >> {}", Minecraft.getInstance().getUser().getName());
}
}
}

View File

@@ -0,0 +1,191 @@
package com.le.teleportmirror;
import net.neoforged.neoforge.common.ModConfigSpec;
public class Config {
private static final ModConfigSpec.Builder BUILDER = new ModConfigSpec.Builder();
// ==================== General ====================
public static final ModConfigSpec.IntValue COOLDOWN_SECONDS = BUILDER
.comment("Cooldown in seconds between mirror uses")
.defineInRange("cooldownSeconds", 60, 0, Integer.MAX_VALUE);
public static final ModConfigSpec.IntValue CHARGE_TICKS = BUILDER
.comment("Ticks required to charge the mirror (20 ticks = 1 second)")
.defineInRange("chargeTicks", 40, 1, Integer.MAX_VALUE);
// ==================== Cross-Dimension ====================
public static final ModConfigSpec.BooleanValue ALLOW_CROSS_DIMENSION_BASIC = BUILDER
.comment("Allow Basic mirrors to teleport across dimensions")
.define("allowCrossDimension.basic", false);
public static final ModConfigSpec.BooleanValue ALLOW_CROSS_DIMENSION_INTERMEDIATE = BUILDER
.comment("Allow Intermediate mirrors to teleport across dimensions")
.define("allowCrossDimension.intermediate", false);
public static final ModConfigSpec.BooleanValue ALLOW_CROSS_DIMENSION_ADVANCED = BUILDER
.comment("Allow Advanced mirrors to teleport across dimensions")
.define("allowCrossDimension.advanced", true);
public static final ModConfigSpec.BooleanValue ALLOW_CROSS_DIMENSION_PERMANENT = BUILDER
.comment("Allow Permanent mirrors to teleport across dimensions")
.define("allowCrossDimension.permanent", true);
// ==================== Teleport Restrictions ====================
public static final ModConfigSpec.BooleanValue TELEPORT_TEAM_ONLY = BUILDER
.comment("Teleport Mirror can only target players with the same team name (tag)")
.define("teleportTeamOnly", false);
// ==================== Durability ====================
public static final ModConfigSpec.IntValue BASIC_DURABILITY = BUILDER
.comment("Durability of Basic tier mirrors")
.defineInRange("durability.basic", 10, 0, Integer.MAX_VALUE);
public static final ModConfigSpec.IntValue INTERMEDIATE_DURABILITY = BUILDER
.comment("Durability of Intermediate tier mirrors")
.defineInRange("durability.intermediate", 50, 0, Integer.MAX_VALUE);
public static final ModConfigSpec.IntValue ADVANCED_DURABILITY = BUILDER
.comment("Durability of Advanced tier mirrors")
.defineInRange("durability.advanced", 100, 0, Integer.MAX_VALUE);
// Permanent tier has no configurable durability (unlimited)
// ==================== Food Cost ====================
// -- Basic --
public static final ModConfigSpec.ConfigValue<String> FOOD_LEVEL_BASIC = BUILDER
.comment("Food level cost for Basic mirrors. \"50%\"=percentage, \"10\"=fixed, \"\"=none")
.define("foodCost.basic.foodLevel", "50%");
public static final ModConfigSpec.ConfigValue<String> SATURATION_COST_BASIC = BUILDER
.comment("Saturation cost for Basic mirrors. \"50%\"=percentage, \"4.0\"=fixed, \"\"=none")
.define("foodCost.basic.saturation", "50%");
// -- Intermediate --
public static final ModConfigSpec.ConfigValue<String> FOOD_LEVEL_INTERMEDIATE = BUILDER
.comment("Food level cost for Intermediate mirrors. \"50%\"=percentage, \"10\"=fixed, \"\"=none")
.define("foodCost.intermediate.foodLevel", "50%");
public static final ModConfigSpec.ConfigValue<String> SATURATION_COST_INTERMEDIATE = BUILDER
.comment("Saturation cost for Intermediate mirrors. \"50%\"=percentage, \"4.0\"=fixed, \"\"=none")
.define("foodCost.intermediate.saturation", "50%");
// -- Advanced --
public static final ModConfigSpec.ConfigValue<String> FOOD_LEVEL_ADVANCED = BUILDER
.comment("Food level cost for Advanced mirrors. \"50%\"=percentage, \"10\"=fixed, \"\"=none")
.define("foodCost.advanced.foodLevel", "50%");
public static final ModConfigSpec.ConfigValue<String> SATURATION_COST_ADVANCED = BUILDER
.comment("Saturation cost for Advanced mirrors. \"50%\"=percentage, \"4.0\"=fixed, \"\"=none")
.define("foodCost.advanced.saturation", "50%");
// -- Permanent --
public static final ModConfigSpec.ConfigValue<String> FOOD_LEVEL_PERMANENT = BUILDER
.comment("Food level cost for Permanent mirrors. \"50%\"=percentage, \"10\"=fixed, \"\"=none")
.define("foodCost.permanent.foodLevel", "");
public static final ModConfigSpec.ConfigValue<String> SATURATION_COST_PERMANENT = BUILDER
.comment("Saturation cost for Permanent mirrors. \"50%\"=percentage, \"4.0\"=fixed, \"\"=none")
.define("foodCost.permanent.saturation", "");
// ==================== Side Effects ====================
// Format per entry: "effect_id,dur_seconds,amplifier;..."
// -- Return Mirrors --
public static final ModConfigSpec.ConfigValue<String> EFFECTS_RETURN_BASIC = BUILDER
.comment("Side effects for Basic Return Mirror")
.define("effects.return.basic", "minecraft:nausea,10,0;minecraft:wither,5,0");
public static final ModConfigSpec.ConfigValue<String> EFFECTS_RETURN_INTERMEDIATE = BUILDER
.comment("Side effects for Intermediate Return Mirror")
.define("effects.return.intermediate", "minecraft:nausea,5,0;minecraft:wither,3,0");
public static final ModConfigSpec.ConfigValue<String> EFFECTS_RETURN_ADVANCED = BUILDER
.comment("Side effects for Advanced Return Mirror")
.define("effects.return.advanced", "minecraft:nausea,3,0");
public static final ModConfigSpec.ConfigValue<String> EFFECTS_RETURN_PERMANENT = BUILDER
.comment("Side effects for Permanent Return Mirror")
.define("effects.return.permanent", "");
// -- Teleport Mirrors --
public static final ModConfigSpec.ConfigValue<String> EFFECTS_TELEPORT_BASIC = BUILDER
.comment("Side effects for Basic Teleport Mirror")
.define("effects.teleport.basic", "minecraft:nausea,10,0;minecraft:wither,5,0");
public static final ModConfigSpec.ConfigValue<String> EFFECTS_TELEPORT_INTERMEDIATE = BUILDER
.comment("Side effects for Intermediate Teleport Mirror")
.define("effects.teleport.intermediate", "minecraft:nausea,5,0;minecraft:wither,3,0");
public static final ModConfigSpec.ConfigValue<String> EFFECTS_TELEPORT_ADVANCED = BUILDER
.comment("Side effects for Advanced Teleport Mirror")
.define("effects.teleport.advanced", "minecraft:nausea,3,0");
public static final ModConfigSpec.ConfigValue<String> EFFECTS_TELEPORT_PERMANENT = BUILDER
.comment("Side effects for Permanent Teleport Mirror")
.define("effects.teleport.permanent", "");
// ==================== Recipe Toggles ====================
public static final ModConfigSpec.BooleanValue ENABLE_BASIC_RECIPE = BUILDER
.comment("Enable crafting recipe for Basic tier mirrors")
.define("enableRecipe.basic", true);
public static final ModConfigSpec.BooleanValue ENABLE_INTERMEDIATE_RECIPE = BUILDER
.comment("Enable crafting recipe for Intermediate tier mirrors")
.define("enableRecipe.intermediate", true);
public static final ModConfigSpec.BooleanValue ENABLE_ADVANCED_RECIPE = BUILDER
.comment("Enable crafting recipe for Advanced tier mirrors")
.define("enableRecipe.advanced", true);
public static final ModConfigSpec.BooleanValue ENABLE_PERMANENT_RECIPE = BUILDER
.comment("Enable crafting recipe for Permanent tier mirrors")
.define("enableRecipe.permanent", true);
// ==================== Recipe Overrides ====================
// Format: "row1;row2;row3|key=item_id;..."
// Example: " C ;CGC; C |C=minecraft:copper_ingot;G=minecraft:glass_pane"
// Empty "" means use the default JSON recipe.
// -- Return Mirrors --
public static final ModConfigSpec.ConfigValue<String> RECIPE_OVERRIDE_RETURN_BASIC = BUILDER
.comment("Override recipe for Basic Return Mirror. Empty = use default.")
.define("recipeOverride.return.basic", "");
public static final ModConfigSpec.ConfigValue<String> RECIPE_OVERRIDE_RETURN_INTERMEDIATE = BUILDER
.comment("Override recipe for Intermediate Return Mirror. Empty = use default.")
.define("recipeOverride.return.intermediate", "");
public static final ModConfigSpec.ConfigValue<String> RECIPE_OVERRIDE_RETURN_ADVANCED = BUILDER
.comment("Override recipe for Advanced Return Mirror. Empty = use default.")
.define("recipeOverride.return.advanced", "");
public static final ModConfigSpec.ConfigValue<String> RECIPE_OVERRIDE_RETURN_PERMANENT = BUILDER
.comment("Override recipe for Permanent Return Mirror. Empty = use default.")
.define("recipeOverride.return.permanent", "");
// -- Teleport Mirrors --
public static final ModConfigSpec.ConfigValue<String> RECIPE_OVERRIDE_TELEPORT_BASIC = BUILDER
.comment("Override recipe for Basic Teleport Mirror. Empty = use default.")
.define("recipeOverride.teleport.basic", "");
public static final ModConfigSpec.ConfigValue<String> RECIPE_OVERRIDE_TELEPORT_INTERMEDIATE = BUILDER
.comment("Override recipe for Intermediate Teleport Mirror. Empty = use default.")
.define("recipeOverride.teleport.intermediate", "");
public static final ModConfigSpec.ConfigValue<String> RECIPE_OVERRIDE_TELEPORT_ADVANCED = BUILDER
.comment("Override recipe for Advanced Teleport Mirror. Empty = use default.")
.define("recipeOverride.teleport.advanced", "");
public static final ModConfigSpec.ConfigValue<String> RECIPE_OVERRIDE_TELEPORT_PERMANENT = BUILDER
.comment("Override recipe for Permanent Teleport Mirror. Empty = use default.")
.define("recipeOverride.teleport.permanent", "");
static final ModConfigSpec SPEC = BUILDER.build();
}

View File

@@ -0,0 +1,284 @@
package com.le.teleportmirror;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.effect.MobEffectInstance;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.food.FoodData;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.UseAnim;
import net.minecraft.world.level.Level;
public class MirrorItem extends Item {
private final MirrorTier tier;
private final MirrorType type;
public MirrorItem(MirrorTier tier, MirrorType type, Properties properties) {
super(properties);
this.tier = tier;
this.type = type;
}
public MirrorTier getTier() {
return tier;
}
public MirrorType getMirrorType() {
return type;
}
@Override
public UseAnim getUseAnimation(ItemStack stack) {
return UseAnim.BOW;
}
@Override
public int getUseDuration(ItemStack stack, LivingEntity entity) {
return 72000;
}
@Override
public InteractionResultHolder<ItemStack> use(Level level, Player player, InteractionHand hand) {
ItemStack stack = player.getItemInHand(hand);
if (player.getCooldowns().isOnCooldown(stack.getItem())) {
return InteractionResultHolder.fail(stack);
}
player.startUsingItem(hand);
return InteractionResultHolder.consume(stack);
}
@Override
public void onUseTick(Level level, LivingEntity livingEntity, ItemStack stack, int remainingUseDuration) {
if (level.isClientSide) {
return;
}
int chargeTicks = Config.CHARGE_TICKS.get();
int usedTicks = getUseDuration(stack, livingEntity) - remainingUseDuration;
if (usedTicks >= chargeTicks) {
if (!(livingEntity instanceof ServerPlayer serverPlayer)) {
return;
}
livingEntity.stopUsingItem();
if (type == MirrorType.RETURN) {
performReturnTeleport(serverPlayer, stack);
} else {
MirrorNetwork.sendOpenSelectionToClient(serverPlayer, tier);
}
}
}
@Override
public void releaseUsing(ItemStack stack, Level level, LivingEntity entity, int timeCharged) {
}
private boolean canCrossDimension() {
return switch (tier) {
case BASIC -> Config.ALLOW_CROSS_DIMENSION_BASIC.get();
case INTERMEDIATE -> Config.ALLOW_CROSS_DIMENSION_INTERMEDIATE.get();
case ADVANCED -> Config.ALLOW_CROSS_DIMENSION_ADVANCED.get();
case PERMANENT -> Config.ALLOW_CROSS_DIMENSION_PERMANENT.get();
};
}
private void performReturnTeleport(ServerPlayer player, ItemStack stack) {
ServerLevel serverLevel = player.serverLevel();
BlockPos targetPos;
ServerLevel targetLevel;
ResourceKey<Level> respawnDimension = player.getRespawnDimension();
BlockPos respawnPos = player.getRespawnPosition();
if (respawnPos != null) {
targetLevel = player.server.getLevel(respawnDimension);
if (targetLevel == null) {
targetLevel = serverLevel;
}
targetPos = respawnPos;
} else {
targetLevel = player.server.getLevel(Level.OVERWORLD);
if (targetLevel == null) {
targetLevel = serverLevel;
}
targetPos = targetLevel.getSharedSpawnPos();
}
if (!canCrossDimension() && targetLevel.dimension() != serverLevel.dimension()) {
return;
}
consumeMirrorUse(player, stack);
player.teleportTo(targetLevel, targetPos.getCenter().x, targetPos.getCenter().y, targetPos.getCenter().z,
player.getYRot(), player.getXRot());
targetLevel.playSound(null, targetPos, SoundEvents.ENDERMAN_TELEPORT, SoundSource.PLAYERS, 1.0f, 1.0f);
}
public void performTeleportToPlayer(ServerPlayer player, ServerPlayer target, ItemStack stack) {
ServerLevel targetLevel = target.serverLevel();
BlockPos targetPos = target.blockPosition();
if (!canCrossDimension() && targetLevel.dimension() != player.serverLevel().dimension()) {
return;
}
consumeMirrorUse(player, stack);
player.teleportTo(targetLevel, targetPos.getCenter().x, targetPos.getCenter().y, targetPos.getCenter().z,
player.getYRot(), player.getXRot());
targetLevel.playSound(null, targetPos, SoundEvents.ENDERMAN_TELEPORT, SoundSource.PLAYERS, 1.0f, 1.0f);
}
private int getConfigMaxDamage() {
return switch (tier) {
case BASIC -> Config.BASIC_DURABILITY.get();
case INTERMEDIATE -> Config.INTERMEDIATE_DURABILITY.get();
case ADVANCED -> Config.ADVANCED_DURABILITY.get();
case PERMANENT -> Integer.MAX_VALUE;
};
}
private void consumeMirrorUse(ServerPlayer player, ItemStack stack) {
applySideEffects(player);
applyCooldown(player);
if (!tier.isPermanent()) {
int maxDamage = getConfigMaxDamage();
if (stack.getDamageValue() + 1 >= maxDamage) {
stack.shrink(1);
player.level().playSound(null, player.blockPosition(),
SoundEvents.ITEM_BREAK, SoundSource.PLAYERS, 1.0f, 1.0f);
} else {
stack.setDamageValue(stack.getDamageValue() + 1);
}
}
}
private void applyCooldown(Player player) {
int cooldownTicks = Config.COOLDOWN_SECONDS.get() * 20;
player.getCooldowns().addCooldown(this, cooldownTicks);
}
private String getEffectsConfig() {
return switch (type) {
case RETURN -> switch (tier) {
case BASIC -> Config.EFFECTS_RETURN_BASIC.get();
case INTERMEDIATE -> Config.EFFECTS_RETURN_INTERMEDIATE.get();
case ADVANCED -> Config.EFFECTS_RETURN_ADVANCED.get();
case PERMANENT -> Config.EFFECTS_RETURN_PERMANENT.get();
};
case TELEPORT -> switch (tier) {
case BASIC -> Config.EFFECTS_TELEPORT_BASIC.get();
case INTERMEDIATE -> Config.EFFECTS_TELEPORT_INTERMEDIATE.get();
case ADVANCED -> Config.EFFECTS_TELEPORT_ADVANCED.get();
case PERMANENT -> Config.EFFECTS_TELEPORT_PERMANENT.get();
};
};
}
private String getFoodLevelConfig() {
return switch (tier) {
case BASIC -> Config.FOOD_LEVEL_BASIC.get();
case INTERMEDIATE -> Config.FOOD_LEVEL_INTERMEDIATE.get();
case ADVANCED -> Config.FOOD_LEVEL_ADVANCED.get();
case PERMANENT -> Config.FOOD_LEVEL_PERMANENT.get();
};
}
private String getSaturationConfig() {
return switch (tier) {
case BASIC -> Config.SATURATION_COST_BASIC.get();
case INTERMEDIATE -> Config.SATURATION_COST_INTERMEDIATE.get();
case ADVANCED -> Config.SATURATION_COST_ADVANCED.get();
case PERMANENT -> Config.SATURATION_COST_PERMANENT.get();
};
}
private void applySideEffects(Player player) {
String effectsConfig = getEffectsConfig();
if (effectsConfig == null || effectsConfig.isBlank()) {
return;
}
String[] effectEntries = effectsConfig.split(";");
for (String entry : effectEntries) {
entry = entry.trim();
if (entry.isEmpty()) continue;
String[] parts = entry.split(",");
if (parts.length < 3) continue;
String effectId = parts[0].trim();
int durationSeconds;
int amplifier;
try {
durationSeconds = Integer.parseInt(parts[1].trim());
amplifier = Integer.parseInt(parts[2].trim());
} catch (NumberFormatException e) {
continue;
}
if (durationSeconds <= 0) continue;
var effectKey = ResourceKey.create(Registries.MOB_EFFECT,
ResourceLocation.parse(effectId));
var effectHolder = BuiltInRegistries.MOB_EFFECT.getHolder(effectKey);
effectHolder.ifPresent(holder ->
player.addEffect(new MobEffectInstance(holder, durationSeconds * 20, amplifier)));
}
applyFoodCost(player);
}
private void applyFoodCost(Player player) {
FoodData foodData = player.getFoodData();
applyCostValue(getFoodLevelConfig(), (float) foodData.getFoodLevel(), newVal -> foodData.setFoodLevel(newVal.intValue()));
applyCostValue(getSaturationConfig(), foodData.getSaturationLevel(), newVal -> foodData.setSaturation(newVal));
}
private void applyCostValue(String config, float currentValue, java.util.function.Consumer<Float> setter) {
if (config == null || config.isBlank()) {
return;
}
String trimmed = config.trim();
if (trimmed.endsWith("%")) {
String pctStr = trimmed.substring(0, trimmed.length() - 1);
try {
float percentage = Float.parseFloat(pctStr) / 100f;
setter.accept(Math.max(0, currentValue * (1f - percentage)));
} catch (NumberFormatException ignored) {
}
} else {
try {
float cost = Float.parseFloat(trimmed);
setter.accept(Math.max(0, currentValue - cost));
} catch (NumberFormatException ignored) {
}
}
}
@Override
public boolean isEnchantable(ItemStack stack) {
return false;
}
@Override
public boolean isFoil(ItemStack stack) {
return tier == MirrorTier.PERMANENT;
}
}

View File

@@ -0,0 +1,167 @@
package com.le.teleportmirror;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.ItemStack;
import net.neoforged.neoforge.network.PacketDistributor;
import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent;
import net.neoforged.neoforge.network.handling.IPayloadContext;
import net.neoforged.neoforge.network.registration.PayloadRegistrar;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class MirrorNetwork {
public static void registerPayloads(final RegisterPayloadHandlersEvent event) {
final PayloadRegistrar registrar = event.registrar(TeleportMirrorMod.MODID).versioned("1.0.0");
registrar.playToClient(
OpenPlayerSelectionPayload.TYPE,
OpenPlayerSelectionPayload.STREAM_CODEC,
MirrorNetwork::handleOpenSelection
);
registrar.playToServer(
RequestTeleportPayload.TYPE,
RequestTeleportPayload.STREAM_CODEC,
MirrorNetwork::handleRequestTeleport
);
}
public static void sendOpenSelectionToClient(ServerPlayer player, MirrorTier tier) {
List<String> names = new ArrayList<>();
List<UUID> uuids = new ArrayList<>();
boolean teamOnly = Config.TELEPORT_TEAM_ONLY.get();
for (ServerPlayer onlinePlayer : player.server.getPlayerList().getPlayers()) {
if (onlinePlayer.getUUID().equals(player.getUUID())) {
continue;
}
if (teamOnly) {
var playerTeam = player.getTeam();
var targetTeam = onlinePlayer.getTeam();
if (playerTeam == null || targetTeam == null) {
continue;
}
if (!playerTeam.getName().equals(targetTeam.getName())) {
continue;
}
}
names.add(onlinePlayer.getName().getString());
uuids.add(onlinePlayer.getUUID());
}
PacketDistributor.sendToPlayer(player, new OpenPlayerSelectionPayload(names, uuids, tier.getName()));
}
private static void handleOpenSelection(OpenPlayerSelectionPayload payload, IPayloadContext context) {
context.enqueueWork(() -> {
try {
Class<?> clientHandlerClass = Class.forName("com.le.teleportmirror.MirrorNetworkClient");
clientHandlerClass.getMethod("openPlayerSelection", OpenPlayerSelectionPayload.class)
.invoke(null, payload);
} catch (ReflectiveOperationException e) {
throw new RuntimeException("Failed to open player selection screen", e);
}
});
}
private static void handleRequestTeleport(RequestTeleportPayload payload, IPayloadContext context) {
context.enqueueWork(() -> {
ServerPlayer sender = (ServerPlayer) context.player();
ServerPlayer target = sender.server.getPlayerList().getPlayer(payload.targetUuid);
if (target == null) {
return;
}
MirrorTier tier = MirrorTier.valueOf(payload.tierName.toUpperCase());
ItemStack mainHand = sender.getMainHandItem();
ItemStack offHand = sender.getOffhandItem();
ItemStack mirrorStack = null;
if (mainHand.getItem() instanceof MirrorItem mirrorItem && mirrorItem.getMirrorType() == MirrorType.TELEPORT) {
mirrorStack = mainHand;
} else if (offHand.getItem() instanceof MirrorItem mirrorItem && mirrorItem.getMirrorType() == MirrorType.TELEPORT) {
mirrorStack = offHand;
}
if (mirrorStack == null || mirrorStack.isEmpty()) {
return;
}
MirrorItem mirrorItem = (MirrorItem) mirrorStack.getItem();
if (mirrorItem.getTier() != tier) {
return;
}
mirrorItem.performTeleportToPlayer(sender, target, mirrorStack);
});
}
public record OpenPlayerSelectionPayload(List<String> playerNames, List<UUID> playerUuids,
String tierName) implements CustomPacketPayload {
public static final Type<OpenPlayerSelectionPayload> TYPE = new Type<>(
ResourceLocation.fromNamespaceAndPath(TeleportMirrorMod.MODID, "open_player_selection"));
public static final StreamCodec<FriendlyByteBuf, OpenPlayerSelectionPayload> STREAM_CODEC = new StreamCodec<>() {
@Override
public OpenPlayerSelectionPayload decode(FriendlyByteBuf buf) {
int size = buf.readVarInt();
List<String> names = new ArrayList<>();
List<UUID> uuids = new ArrayList<>();
for (int i = 0; i < size; i++) {
names.add(buf.readUtf());
uuids.add(buf.readUUID());
}
String tierName = buf.readUtf();
return new OpenPlayerSelectionPayload(names, uuids, tierName);
}
@Override
public void encode(FriendlyByteBuf buf, OpenPlayerSelectionPayload payload) {
buf.writeVarInt(payload.playerNames.size());
for (int i = 0; i < payload.playerNames.size(); i++) {
buf.writeUtf(payload.playerNames.get(i));
buf.writeUUID(payload.playerUuids.get(i));
}
buf.writeUtf(payload.tierName);
}
};
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
}
public record RequestTeleportPayload(UUID targetUuid, String tierName) implements CustomPacketPayload {
public static final Type<RequestTeleportPayload> TYPE = new Type<>(
ResourceLocation.fromNamespaceAndPath(TeleportMirrorMod.MODID, "request_teleport"));
public static final StreamCodec<FriendlyByteBuf, RequestTeleportPayload> STREAM_CODEC = new StreamCodec<>() {
@Override
public RequestTeleportPayload decode(FriendlyByteBuf buf) {
UUID targetUuid = buf.readUUID();
String tierName = buf.readUtf();
return new RequestTeleportPayload(targetUuid, tierName);
}
@Override
public void encode(FriendlyByteBuf buf, RequestTeleportPayload payload) {
buf.writeUUID(payload.targetUuid);
buf.writeUtf(payload.tierName);
}
};
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
}
}

View File

@@ -0,0 +1,16 @@
package com.le.teleportmirror;
import com.le.teleportmirror.screen.PlayerSelectionScreen;
import net.minecraft.client.Minecraft;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.api.distmarker.OnlyIn;
@OnlyIn(Dist.CLIENT)
public class MirrorNetworkClient {
public static void openPlayerSelection(MirrorNetwork.OpenPlayerSelectionPayload payload) {
Minecraft client = Minecraft.getInstance();
MirrorTier tier = MirrorTier.valueOf(payload.tierName().toUpperCase());
client.setScreen(new PlayerSelectionScreen(payload.playerNames(), payload.playerUuids(), tier));
}
}

View File

@@ -0,0 +1,28 @@
package com.le.teleportmirror;
public enum MirrorTier {
BASIC(10, "basic"),
INTERMEDIATE(50, "intermediate"),
ADVANCED(100, "advanced"),
PERMANENT(-1, "permanent");
private final int defaultDurability;
private final String name;
MirrorTier(int defaultDurability, String name) {
this.defaultDurability = defaultDurability;
this.name = name;
}
public int getDefaultDurability() {
return defaultDurability;
}
public String getName() {
return name;
}
public boolean isPermanent() {
return this == PERMANENT;
}
}

View File

@@ -0,0 +1,16 @@
package com.le.teleportmirror;
public enum MirrorType {
RETURN("return"),
TELEPORT("teleport");
private final String name;
MirrorType(String name) {
this.name = name;
}
public String getName() {
return name;
}
}

View File

@@ -0,0 +1,186 @@
package com.le.teleportmirror;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.CreativeModeTab;
import net.minecraft.world.item.CreativeModeTabs;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.RecipeHolder;
import net.minecraft.world.item.crafting.RecipeType;
import net.minecraft.world.item.crafting.ShapedRecipe;
import net.minecraft.world.item.crafting.ShapedRecipePattern;
import net.minecraft.world.item.crafting.Ingredient;
import net.minecraft.world.item.crafting.CraftingBookCategory;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.fml.ModContainer;
import net.neoforged.fml.common.Mod;
import net.neoforged.fml.config.ModConfig;
import net.neoforged.neoforge.common.NeoForge;
import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent;
import net.neoforged.neoforge.registries.DeferredHolder;
import net.neoforged.neoforge.registries.DeferredItem;
import net.neoforged.neoforge.registries.DeferredRegister;
import net.neoforged.neoforge.event.server.ServerStartedEvent;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Mod(TeleportMirrorMod.MODID)
public class TeleportMirrorMod {
public static final String MODID = "teleportmirror";
public static final DeferredRegister.Items ITEMS = DeferredRegister.createItems(MODID);
public static final DeferredRegister<CreativeModeTab> CREATIVE_MODE_TABS =
DeferredRegister.create(Registries.CREATIVE_MODE_TAB, MODID);
public static final DeferredItem<MirrorItem> BASIC_RETURN_MIRROR = ITEMS.register("basic_return_mirror",
() -> new MirrorItem(MirrorTier.BASIC, MirrorType.RETURN,
new Item.Properties().stacksTo(1).durability(MirrorTier.BASIC.getDefaultDurability())));
public static final DeferredItem<MirrorItem> INTERMEDIATE_RETURN_MIRROR = ITEMS.register("intermediate_return_mirror",
() -> new MirrorItem(MirrorTier.INTERMEDIATE, MirrorType.RETURN,
new Item.Properties().stacksTo(1).durability(MirrorTier.INTERMEDIATE.getDefaultDurability())));
public static final DeferredItem<MirrorItem> ADVANCED_RETURN_MIRROR = ITEMS.register("advanced_return_mirror",
() -> new MirrorItem(MirrorTier.ADVANCED, MirrorType.RETURN,
new Item.Properties().stacksTo(1).durability(MirrorTier.ADVANCED.getDefaultDurability())));
public static final DeferredItem<MirrorItem> PERMANENT_RETURN_MIRROR = ITEMS.register("permanent_return_mirror",
() -> new MirrorItem(MirrorTier.PERMANENT, MirrorType.RETURN,
new Item.Properties().stacksTo(1)));
public static final DeferredItem<MirrorItem> BASIC_TELEPORT_MIRROR = ITEMS.register("basic_teleport_mirror",
() -> new MirrorItem(MirrorTier.BASIC, MirrorType.TELEPORT,
new Item.Properties().stacksTo(1).durability(MirrorTier.BASIC.getDefaultDurability())));
public static final DeferredItem<MirrorItem> INTERMEDIATE_TELEPORT_MIRROR = ITEMS.register("intermediate_teleport_mirror",
() -> new MirrorItem(MirrorTier.INTERMEDIATE, MirrorType.TELEPORT,
new Item.Properties().stacksTo(1).durability(MirrorTier.INTERMEDIATE.getDefaultDurability())));
public static final DeferredItem<MirrorItem> ADVANCED_TELEPORT_MIRROR = ITEMS.register("advanced_teleport_mirror",
() -> new MirrorItem(MirrorTier.ADVANCED, MirrorType.TELEPORT,
new Item.Properties().stacksTo(1).durability(MirrorTier.ADVANCED.getDefaultDurability())));
public static final DeferredItem<MirrorItem> PERMANENT_TELEPORT_MIRROR = ITEMS.register("permanent_teleport_mirror",
() -> new MirrorItem(MirrorTier.PERMANENT, MirrorType.TELEPORT,
new Item.Properties().stacksTo(1)));
public static final DeferredHolder<CreativeModeTab, CreativeModeTab> MIRROR_TAB =
CREATIVE_MODE_TABS.register("mirror_tab", () -> CreativeModeTab.builder()
.title(Component.translatable("itemGroup.teleportmirror"))
.withTabsBefore(CreativeModeTabs.COMBAT)
.icon(() -> BASIC_RETURN_MIRROR.get().getDefaultInstance())
.displayItems((parameters, output) -> {
output.accept(BASIC_RETURN_MIRROR.get());
output.accept(INTERMEDIATE_RETURN_MIRROR.get());
output.accept(ADVANCED_RETURN_MIRROR.get());
output.accept(PERMANENT_RETURN_MIRROR.get());
output.accept(BASIC_TELEPORT_MIRROR.get());
output.accept(INTERMEDIATE_TELEPORT_MIRROR.get());
output.accept(ADVANCED_TELEPORT_MIRROR.get());
output.accept(PERMANENT_TELEPORT_MIRROR.get());
}).build());
public TeleportMirrorMod(IEventBus modEventBus, ModContainer modContainer) {
ITEMS.register(modEventBus);
CREATIVE_MODE_TABS.register(modEventBus);
modContainer.registerConfig(ModConfig.Type.COMMON, Config.SPEC);
modEventBus.addListener(this::onRegisterPayloads);
NeoForge.EVENT_BUS.addListener(this::onServerStarted);
}
private void onRegisterPayloads(final RegisterPayloadHandlersEvent event) {
MirrorNetwork.registerPayloads(event);
}
private void onServerStarted(final ServerStartedEvent event) {
registerConfigRecipes(event.getServer().getRecipeManager());
}
private void registerConfigRecipes(net.minecraft.world.item.crafting.RecipeManager manager) {
tryRegisterRecipe(manager, Config.ENABLE_BASIC_RECIPE.get(),
Config.RECIPE_OVERRIDE_RETURN_BASIC.get(), BASIC_RETURN_MIRROR.get(), "basic_return_mirror");
tryRegisterRecipe(manager, Config.ENABLE_INTERMEDIATE_RECIPE.get(),
Config.RECIPE_OVERRIDE_RETURN_INTERMEDIATE.get(), INTERMEDIATE_RETURN_MIRROR.get(), "intermediate_return_mirror");
tryRegisterRecipe(manager, Config.ENABLE_ADVANCED_RECIPE.get(),
Config.RECIPE_OVERRIDE_RETURN_ADVANCED.get(), ADVANCED_RETURN_MIRROR.get(), "advanced_return_mirror");
tryRegisterRecipe(manager, Config.ENABLE_PERMANENT_RECIPE.get(),
Config.RECIPE_OVERRIDE_RETURN_PERMANENT.get(), PERMANENT_RETURN_MIRROR.get(), "permanent_return_mirror");
tryRegisterRecipe(manager, Config.ENABLE_BASIC_RECIPE.get(),
Config.RECIPE_OVERRIDE_TELEPORT_BASIC.get(), BASIC_TELEPORT_MIRROR.get(), "basic_teleport_mirror");
tryRegisterRecipe(manager, Config.ENABLE_INTERMEDIATE_RECIPE.get(),
Config.RECIPE_OVERRIDE_TELEPORT_INTERMEDIATE.get(), INTERMEDIATE_TELEPORT_MIRROR.get(), "intermediate_teleport_mirror");
tryRegisterRecipe(manager, Config.ENABLE_ADVANCED_RECIPE.get(),
Config.RECIPE_OVERRIDE_TELEPORT_ADVANCED.get(), ADVANCED_TELEPORT_MIRROR.get(), "advanced_teleport_mirror");
tryRegisterRecipe(manager, Config.ENABLE_PERMANENT_RECIPE.get(),
Config.RECIPE_OVERRIDE_TELEPORT_PERMANENT.get(), PERMANENT_TELEPORT_MIRROR.get(), "permanent_teleport_mirror");
}
@SuppressWarnings("unchecked")
private void tryRegisterRecipe(net.minecraft.world.item.crafting.RecipeManager manager,
boolean enabled, String override, Item result, String name) {
if (!enabled) return;
if (override.isBlank()) return;
RecipeHolder<?> recipe = parseRecipeOverride(override, result, name);
if (recipe == null) return;
try {
Field recipesField = manager.getClass().getDeclaredField("recipes");
recipesField.setAccessible(true);
Object recipeMap = recipesField.get(manager);
Field byTypeField = recipeMap.getClass().getDeclaredField("byType");
byTypeField.setAccessible(true);
Map<RecipeType<?>, Map<ResourceLocation, RecipeHolder<?>>> byType =
(Map<RecipeType<?>, Map<ResourceLocation, RecipeHolder<?>>>) byTypeField.get(recipeMap);
Map<ResourceLocation, RecipeHolder<?>> crafting = byType
.computeIfAbsent(RecipeType.CRAFTING, k -> new HashMap<>());
crafting.put(recipe.id(), recipe);
} catch (Exception e) {
// Silently ignore - default JSON recipe will be used
}
}
private RecipeHolder<?> parseRecipeOverride(String override, Item result, String name) {
String[] parts = override.split("\\|");
if (parts.length < 2) return null;
String[] rows = parts[0].split(";");
if (rows.length != 3) return null;
Map<Character, Ingredient> key = new HashMap<>();
String[] keyEntries = parts[1].split(";");
for (String entry : keyEntries) {
String[] kv = entry.split("=");
if (kv.length != 2) continue;
char k = kv[0].trim().charAt(0);
String itemId = kv[1].trim();
var item = BuiltInRegistries.ITEM.get(ResourceLocation.parse(itemId));
key.put(k, Ingredient.of(item));
}
List<String> pattern = List.of(rows[0].trim(), rows[1].trim(), rows[2].trim());
ShapedRecipePattern shapedPattern;
try {
shapedPattern = ShapedRecipePattern.of(key, pattern);
} catch (Exception e) {
return null;
}
ShapedRecipe recipe = new ShapedRecipe("", CraftingBookCategory.MISC, shapedPattern, new ItemStack(result));
return new RecipeHolder<>(
ResourceLocation.fromNamespaceAndPath(MODID, name + "_config"),
recipe);
}
}

View File

@@ -0,0 +1,107 @@
package com.le.teleportmirror.screen;
import com.le.teleportmirror.MirrorNetwork;
import com.le.teleportmirror.MirrorTier;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.chat.Component;
import net.neoforged.neoforge.network.PacketDistributor;
import java.util.List;
import java.util.UUID;
public class PlayerSelectionScreen extends Screen {
private final List<String> playerNames;
private final List<UUID> playerUuids;
private final MirrorTier tier;
private int scrollOffset = 0;
private static final int BUTTONS_PER_PAGE = 8;
public PlayerSelectionScreen(List<String> playerNames, List<UUID> playerUuids, MirrorTier tier) {
super(Component.translatable("screen.teleportmirror.select_player"));
this.playerNames = playerNames;
this.playerUuids = playerUuids;
this.tier = tier;
}
@Override
protected void init() {
super.init();
rebuildButtons();
}
private void rebuildButtons() {
this.clearWidgets();
int centerX = this.width / 2;
int startY = 40;
int buttonWidth = 200;
int buttonHeight = 20;
int spacing = 4;
int maxOffset = Math.max(0, playerNames.size() - BUTTONS_PER_PAGE);
if (scrollOffset > maxOffset) scrollOffset = maxOffset;
int endIndex = Math.min(scrollOffset + BUTTONS_PER_PAGE, playerNames.size());
for (int i = scrollOffset; i < endIndex; i++) {
final int idx = i;
int y = startY + (i - scrollOffset) * (buttonHeight + spacing);
this.addRenderableWidget(Button.builder(
Component.literal(playerNames.get(i)),
btn -> selectPlayer(idx))
.pos(centerX - buttonWidth / 2, y)
.size(buttonWidth, buttonHeight)
.build());
}
if (playerNames.size() > BUTTONS_PER_PAGE) {
this.addRenderableWidget(Button.builder(
Component.literal("\u25B2"),
btn -> { scrollOffset = Math.max(0, scrollOffset - 1); rebuildButtons(); })
.pos(centerX + buttonWidth / 2 + 5, startY)
.size(20, 20)
.build());
this.addRenderableWidget(Button.builder(
Component.literal("\u25BC"),
btn -> { scrollOffset = Math.min(maxOffset, scrollOffset + 1); rebuildButtons(); })
.pos(centerX + buttonWidth / 2 + 5, startY + BUTTONS_PER_PAGE * (buttonHeight + spacing) - spacing)
.size(20, 20)
.build());
}
this.addRenderableWidget(Button.builder(
Component.translatable("gui.cancel"),
btn -> this.onClose())
.pos(centerX - 50, this.height - 30)
.size(100, 20)
.build());
}
private void selectPlayer(int index) {
if (index >= 0 && index < playerUuids.size()) {
UUID targetUuid = playerUuids.get(index);
PacketDistributor.sendToServer(new MirrorNetwork.RequestTeleportPayload(targetUuid, tier.getName()));
}
this.onClose();
}
@Override
public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) {
guiGraphics.fill(0, 0, this.width, this.height, 0xC0101010);
guiGraphics.drawCenteredString(this.font, this.title, this.width / 2, 12, 0xFFFFFF);
for (var renderable : this.renderables) {
renderable.render(guiGraphics, mouseX, mouseY, partialTick);
}
}
@Override
public void renderBackground(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) {
}
@Override
public boolean isPauseScreen() {
return false;
}
}

View File

@@ -1,5 +0,0 @@
{
"itemGroup.examplemod": "Example Mod Tab",
"block.examplemod.example_block": "Example Block",
"item.examplemod.example_item": "Example Item"
}

View File

@@ -0,0 +1,13 @@
{
"itemGroup.teleportmirror": "Teleport Mirror",
"item.teleportmirror.basic_return_mirror": "Basic Return Mirror",
"item.teleportmirror.intermediate_return_mirror": "Intermediate Return Mirror",
"item.teleportmirror.advanced_return_mirror": "Advanced Return Mirror",
"item.teleportmirror.permanent_return_mirror": "Permanent Return Mirror",
"item.teleportmirror.basic_teleport_mirror": "Basic Teleport Mirror",
"item.teleportmirror.intermediate_teleport_mirror": "Intermediate Teleport Mirror",
"item.teleportmirror.advanced_teleport_mirror": "Advanced Teleport Mirror",
"item.teleportmirror.permanent_teleport_mirror": "Permanent Teleport Mirror",
"screen.teleportmirror.select_player": "Select Target Player",
"teleportmirror.cooldown": "Mirror is on cooldown"
}

View File

@@ -0,0 +1,13 @@
{
"itemGroup.teleportmirror": "传送魔镜",
"item.teleportmirror.basic_return_mirror": "初级回城魔镜",
"item.teleportmirror.intermediate_return_mirror": "中级回城魔镜",
"item.teleportmirror.advanced_return_mirror": "高级回城魔镜",
"item.teleportmirror.permanent_return_mirror": "永久回城魔镜",
"item.teleportmirror.basic_teleport_mirror": "初级传送魔镜",
"item.teleportmirror.intermediate_teleport_mirror": "中级传送魔镜",
"item.teleportmirror.advanced_teleport_mirror": "高级传送魔镜",
"item.teleportmirror.permanent_teleport_mirror": "永久传送魔镜",
"screen.teleportmirror.select_player": "选择目标玩家",
"teleportmirror.cooldown": "魔镜冷却中"
}

View File

@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "teleportmirror:item/advanced_return_mirror"
}
}

View File

@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "teleportmirror:item/advanced_teleport_mirror"
}
}

View File

@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "teleportmirror:item/basic_return_mirror"
}
}

View File

@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "teleportmirror:item/basic_teleport_mirror"
}
}

View File

@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "teleportmirror:item/intermediate_return_mirror"
}
}

View File

@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "teleportmirror:item/intermediate_teleport_mirror"
}
}

View File

@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "teleportmirror:item/permanent_return_mirror"
}
}

View File

@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "teleportmirror:item/permanent_teleport_mirror"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

View File

@@ -0,0 +1,19 @@
{
"type": "minecraft:crafting_shaped",
"pattern": [
"DED",
"MRM",
"DBD"
],
"key": {
"D": { "item": "minecraft:diamond" },
"E": { "item": "minecraft:ender_eye" },
"M": { "item": "minecraft:emerald" },
"R": { "item": "teleportmirror:intermediate_return_mirror" },
"B": { "item": "minecraft:blaze_rod" }
},
"result": {
"id": "teleportmirror:advanced_return_mirror",
"count": 1
}
}

View File

@@ -0,0 +1,19 @@
{
"type": "minecraft:crafting_shaped",
"pattern": [
"DED",
"MRM",
"DBD"
],
"key": {
"D": { "item": "minecraft:diamond" },
"E": { "item": "minecraft:ender_eye" },
"M": { "item": "minecraft:emerald" },
"R": { "item": "teleportmirror:intermediate_teleport_mirror" },
"B": { "item": "minecraft:blaze_rod" }
},
"result": {
"id": "teleportmirror:advanced_teleport_mirror",
"count": 1
}
}

View File

@@ -0,0 +1,19 @@
{
"type": "minecraft:crafting_shaped",
"pattern": [
"GCG",
"IBI",
"PIP"
],
"key": {
"G": { "item": "minecraft:gold_ingot" },
"C": { "item": "minecraft:compass" },
"B": { "item": "minecraft:blue_stained_glass_pane" },
"I": { "item": "minecraft:iron_ingot" },
"P": { "item": "minecraft:copper_ingot" }
},
"result": {
"id": "teleportmirror:basic_return_mirror",
"count": 1
}
}

View File

@@ -0,0 +1,19 @@
{
"type": "minecraft:crafting_shaped",
"pattern": [
"GLG",
"IOI",
"PIP"
],
"key": {
"G": { "item": "minecraft:gold_ingot" },
"L": { "item": "minecraft:lead" },
"O": { "item": "minecraft:orange_stained_glass_pane" },
"I": { "item": "minecraft:iron_ingot" },
"P": { "item": "minecraft:copper_ingot" }
},
"result": {
"id": "teleportmirror:basic_teleport_mirror",
"count": 1
}
}

View File

@@ -0,0 +1,20 @@
{
"type": "minecraft:crafting_shaped",
"pattern": [
"DED",
"LRL",
"TAT"
],
"key": {
"D": { "item": "minecraft:diamond" },
"E": { "item": "minecraft:ender_pearl" },
"L": { "item": "minecraft:lapis_lazuli" },
"R": { "item": "teleportmirror:basic_return_mirror" },
"T": { "item": "minecraft:redstone" },
"A": { "item": "minecraft:amethyst_shard" }
},
"result": {
"id": "teleportmirror:intermediate_return_mirror",
"count": 1
}
}

View File

@@ -0,0 +1,20 @@
{
"type": "minecraft:crafting_shaped",
"pattern": [
"DED",
"LRL",
"TAT"
],
"key": {
"D": { "item": "minecraft:diamond" },
"E": { "item": "minecraft:ender_pearl" },
"L": { "item": "minecraft:lapis_lazuli" },
"R": { "item": "teleportmirror:basic_teleport_mirror" },
"T": { "item": "minecraft:redstone" },
"A": { "item": "minecraft:amethyst_shard" }
},
"result": {
"id": "teleportmirror:intermediate_teleport_mirror",
"count": 1
}
}

View File

@@ -0,0 +1,19 @@
{
"type": "minecraft:crafting_shaped",
"pattern": [
"SCS",
"ERE",
"SBS"
],
"key": {
"S": { "item": "minecraft:shulker_shell" },
"C": { "item": "minecraft:chorus_fruit" },
"E": { "item": "minecraft:ender_pearl" },
"R": { "item": "teleportmirror:advanced_return_mirror" },
"B": { "item": "minecraft:blaze_rod" }
},
"result": {
"id": "teleportmirror:permanent_return_mirror",
"count": 1
}
}

View File

@@ -0,0 +1,19 @@
{
"type": "minecraft:crafting_shaped",
"pattern": [
"SCS",
"ERE",
"SBS"
],
"key": {
"S": { "item": "minecraft:shulker_shell" },
"C": { "item": "minecraft:chorus_fruit" },
"E": { "item": "minecraft:ender_pearl" },
"R": { "item": "teleportmirror:advanced_teleport_mirror" },
"B": { "item": "minecraft:blaze_rod" }
},
"result": {
"id": "teleportmirror:permanent_teleport_mirror",
"count": 1
}
}

View File

@@ -1,95 +1,33 @@
# This is an example neoforge.mods.toml file. It contains the data relating to the loading mods.
# There are several mandatory fields (#mandatory), and many more that are optional (#optional).
# The overall format is standard TOML format, v0.5.0.
# Note that there are a couple of TOML lists in this file.
# Find more information on toml format here: https://github.com/toml-lang/toml
modLoader="javafml"
# The name of the mod loader type to load - for regular FML @Mod mods it should be javafml
modLoader="javafml" #mandatory
loaderVersion="${loader_version_range}"
# A version range to match for said mod loader - for regular FML @Mod it will be the FML version. This is currently 2.
loaderVersion="${loader_version_range}" #mandatory
# The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties.
# Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here.
license="${mod_license}"
# A URL to refer people to when problems occur with this mod
#issueTrackerURL="https://change.me.to.your.issue.tracker.example.invalid/" #optional
[[mods]]
# A list of mods - how many allowed here is determined by the individual mod loader
[[mods]] #mandatory
modId="${mod_id}"
# The modid of the mod
modId="${mod_id}" #mandatory
version="${mod_version}"
# The version number of the mod
version="${mod_version}" #mandatory
displayName="${mod_name}"
# A display name for the mod
displayName="${mod_name}" #mandatory
# A URL to query for updates for this mod. See the JSON update specification https://docs.neoforged.net/docs/misc/updatechecker/
#updateJSONURL="https://change.me.example.invalid/updates.json" #optional
# A URL for the "homepage" for this mod, displayed in the mod UI
#displayURL="https://change.me.to.your.mods.homepage.example.invalid/" #optional
# A file name (in the root of the mod JAR) containing a logo for display
#logoFile="examplemod.png" #optional
# A text field displayed in the mod UI
#credits="" #optional
# The authors of the mod, displayed in the mod UI (optional)
#authors=""
# The description text for the mod (multi line!) (#mandatory)
description='''
Example mod description.
Teleport Mirror mod adds magical mirrors that allow players to teleport.
Two types: Return Mirror (teleports to spawn) and Teleport Mirror (teleports to another player).
Four tiers: Basic, Intermediate, Advanced, and Permanent.
'''
# The [[mixins]] block allows you to declare your mixin config to FML so that it gets loaded.
#[[mixins]]
#config="${mod_id}.mixins.json"
# The [[accessTransformers]] block allows you to declare where your AT file is.
# If this block is omitted, a fallback attempt will be made to load an AT from META-INF/accesstransformer.cfg
#[[accessTransformers]]
#file="META-INF/accesstransformer.cfg"
# The coremods config file path is not configurable and is always loaded from META-INF/coremods.json
# A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional.
[[dependencies.${mod_id}]] #optional
# the modid of the dependency
modId="neoforge" #mandatory
# The type of the dependency. Can be one of "required", "optional", "incompatible" or "discouraged" (case insensitive).
# 'required' requires the mod to exist, 'optional' does not
# 'incompatible' will prevent the game from loading when the mod exists, and 'discouraged' will show a warning
type="required" #mandatory
# Optional field describing why the dependency is required or why it is incompatible
# reason="..."
# The version range of the dependency
versionRange="[${neo_version},)" #mandatory
# An ordering relationship for the dependency.
# BEFORE - This mod is loaded BEFORE the dependency
# AFTER - This mod is loaded AFTER the dependency
[[dependencies.${mod_id}]]
modId="neoforge"
type="required"
versionRange="[${neo_version},)"
ordering="NONE"
# Side this dependency is applied on - BOTH, CLIENT, or SERVER
side="BOTH"
# Here's another dependency
[[dependencies.${mod_id}]]
modId="minecraft"
type="required"
# This version range declares a minimum of the current minecraft version up to but not including the next major version
versionRange="${minecraft_version_range}"
ordering="NONE"
side="BOTH"
# Features are specific properties of the game environment, that you may want to declare you require. This example declares
# that your mod requires GL version 3.2 or higher. Other features will be added. They are side aware so declaring this won't
# stop your mod loading on the server for example.
#[features.${mod_id}]
#openGLVersion="[3.2,)"