軒云熊 發表於 2025-12-20 04:51

請問有大大知道這個介面是甚麼檔案在管的嗎?

請問有大大知道這個小地圖 是甚麼檔案在管的嗎?希望有大大可以幫幫忙
目前直有顯示玩家而已 我想新增怪物顯示到時候改好我會分享給大家

滅龍猛虎紫電 發表於 2025-12-21 00:14

本帖最後由 滅龍猛虎紫電 於 2025-12-21 00:30 編輯

我只知道抽檔PNG可以帶地圖圖片
控制顯示可能在XML 沒碰過猜測

軒云熊 發表於 2025-12-21 04:57

本帖最後由 軒云熊 於 2025-12-21 05:01 編輯

滅龍猛虎紫電 發表於 2025-12-21 00:14
我只知道抽檔PNG可以帶地圖圖片
控制顯示可能在XML 沒碰過猜測
我是說核心檔案部分希望有大大可以幫忙謝謝你的回覆 ^__^

如果有類似 DAT 解密工具就更好了 可以直接找它的邏輯 >"<

爱吃鱼的大柚 發表於 2025-12-21 19:03

核心原理
实现动态怪物标识,需要完成一个数据流闭环:

​服务端​:实时计算玩家周围怪物的坐标。
​通信​:将怪物坐标列表通过网络封包发送给客户端。
​客户端​:接收封包,解析坐标,并将其转换为小地图上的像素坐标,动态绘制(如红点)。

具体实现方案(基于您拥有的源码)
这是一个高级修改方案,需要对游戏代码有一定的理解。
​方案一:利用现有任务目标标记系统(推荐先尝试)​​
从您的截图看到,游戏本身已有“红色/蓝色/绿色怪物任務”的追踪功能。这是最理想的切入点。

​目标​:尝试将“所有可见怪物”临时标记为一种“可攻击的任务目标”。
​操作​:

在服务端源码中,找到处理“发送周围实体信息”给客户端的函数(通常与玩家视野更新相关)。
修改此函数,使其在发送给客户端的怪物数据封包中,为每个怪物添加一个特殊的“标记状态”标志​(例如,模仿任务怪物的标志位)。
在客户端源码中,找到处理“小地图绘制实体”的函数。修改其逻辑,使其对带有“标记状态”的怪物,​使用一个醒目的图标(如红点)​​ 进行绘制,而非默认的普通点或不绘制。


​优点​:复用游戏原有渲染逻辑,稳定。
​缺点​:需要对网络封包结构和渲染逻辑有深入研究。

​方案二:通过Lua脚本系统实现(如果服务端支持)​​
许多天堂私服核心支持Lua脚本扩展,这是最安全、最模块化的方式。

​服务端(Lua脚本)​​:

编写一个Lua脚本,定时(如每秒)获取指定玩家的位置,并查询其周围一定范围内的所有怪物。
将这些怪物的ID、坐标、类型等信息,通过游戏内建的自定义封包功能发送给对应的客户端。


​客户端(Lua脚本或修改)​​:

如果客户端支持Lua,则编写对应的接收脚本,解析封包,并在小地图的UI层上绘制图标。
如果客户端不支持,则需修改客户端源码,增加一个用于接收和解析此自定义封包的模块,并调用小地图绘制接口。


​优点​:无需修改核心源码,热加载,易于调试和管理。
​缺点​:需要服务器和客户端都支持Lua,且性能需优化(不能过于频繁发送)。

​方案三:直接修改客户端与服务端通信(底层方法)​​
如果Lua不支持,则必须直接修改源码。

​服务端修改​:

定位到 Player 或 World 相关的类,找到 updateSurroundingObjects 或类似名称的函数。
在此函数中,除了发送怪物基础信息外,​新增一个专门用于发送小地图标识的自定义封包。这个封包应尽量简洁,只包含怪物坐标和基础类型。


​客户端修改​:

在客户端网络封包处理模块中,​注册并解析这个新的自定义封包。
将解析到的坐标数据,传递给小地图UI模块。
修改小地图的绘制函数(通常在一个单独的UI类中),使其在绘制地图背景和玩家自身位置后,遍历接收到的怪物坐标列表,在对应位置绘制一个小的红色位图(Sprite)​。


​坐标转换关键​:必须正确将游戏世界坐标 (X, Y) 转换为小地图的屏幕坐标 (pixelX, pixelY)。转换公式通常在客户端已有,需要找到并复用。

给您的最直接建议

​首先确认​:检查您的服务器端和客户端核心是否支持Lua脚本扩展。如果支持,​方案二是首选。
​从查看开始​:在您的服务器端源码中,搜索关键词如 SendNearbyObjects、spawn、NpcInfo 或 AbstractServerPacket,找到发送周围怪物数据给客户端的地方。这是您需要修改的起点。
​绘制测试​:在客户端源码中,搜索 Minimap、SmallMap、DrawMap 等类,先尝试硬编码一个静态的坐标点,看看能否成功在小地图上绘制一个红点。这是验证客户端绘制能力的关键一步。
.........................................................................................................................................
DAT文件解密与解析
DAT文件通常是游戏的资源封包,里面包含了图片、配置、模型等所有客户端资源。

​工具建议​:

​通用工具​:对于《天堂》这类游戏,可以尝试 GameDataViewer、L2FileEdit 或特定私服社区发布的解包工具。
​自研工具​:如果通用工具无效,可能需要根据文件头特征(如 0x00、0x11 等标识)自己编写解密脚本。常见方法是逆向主程序的 OpenFile、ReadFile 等函数,找到解密密钥和算法。


​解密后​:您会得到大量子文件,其中就包含地图相关的 ​PNG图片​ 和 ​XML/TXT配置。

軒云熊 發表於 2025-12-22 16:40

爱吃鱼的大柚 發表於 2025-12-21 19:03
核心原理
实现动态怪物标识,需要完成一个数据流闭环:



請問大大可以告訴我玩家顯示在小地圖上的檔案名稱嗎? 也許那裏可以找到答案

爱吃鱼的大柚 發表於 2025-12-22 19:25

MiniMapUI.xml
<?xml version="1.0"?>
<MiniMapUI Name="MiniMapUI">
        <Window Name="Window" X="485" Y="475" Toggle="false" MostTop="1" Activate="1" Transparent="false">
                <Image Name="Frame" X="4" Y="-9" Surf="3275;1"/>
                <Image Name="Base" X="11" Y="20" Surf="3274" MostTop="1"/>
        </Window>
</MiniMapUI>
既然您已经找到小地图的UI配置文件,可以尝试修改这个XML来添加一个“怪物标记层”​,但这需要客户端代码支持:

​尝试在XML中添加一个图层​:
<?xml version="1.0"?>
<MiniMapUI Name="MiniMapUI">
    <Window Name="Window" X="485" Y="475" Toggle="false" MostTop="1" Activate="1" Transparent="false">
      <Image Name="Frame" X="4" Y="-9" Surf="3275;1"/>
      <Image Name="Base" X="11" Y="20" Surf="3274" MostTop="1"/>
      <!-- 尝试添加一个怪物标记层 -->
      <Layer Name="MonsterMarkers" X="0" Y="0" Visible="true"/>
    </Window>
</MiniMapUI>
​但更实际的途径是​:
在客户端源码中找到绘制小地图的函数
在此函数中,​在绘制玩家自身图标后,添加遍历周围怪物并绘制红点的代码
需要从游戏世界对象中获取怪物坐标列表
具体查找步骤
​搜索资源引用​:
在客户端源码中全局搜索 "3274" 和 "3275"(Base和Frame的资源ID)。找到引用这些ID的代码,那里就是小地图的绘制起点。
​从绘制函数入手​:
一旦找到绘制小地图背景的代码,就在附近寻找绘制玩家图标的函数调用。
​坐标转换是关键​:
找到将游戏世界坐标 (x, y) 转换为小地图屏幕坐标 (screenX, screenY) 的函数。这个函数是您添加怪物标记时必须使用的。
1. 文件功能解析


​**3274.img​:
这是小地图的背景图片文件**。它就是您在游戏小地图窗口中看到的那张静态的、绘制了地形、道路和固定建筑的图片。通常,游戏中的每张大地图(如“歌唱之岛”、“古鲁丁村庄”)都会对应一个这样编号的 .img 文件。


​**3274.til​:
这是关键的地图数据索引文件**。.til 文件通常存储了地图的“块”(Tile)信息,它定义了游戏世界坐标 (X, Y) 与 3274.img 这张背景图片上像素位置之间的映射关系。简单来说,它是一张“转换表”,告诉游戏引擎:当玩家位于游戏世界中的某个坐标时,应该在小地图背景图片的哪个位置绘制代表玩家的箭头。


2. 它们如何与您的目标关联
您希望实现的“实时怪物标识”功能,其技术基础就建立在这个坐标转换上。要实现它,您需要:


​理解坐标转换​:您需要从游戏代码或 .til 文件的结构中,​逆向推导出转换公式。公式大致是:小地图像素X = (游戏世界坐标X - 地图偏移X) * 缩放比例。.til 文件很可能就存储了“偏移量”和“缩放比例”等关键参数。


​找到动态绘制层​:即使您修改了 3274.img 这张背景图(例如直接在上面画上红点),它也是静态的,无法实现“实时”跟踪移动的怪物。您之前找到的 MiniMapUI.xml 只定义了静态的UI框架。真正的动态图标(玩家自身、队友、怪物)是在游戏引擎中,​在渲染小地图背景(3274.img)之后,再实时绘制上去的。


3. 给您的具体操作建议
要实现怪物红点,您不能直接修改 3274.img,而应该:


​解析 .til 文件​:使用十六进制编辑器或编写一个小程序,尝试解析 3274.til 的文件格式,找到坐标转换所需的偏移量和缩放系数。这是最核心的一步。


​定位绘制代码​:在客户端源码中,搜索加载或使用 3274.img 和 3274.til 的代码。找到这个位置,您就找到了小地图引擎的入口。通常,在这段代码的后面,就会接着绘制玩家、NPC等动态图标的函数。


​添加绘制逻辑​:在绘制动态图标的代码段中,您需要插入新的逻辑:

从游戏内存或网络封包中,​获取周围怪物的坐标列表。
利用从 .til 文件解析出的转换公式,将每个怪物的游戏坐标转换为小地图上的像素坐标。
调用游戏的2D绘图函数,在计算出的像素坐标上,​绘制一个红色的点或小图标。



​总结​:3274.img 是小地图的“画布”,3274.til 是这张画布的“比例尺和坐标轴”。您的目标不是修改画布本身,而是要在游戏引擎使用这个画布和比例尺绘图时,​让它多画一种红色的点。请集中精力在客户端源码中,寻找负责“使用 .til 文件转换坐标并绘制图标”的那个核心函数。

我搜一搜核心,搜到了回你

爱吃鱼的大柚 發表於 2025-12-22 20:45

是不是可以从组队的地图显示队友入手?

軒云熊 發表於 2025-12-22 23:40

本帖最後由 軒云熊 於 2025-12-22 23:47 編輯

爱吃鱼的大柚 發表於 2025-12-22 20:45
是不是可以从组队的地图显示队友入手?
謝謝 爱吃鱼的大柚 大大 MiniMapUI 測試過 確定是小地圖雷達框架
显示队友也可以如果能找到 玩家本身 顯示在小地圖上的 檔案名稱 就可以找到答案了

爱吃鱼的大柚 發表於 2025-12-23 07:41

src\com\lineage\server\model\L1Party.java文件路径
文件内搜索
        /**
       * 該隊伍目前人數(同地圖)
       * @return
       */
        public int partyUserInMap(final short mapid) {
                int i = 0;
                if (this._membersList.isEmpty()) {
                        return 0;
                }
                if (this._membersList.size() <= 0) {
                        return 0;
                }
               
                for (final L1PcInstance tgpc : getMembers()) {
                        short tgpcMapid = tgpc.getMapId();
                        if (tgpcMapid == mapid) {
                                i += 1;
                        }
                }
                return i;
        }
       
        public List<L1PcInstance> getMemberList() {
                return _membersList;
        }
       
        public final int checkMentor(final L1Apprentice apprentice) {
                int checkType = 0;
                for (final L1PcInstance member : _membersList) {
                        if (apprentice.getMaster().getId() == member.getId()) {
                                checkType += 4;
                        } else if (apprentice.getTotalList().contains(member)) {
                                checkType++;
                        }
                }
                return checkType;
        }
       
        /**

爱吃鱼的大柚 發表於 2025-12-23 07:43

您要实现“实时标识当前地图的怪物”,在服务端需要编写一个极其相似的方法。以下是仿照此逻辑的伪代码:
/**
* 获取指定地图上的所有怪物列表
* @param mapId 地图ID
* @return 该地图上的怪物列表
*/
public List<L1NpcInstance> getMonstersInMap(final short mapId) {
    List<L1NpcInstance> monstersInMap = new ArrayList<>();
    // 1. 获取全服务器怪物列表(假设通过L1World获取)
    for (final L1NpcInstance npc : L1World.getInstance().getVisibleNpc()) {
      // 2. 筛选:是怪物(非NPC)且位于指定地图
      if (npc instanceof L1MonsterInstance && npc.getMapId() == mapId) {
            monstersInMap.add(npc);
      }
    }
    return monstersInMap;
}

軒云熊 發表於 2025-12-24 09:54

本帖最後由 軒云熊 於 2025-12-24 10:17 編輯

爱吃鱼的大柚 發表於 2025-12-23 07:43
您要实现“实时标识当前地图的怪物”,在服务端需要编写一个极其相似的方法。以下是仿照此逻辑的伪代码:
/ ...

位置在 com.lineage.server.clientpackets.C_LoginToServer;
getFocus()


      /**
         * 遊戲焦點及人物狀態更新
         *
         * @param pc
         */
      private void getFocus(L1PcInstance pc) {//src014
                try {
                        pc.set_showId(-1);// 重置副本編號

                        World.get().addVisibleObject(pc);// 將物件增加到MAP世界裡

                        if ((ConfigTurn.METE_LEVEL > 0) && (pc.getMeteLevel() > 0)) {// 轉生能力
                              pc.resetMeteAbility();
                        }
                        
                        pc.resetPolyPower();

                        pc.sendPackets(new S_OwnCharStatus(pc));// 角色資訊

                        pc.sendPackets(new S_MapID(pc, pc.getMapId(), pc.getMap().isUnderwater()));// 更新角色所在的地圖

                        pc.sendPackets(new S_OwnCharPack(pc));// 物件封包(本身)

                        boolean isTimingmap = MapsTable.get().isTimingMap(pc.getMapId());// 傳送地圖是否為限時地圖
                        /** 加入限時地圖清單 */
                        if (isTimingmap) { // 是限時地圖
                              // 地圖限制時間(秒數)
                              final int maxMapUsetime = MapsTable.get().getMapTime(pc.getMapId()) * 60;
                              // 已使用秒數
                              int usedtime = pc.getMapUseTime(pc.getMapId());
                              // 剩餘時間(秒)
                              int leftTime = (maxMapUsetime - usedtime);

                              MapTimerThread.put(pc, leftTime);
                              // System.out.println("加入限時地圖清單");

                        } else if (MapTimerThread.TIMINGMAP.get(pc) != null) {// 在清單中
                              MapTimerThread.TIMINGMAP.remove(pc);// 移出清單
                              
                              // System.out.println("移出清單");
                        }

                        ArrayList<L1PcInstance> otherPc = World.get().getVisiblePlayer(pc);
                        if (otherPc.size() > 0) {
                              for (L1PcInstance tg : otherPc) {
                                        tg.sendPackets(new S_OtherCharPacks(pc));//// 物件封包(其他人物)
                              }
                        }

                        pc.setVipStatus();
                } catch (Exception e) {
                        _log.error(e.getLocalizedMessage(), e);
                }
      }



這段 pc.sendPackets(new S_MapID(pc, pc.getMapId(), pc.getMap().isUnderwater()));// 更新角色所在的地圖
我ˋ試過 就是 把人物焦點放在小地圖和主畫面地圖上   它是綁在一起的
大大可以幫忙看一下嗎?   看看能否找到標記紅色三角形的片段可能在getFocus()這個方法的其中一個裡
應該有一個封包對應到小地圖 顯示紅色三角形 >"<




爱吃鱼的大柚 發表於 2025-12-24 17:09

本帖最後由 爱吃鱼的大柚 於 2025-12-24 17:22 編輯

小地图相关逻辑深入分析
一、 S_MapID类中的小地图逻辑
1. 核心小地图功能
java
下载
复制
this.writeC(S_WORLD);
this.writeH(mapid);      // 关键:写入地图ID,客户端据此加载对应的小地图
this.writeC(isUnderwater ? 0x01 : 0x00);// 水下状态,可能影响小地图显示
2. 地图ID处理流程

副本地图检测:

if (QuestMapTable.get().isQuestMap(pc.getMapId())) {
    // 副本地图特殊处理
}

副本地图可能有独立的小地图配置

虚拟地图映射:

int newmapid = MapsTable.get().getCopyMapId(mapid);
if (newmapid >= 0) {
    mapid = newmapid;// 使用映射后的地图ID
}

影响:虚拟地图可能共享或拥有独立的小地图资源

3. GM专用构造函数的特殊处理
this.writeC(S_WORLD);
this.writeH(mapid);
this.writeC(0x00);// 固定为非水下状态

// 额外写入固定字节序列
this.writeC(0x10);
this.writeH(0x00f8);
// ... 更多固定数据

GM移动可能绕过正常的小地图加载逻辑

固定字节可能包含特殊的小地图显示标志

二、 S_OwnCharPack类中的小地图逻辑
1. 角色状态对小地图的影响
if (pc.isInvisble() || pc.isGmInvis()) {
    status |= STATUS_INVISIBLE;// 隐身状态,可能影响小地图上自身的显示
}

隐身状态下,角色在小地图上可能不可见

2. 移动状态相关标志
if (pc.isFastMovable()) {
    status |= STATUS_FASTMOVABLE;// 加速状态,可能影响小地图移动动画
}

加速状态可能改变小地图上移动指针的显示效果

3. 坐标和方向信息
writeH(pc.getX());
writeH(pc.getY());
writeC(pc.getHeading());// 角色朝向,影响小地图箭头方向

这些是小地图定位的核心数据

4. 光照范围影响
writeC(pc.getOwnLightSize());// 自身光照范围

可能影响小地图的视野范围或迷雾显示

5. 血盟和队伍信息

writeD(pc.getClanid() > 0 ? pc.getClan().getEmblemId() : 0);// 盟徽
writeS(pc.getClanname());// 血盟名称
writeC(pc.getClanRank() > 0 ? pc.getClanRank() << 4 : 0xb0);// 阶级

if (pc.isInParty()) {
    writeC(100 * pc.getCurrentHp() / pc.getMaxHp());// 队伍中显示血量百分比
} else {
    writeC(255);// 非队伍成员
}

血盟信息可能影响小地图上的标识

队伍血量显示可能与小地图上的队友状态相关

三、小地图相关的重要发现
1. 地图切换的完整流程
复制
玩家移动 → S_MapID(新地图) → 客户端加载小地图资源 → S_OwnCharPack(更新位置)
2. 潜在的小地图问题点

问题1:副本地图的小地图同步

// 离开副本时重置showId,但地图ID可能已经改变
pc.set_showId(-1);

如果副本有特殊小地图,离开时可能需要额外清理

问题2:虚拟地图映射

int newmapid = MapsTable.get().getCopyMapId(mapid);

虚拟地图的小地图资源需要正确配置,否则可能显示错误

问题3:状态同步时机

隐身、加速等状态变化时,可能需要额外的小地图更新包

3. GM功能的特殊处理

GM构造函数中的固定字节序列可能包含强制小地图刷新的标志,但具体含义需要查看客户端代码确认。

四、建议的优化和改进

明确小地图状态标志

在状态位掩码中增加明确的小地图相关标志

如:STATUS_MINIMAP_VISIBLE等

小地图数据单独封装

考虑创建专门的S_MiniMapPack数据包

包含小地图需要的精简数据

状态变化时的主动通知

当影响小地图显示的状态改变时,主动发送更新包

总结

两个文件中小地图相关的逻辑主要体现为:

S_MapID:负责地图切换和小地图资源加载

S_OwnCharPack:负责角色在小地图上的状态和位置显示

爱吃鱼的大柚 發表於 2025-12-24 17:31

这是一个实现怪物在小地图上显示为红点并跟随移动的模拟系统。基于您提供的游戏服务器结构,我创建了一个完整的客户端-服务器模拟,演示如何实现这一功能。

关键技术要点说明:

怪物分类显示系统:

主动攻击怪物显示为红点

被动怪物显示为黄点

BOSS级怪物显示为大红点并标注"BOSS"

实时位置同步机制:

服务器发送S_NPCPack封包通知怪物出现

服务器发送S_MoveNpcPacket封包更新怪物位置

客户端接收并立即更新小地图显示

小地图坐标转换:

将游戏世界坐标转换为小地图像素坐标

只显示玩家视野范围内的怪物

玩家固定在小地图中心,怪物位置相对显示

性能优化考虑:

使用CopyOnWriteArrayList保证线程安全

只更新视野内的怪物,减少绘制开销

根据怪物类型决定绘制细节(名称显示等)

扩展性设计:

可轻松添加更多怪物类型和图标

支持动态添加/移除怪物

可扩展为显示NPC、队友等其他实体import java.util.*;
import java.util.concurrent.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import javax.swing.*;
import java.util.List;

/**
* 怪物小地图红点显示系统模拟
* 演示如何在小地图上显示所有怪物为红点并实时跟随移动
*/

// 1. 怪物数据包类 - 发送怪物出现信息给客户端
class S_NPCPack {
    private final int monsterId;
    private final int x, y;
    private final int gfxId;
    private final int level;
    private final boolean isAggressive;
    private final int minimapType; // 0=不显示, 1=红点, 2=黄点, 3=BOSS
   
    public S_NPCPack(Monster monster) {
      this.monsterId = monster.getId();
      this.x = monster.getX();
      this.y = monster.getY();
      this.gfxId = monster.getGfxId();
      this.level = monster.getLevel();
      this.isAggressive = monster.isAggressive();
      this.minimapType = calculateMinimapType(monster);
    }
   
    private int calculateMinimapType(Monster monster) {
      if (monster.isBoss()) {
            return 3; // BOSS特殊图标
      } else if (monster.isAggressive() || monster.getLevel() > 10) {
            return 1; // 主动怪或高级怪显示红点
      } else {
            return 0; // 被动低级怪不显示
      }
    }
   
    // 获取封包数据(模拟)
    public byte[] getPacketData() {
      // 实际游戏中会使用二进制协议
      // 这里简化返回关键信息
      System.out.println("发送怪物出现封包: ID=" + monsterId +
                         ", 位置=(" + x + "," + y + ")" +
                         ", 小地图类型=" + minimapType);
      return new byte;
    }
   
    public int getMonsterId() { return monsterId; }
    public int getX() { return x; }
    public int getY() { return y; }
    public int getMinimapType() { return minimapType; }
}

// 2. 怪物移动封包类 - 发送怪物移动信息给客户端
class S_MoveNpcPacket {
    private final int monsterId;
    private final int newX, newY;
    private final int heading;
   
    public S_MoveNpcPacket(Monster monster, int newX, int newY, int heading) {
      this.monsterId = monster.getId();
      this.newX = newX;
      this.newY = newY;
      this.heading = heading;
    }
   
    public byte[] getPacketData() {
      System.out.println("发送怪物移动封包: ID=" + monsterId +
                         ", 新位置=(" + newX + "," + newY + ")" +
                         ", 方向=" + heading);
      return new byte;
    }
   
    public int getMonsterId() { return monsterId; }
    public int getNewX() { return newX; }
    public int getNewY() { return newY; }
}

// 3. 怪物对象类
class Monster {
    private static int nextId = 1;
   
    private final int id;
    private int x, y;
    private int gfxId;
    private int level;
    private boolean aggressive;
    private boolean boss;
    private String name;
   
    public Monster(int x, int y, String name, int level, boolean aggressive, boolean boss) {
      this.id = nextId++;
      this.x = x;
      this.y = y;
      this.name = name;
      this.level = level;
      this.aggressive = aggressive;
      this.boss = boss;
      this.gfxId = level * 10; // 模拟图形ID
    }
   
    public void move(int newX, int newY) {
      this.x = newX;
      this.y = newY;
    }
   
    public void randomMove(int mapWidth, int mapHeight) {
      int dx = (int)(Math.random() * 21) - 10; // -10 到 10
      int dy = (int)(Math.random() * 21) - 10;
      
      int newX = Math.max(0, Math.min(mapWidth - 1, x + dx));
      int newY = Math.max(0, Math.min(mapHeight - 1, y + dy));
      
      move(newX, newY);
    }
   
    // Getters
    public int getId() { return id; }
    public int getX() { return x; }
    public int getY() { return y; }
    public int getGfxId() { return gfxId; }
    public int getLevel() { return level; }
    public boolean isAggressive() { return aggressive; }
    public boolean isBoss() { return boss; }
    public String getName() { return name; }
}

// 4. 游戏服务器类 - 管理怪物和发送封包
class GameServer {
    private final List<Monster> monsters = new ArrayList<>();
    private final List<Client> clients = new ArrayList<>();
    private final int mapWidth = 5000;
    private final int mapHeight = 5000;
   
    // 添加怪物
    public void addMonster(Monster monster) {
      monsters.add(monster);
      // 通知所有客户端怪物出现
      broadcastToAllClients(new S_NPCPack(monster));
    }
   
    // 添加客户端
    public void addClient(Client client) {
      clients.add(client);
      // 发送所有已有怪物信息给新客户端
      for (Monster monster : monsters) {
            client.receivePacket(new S_NPCPack(monster));
      }
    }
   
    // 广播给所有客户端
    private void broadcastToAllClients(Object packet) {
      for (Client client : clients) {
            client.receivePacket(packet);
      }
    }
   
    // 更新所有怪物位置(模拟怪物移动)
    public void updateMonsterPositions() {
      for (Monster monster : monsters) {
            int oldX = monster.getX();
            int oldY = monster.getY();
            
            // 随机移动怪物
            monster.randomMove(mapWidth, mapHeight);
            
            // 如果位置改变,通知客户端
            if (monster.getX() != oldX || monster.getY() != oldY) {
                int heading = calculateHeading(oldX, oldY, monster.getX(), monster.getY());
                broadcastToAllClients(new S_MoveNpcPacket(monster,
                  monster.getX(), monster.getY(), heading));
            }
      }
    }
   
    // 计算移动方向
    private int calculateHeading(int fromX, int fromY, int toX, int toY) {
      double angle = Math.atan2(toY - fromY, toX - fromX);
      double degree = Math.toDegrees(angle);
      if (degree < 0) degree += 360;
      
      // 转换为8方向
      if (degree >= 337.5 || degree < 22.5) return 0;   // 东
      if (degree >= 22.5 && degree < 67.5) return 1;    // 东北
      if (degree >= 67.5 && degree < 112.5) return 2;   // 北
      if (degree >= 112.5 && degree < 157.5) return 3;// 西北
      if (degree >= 157.5 && degree < 202.5) return 4;// 西
      if (degree >= 202.5 && degree < 247.5) return 5;// 西南
      if (degree >= 247.5 && degree < 292.5) return 6;// 南
      return 7; // 东南
    }
   
    public List<Monster> getMonsters() { return monsters; }
    public int getMapWidth() { return mapWidth; }
    public int getMapHeight() { return mapHeight; }
}

// 5. 小地图显示类(客户端)
class MiniMapPanel extends JPanel {
    private final List<MonsterInfo> monsterInfos = new CopyOnWriteArrayList<>();
    private final int playerX = 2500; // 玩家固定在小地图中心
    private final int playerY = 2500;
    private final int minimapSize = 400; // 小地图显示大小
    private final int viewRange = 1000; // 小地图显示范围
   
    // 怪物信息类(客户端存储)
    static class MonsterInfo {
      int id;
      int x, y;
      int type; // 1=红点, 2=黄点, 3=BOSS
      String name;
      
      MonsterInfo(int id, int x, int y, int type, String name) {
            this.id = id;
            this.x = x;
            this.y = y;
            this.type = type;
            this.name = name;
      }
    }
   
    public MiniMapPanel() {
      setPreferredSize(new Dimension(minimapSize + 50, minimapSize + 50));
      setBackground(Color.DARK_GRAY);
    }
   
    // 更新怪物位置
    public void updateMonsterPosition(int monsterId, int x, int y) {
      for (MonsterInfo info : monsterInfos) {
            if (info.id == monsterId) {
                info.x = x;
                info.y = y;
                repaint();
                return;
            }
      }
    }
   
    // 添加怪物到小地图
    public void addMonster(int monsterId, int x, int y, int type, String name) {
      monsterInfos.add(new MonsterInfo(monsterId, x, y, type, name));
      repaint();
    }
   
    // 移除怪物(当怪物死亡时)
    public void removeMonster(int monsterId) {
      monsterInfos.removeIf(info -> info.id == monsterId);
      repaint();
    }
   
    @Override
    protected void paintComponent(Graphics g) {
      super.paintComponent(g);
      Graphics2D g2d = (Graphics2D) g;
      
      // 绘制小地图背景
      g2d.setColor(new Color(50, 50, 50));
      g2d.fillRect(25, 25, minimapSize, minimapSize);
      
      // 绘制小地图边框
      g2d.setColor(Color.WHITE);
      g2d.drawRect(25, 25, minimapSize, minimapSize);
      
      // 绘制玩家位置(中心点)
      g2d.setColor(Color.GREEN);
      int playerMapX = minimapSize / 2 + 25;
      int playerMapY = minimapSize / 2 + 25;
      g2d.fillOval(playerMapX - 5, playerMapY - 5, 10, 10);
      g2d.setColor(Color.WHITE);
      g2d.drawString("玩家", playerMapX - 10, playerMapY - 10);
      
      // 绘制怪物红点
      for (MonsterInfo monster : monsterInfos) {
            if (monster.type == 0) continue; // 不显示的怪物
            
            // 计算怪物在小地图上的相对位置
            int monsterMapX = 25 + (int)((monster.x - (playerX - viewRange / 2)) *
                minimapSize / viewRange);
            int monsterMapY = 25 + (int)((monster.y - (playerY - viewRange / 2)) *
                minimapSize / viewRange);
            
            // 只显示在视野范围内的怪物
            if (monsterMapX >= 25 && monsterMapX <= 25 + minimapSize &&
                monsterMapY >= 25 && monsterMapY <= 25 + minimapSize) {
               
                // 根据类型设置颜色
                switch (monster.type) {
                  case 1: // 红点(普通主动怪)
                        g2d.setColor(Color.RED);
                        break;
                  case 2: // 黄点(被动怪)
                        g2d.setColor(Color.YELLOW);
                        break;
                  case 3: // BOSS(大红色)
                        g2d.setColor(new Color(255, 0, 0, 200));
                        g2d.fillOval(monsterMapX - 8, monsterMapY - 8, 16, 16);
                        g2d.setColor(Color.WHITE);
                        g2d.drawString("BOSS", monsterMapX - 15, monsterMapY - 15);
                        continue;
                }
               
                // 绘制怪物点
                g2d.fillOval(monsterMapX - 4, monsterMapY - 4, 8, 8);
               
                // 绘制怪物名称(简化显示)
                if (monster.type == 1) {
                  g2d.setColor(Color.WHITE);
                  g2d.drawString(monster.name.substring(0, Math.min(3, monster.name.length())),
                        monsterMapX - 5, monsterMapY - 10);
                }
            }
      }
      
      // 绘制图例
      g2d.setColor(Color.WHITE);
      g2d.drawString("小地图图例:", 25, minimapSize + 50);
      g2d.setColor(Color.GREEN);
      g2d.fillOval(80, minimapSize + 45, 8, 8);
      g2d.setColor(Color.WHITE);
      g2d.drawString("玩家", 95, minimapSize + 52);
      
      g2d.setColor(Color.RED);
      g2d.fillOval(140, minimapSize + 45, 8, 8);
      g2d.setColor(Color.WHITE);
      g2d.drawString("主动怪", 155, minimapSize + 52);
      
      g2d.setColor(Color.YELLOW);
      g2d.fillOval(210, minimapSize + 45, 8, 8);
      g2d.setColor(Color.WHITE);
      g2d.drawString("被动怪", 225, minimapSize + 52);
    }
}

// 6. 客户端类 - 接收服务器封包并更新小地图
class Client {
    private final MiniMapPanel minimap;
    private final String playerName;
   
    public Client(String playerName, MiniMapPanel minimap) {
      this.playerName = playerName;
      this.minimap = minimap;
    }
   
    // 接收服务器封包并处理
    public void receivePacket(Object packet) {
      if (packet instanceof S_NPCPack) {
            S_NPCPack npcPack = (S_NPCPack) packet;
            if (npcPack.getMinimapType() > 0) {
                minimap.addMonster(npcPack.getMonsterId(),
                  npcPack.getX(), npcPack.getY(),
                  npcPack.getMinimapType(), "怪物" + npcPack.getMonsterId());
                System.out.println("客户端收到怪物出现: ID=" + npcPack.getMonsterId());
            }
      } else if (packet instanceof S_MoveNpcPacket) {
            S_MoveNpcPacket movePacket = (S_MoveNpcPacket) packet;
            minimap.updateMonsterPosition(movePacket.getMonsterId(),
                movePacket.getNewX(), movePacket.getNewY());
            System.out.println("客户端收到怪物移动: ID=" + movePacket.getMonsterId() +
                           " -> (" + movePacket.getNewX() + "," + movePacket.getNewY() + ")");
      }
    }
}

// 7. 主程序 - 模拟游戏运行
public class MonsterMinimapSystem {
    private static GameServer server;
    private static MiniMapPanel minimap;
    private static JFrame frame;
   
    public static void main(String[] args) {
      // 初始化服务器
      server = new GameServer();
      
      // 创建小地图UI
      minimap = new MiniMapPanel();
      
      // 创建客户端
      Client client = new Client("测试玩家", minimap);
      server.addClient(client);
      
      // 创建并显示UI
      SwingUtilities.invokeLater(() -> {
            frame = new JFrame("怪物小地图红点显示系统");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setLayout(new BorderLayout());
            
            // 信息面板
            JPanel infoPanel = new JPanel();
            infoPanel.setLayout(new FlowLayout());
            JLabel infoLabel = new JLabel("模拟所有怪物在小地图显示为红点,红点实时跟随怪物移动");
            infoPanel.add(infoLabel);
            
            // 控制面板
            JPanel controlPanel = new JPanel();
            JButton addMonsterBtn = new JButton("添加新怪物");
            JButton startSimulationBtn = new JButton("开始模拟移动");
            controlPanel.add(addMonsterBtn);
            controlPanel.add(startSimulationBtn);
            
            // 按钮事件
            addMonsterBtn.addActionListener(e -> {
                // 随机生成怪物
                Random rand = new Random();
                int x = rand.nextInt(server.getMapWidth());
                int y = rand.nextInt(server.getMapHeight());
                boolean aggressive = rand.nextBoolean();
                boolean boss = rand.nextInt(10) == 0; // 10%几率生成BOSS
                int level = rand.nextInt(50) + 1;
               
                String name = (boss ? "BOSS_" : "") +
                           (aggressive ? "主动怪" : "被动怪") + "_Lv" + level;
               
                Monster monster = new Monster(x, y, name, level, aggressive, boss);
                server.addMonster(monster);
               
                System.out.println("添加新怪物: " + name + " 位置=(" + x + "," + y + ")");
            });
            
            startSimulationBtn.addActionListener(e -> {
                startMonsterMovementSimulation();
            });
            
            // 添加到框架
            frame.add(infoPanel, BorderLayout.NORTH);
            frame.add(minimap, BorderLayout.CENTER);
            frame.add(controlPanel, BorderLayout.SOUTH);
            
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
            
            // 初始添加一些怪物
            initializeMonsters();
      });
    }
   
    private static void initializeMonsters() {
      // 添加初始怪物
      server.addMonster(new Monster(2300, 2400, "狼", 5, true, false));
      server.addMonster(new Monster(2600, 2500, "史莱姆", 3, false, false));
      server.addMonster(new Monster(2400, 2600, "哥布林", 8, true, false));
      server.addMonster(new Monster(2700, 2300, "龙", 45, true, true)); // BOSS
      server.addMonster(new Monster(2200, 2700, "鹿", 2, false, false));
      
      System.out.println("初始化完成,添加了5个怪物(包含1个BOSS)");
    }
   
    private static void startMonsterMovementSimulation() {
      Thread simulationThread = new Thread(() -> {
            try {
                System.out.println("开始模拟怪物移动...");
                for (int i = 0; i < 100; i++) { // 模拟100次移动
                  server.updateMonsterPositions();
                  Thread.sleep(1000); // 每秒移动一次
                  
                  // 更新UI
                  SwingUtilities.invokeLater(() -> {
                        minimap.repaint();
                  });
                }
                System.out.println("模拟结束");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
      });
      simulationThread.setDaemon(true);
      simulationThread.start();
    }
}
頁: [1]
查看完整版本: 請問有大大知道這個介面是甚麼檔案在管的嗎?