refactor-from-js
将JavaScript脚本用C#重写
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
将JavaScript脚本用C#重写
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
| name | refactor-from-js |
| description | 将JavaScript脚本用C#重写 |
本项目将MapleStory游戏脚本从JavaScript重写为C#。所有C#脚本类都是通过 ScriptService 注册的,使用 TypeUtils.ExtractMethodsToDictionary 通过方法名或 [ScriptName] 特性将脚本ID映射到C#方法。
TypeUtils.ExtractMethodsToDictionary 扫描指定类型的所有实例方法,构建 Dictionary<string, MethodInfo>:
[ScriptName("name1", "name2")] 特性,则特性中指定的每个名称都会作为键注册[ScriptName("08_xmas_out")] // 通过特性指定脚本名称
public async Task<bool> _08_xmas_out() { ... }
public async Task<bool> advice00() { ... } // 方法名 advice00 就是键
| 类型 | JS目录 | C#类 | 上下文对象 | JS入口函数 | C#返回值 |
|---|---|---|---|---|---|
| NPC | scripts/npc/ | NpcScript (partial) | cm | start() + action(mode,type,selection) | Task |
| Quest | scripts/quest/ | QuestScript (partial) | qm | start(mode,type,selection) / end(mode,type,selection) | Task |
| Portal | scripts/portal/ | PortalScript | pi | enter(pi) | Task<bool> |
| Item | scripts/item/ | ItemScript | im | start() + action(mode,type,selection) | Task |
| Reactor Act | scripts/reactor/ | ReactorActScript | rm | act() | Task |
| Reactor Hit | scripts/reactor/ | ReactorHitScript | rm | hit() | Task |
| Reactor Touch | scripts/reactor/ | ReactorTouchScript | rm | touch() | Task |
| Reactor Untouch | scripts/reactor/ | ReactorUntouchScript | rm | untouch() | Task |
| Map Enter | 无JS | MapEnterScript | - | - | Task |
| Map FirstEnter | 无JS | MapFirstEnterScript | - | - | Task |
// TODO 的需要重写实现,没有 TODO 的说明已处理,跳过#b、#k、#n、#e、#r、#t、#m、#i、#p、#f、#s、#v、#z 等)var 定义 status 变量和 action 回调的概念,直接使用 await 等待用户交互cm.xxx() / qm.xxx() / pi.xxx() / rm.xxx() / im.xxx() 调用在 C# 中直接对应 await Xxx() 或 Xxx() 实例方法,无需前缀JS: /src/Application.Resources/scripts/npc/*.js
C#: /src/Application.Plugin.Script/Npc/*.cs (partial class NpcScript)
NpcScript 是一个 partial class,分散在多个文件中,每个文件按功能领域拆分(职业、PQ、Boss等)。
查找方式:方法上的注释 // Npc: {npc_id} 标明了对应的NPC ID。
npc_id.js → C# 中拥有 `// Npc: {npc_id}` 注释的方法
多个NPC ID可以映射到同一个C#方法:
// Npc: 1002002, 2010005, 2040048
public async Task florina2() { ... }
| JS | C# |
|---|---|
cm.xxx() | await Xxx() 或 Xxx() (直接调用基类方法) |
cm.getPlayer() | getPlayer() |
cm.getJobId() | getJobId() |
cm.getMeso() | getMeso() |
cm.getLevel() | getLevel() |
cm.getMapId() | getMapId() |
cm.haveItem(id) | haveItem(id) |
cm.canHold(id) | canHold(id) |
cm.gainItem(id, n) | await gainItem(id, n) |
cm.gainMeso(n) | await gainMeso(n) |
cm.gainExp(n) | await gainExp(n) |
cm.warp(map, portal) | await warp(map, portal) |
cm.isQuestStarted(id) | isQuestStarted(id) |
cm.isQuestCompleted(id) | isQuestCompleted(id) |
cm.startQuest(id) | await startQuest(id) |
cm.completeQuest(id) | await completeQuest(id) |
cm.forceStartQuest() | await forceStartQuest(id) |
cm.forceCompleteQuest() | await forceCompleteQuest(id) |
cm.dispose() | 不需要(方法结束自动dispose) |
| JS (cm) | C# |
|---|---|
cm.sendOk(text) | await SayOK(text) |
cm.sendNext(text) | await SayNext(text) |
cm.sendYesNo(text) | await AskYesNo(text) → 返回 bool |
cm.sendAcceptDecline(text) | await SayAcceptDecline(text) → 返回 bool |
cm.sendSimple(text) | await AskMenu(text, options?) → 返回 int |
cm.sendStyle(text) | await AskAvatar(text) → 返回 int |
cm.sendGetNumber(text, def, min, max) | await AskNumber(text, def, min, max) → 返回 int |
cm.sendGetText(text) | await AskText(text) → 返回 string |
cm.sendNext(text) + cm.sendPrev(text) | await SaySpeech(texts) → 传入 string[] |
cm.sendOk(text) + cm.sendOk(text) | await SayOK(text) (连续调用) |
// npc/10200.js
var status = -1;
function start() {
cm.sendNext("弓箭手具有灵巧和力量的天赋...");
}
function action(mode, type, selection) {
status++;
if (mode != 1) {
if (mode == 0) {
cm.sendNext("如果你想体验成为一个弓箭手的感觉,再来找我吧。");
}
cm.dispose();
return;
}
if (status == 0) {
cm.sendYesNo("你想体验一下成为一个弓箭手是什么感觉吗?");
} else if (status == 1) {
cm.lockUI();
cm.warp(1020300, 0);
cm.dispose();
}
}
// Npc: 10200
public async Task job_10200()
{
await SayNext("弓箭手具有灵巧和力量的天赋...");
if (await AskYesNo("你想体验一下成为一个弓箭手是什么感觉吗?"))
{
await lockUI();
await warp(1020300, 0);
}
else
{
await SayNext("如果你想体验成为一个弓箭手的感觉,再来找我吧。");
}
}
JS 使用 status 变量+ action(mode,type,selection) 回调实现多步对话。C# 通过 await 直接按顺序书写:
// JS: 三步对话
var status = -1;
function start() { cm.sendNext("第一步"); }
function action(mode, type, selection) {
if (mode == 1) status++;
else { cm.dispose(); return; }
if (status == 0) cm.sendNext("第二步");
else if (status == 1) cm.sendOk("第三步");
else cm.dispose();
}
// C#: 直接顺序书写
public async Task example()
{
await SayNext("第一步");
await SayNext("第二步");
await SayOK("第三步");
}
NpcScript.cs 中用 n{npc_id} 作为方法名创建新方法,并添加 // Npc: {npc_id} 注释// Npc: id1, id2, id3,如果同时存在这多个NPC脚本,观察他们是否相同,如果相同,则取其中任意一份;如果不同,则不修改并注释说明;如果只有其中一个存在脚本,则正好使用这个NPC脚本重写// TODO + 简单注释(记录NPC ID和原始功能描述)JS: /src/Application.Resources/scripts/quest/*.js
C#: /src/Application.Plugin.Script/Quest/*.cs (partial class QuestScript)
QuestScript 也是 partial class,按任务区域拆分。
quest_id.js → C# `q{quest_id}s` (start) / `q{quest_id}e` (end) 方法
方法注释标记:// Quest: {quest_id}
JS 中任务脚本使用 qm 上下文,方法签名通常为 start(mode, type, selection)(可能同时包含开始和完成的逻辑,也可能没有独立的 end 函数)。
C# 分为 q{id}s(开始)/ q{id}e(完成)两个方法:
JS qm | C# |
|---|---|
qm.forceStartQuest() | await forceStartQuest() |
qm.forceCompleteQuest() | await forceCompleteQuest() |
qm.gainItem(id, n) | await gainItem(id, n) |
qm.gainExp(n) | await gainExp(n) |
qm.getQuestStatus(id) | getQuestStatus(id) |
qm.getQuestProgress(id) | getQuestProgress(id) |
对话方法与NPC脚本一致(继承自同一基类)。
// quest/20000.js
function start(mode, type, selection) {
if (mode == -1) { qm.dispose(); }
else {
if (mode > 0) status++;
else status--;
if (status == 0)
qm.sendNext("啊,你来了。。。");
else if (status == 3) {
qm.gainItem(1142065, 1);
qm.gainExp(20);
qm.forceStartQuest();
qm.forceCompleteQuest();
qm.dispose();
}
}
}
// Quest: 20000
public async Task q20000s()
{
await SayNext("啊,你来了。。。");
await SayNext("对抗想吞没整个枫叶世界的黑魔法师的邪恶本性...");
await SayOK("但我不担心这些。我相信你一定能战胜这一切...");
await gainItem(1142065, 1);
await gainExp(20);
await forceStartQuest();
await forceCompleteQuest();
}
end 方法,则不需要创建 q{id}e 方法{quest_id} 是任务ID数字,直接拼接(如 q20000s)JS: /src/Application.Resources/scripts/portal/*.js
C#: /src/Application.Plugin.Script/PortalScript.cs(单文件,非partial)
portal 脚本通过文件名(不含.js)作为键查找:
portal_name.js → C# 中同名方法 或 [ScriptName("portal_name")] 特性标记的方法
JS 入口函数 enter(pi),返回 true(允许通过)或 false(阻止通过)。
C# 方法签名:public async Task<bool> portalName(),返回 true/false。
JS pi | C# |
|---|---|
pi.warp(map, portal) | await warp(map, portal) |
pi.playPortalSound() | await playPortalSound() |
pi.blockPortal() | await blockPortal() |
pi.showInstruction(msg, w, h) | await showInstruction(msg, w, h) |
pi.showInfo(effect) | await showInfo(effect) |
pi.containsAreaInfo(id, info) | containsAreaInfo(id, info) |
pi.updateAreaInfo(id, info) | await updateAreaInfo(id, info) |
pi.getMapId() | getMapId() |
// portal/advice00.js
function enter(pi) {
pi.showInstruction("您可以使用箭头键移动。", 250, 5);
return true;
}
public async Task<bool> advice00()
{
await showInstruction("您可以使用箭头键移动。", 250, 5);
return true;
}
[ScriptName("08_xmas_out")] 特性映射[ScriptName] 特性指定实际portal名称JS: /src/Application.Resources/scripts/reactor/*.js
C#: 4个类,分别对应不同的触发阶段
| 触发阶段 | C#类 | JS函数 |
|---|---|---|
| Hit (被击打) | ReactorHitScript | hit() |
| Act (激活) | ReactorActScript | act() |
| Touch (玩家触碰) | ReactorTouchScript | touch() |
| Untouch (玩家离开) | ReactorUntouchScript | untouch() |
reactor_id.js → C# 方法,通过 `// Reactor: {id}` 注释匹配
多个reactor ID可以映射到同一个C#方法:
// Reactor: 1022002, 1032000, 1202000, 1202004
public async Task EpisodeQuest0()
{
await dropItems();
}
JS rm 上下文 → C# this(ReactorActionManager 基类):
JS rm | C# |
|---|---|
rm.dropItems() | await dropItems() |
rm.dropItems(bool f, int r, int m, int M) | await dropItems(f, r, m, M) |
rm.spawnMonster(id) | await spawnMonster(id) |
rm.spawnMonster(id, n) | await spawnMonster(id, n) |
rm.warp(map, portal) | await warp(map, portal) |
rm.getMap() | getMap() |
rm.getReactor() | getReactor() |
rm.getEventInstance() | getEventInstance() → 返回 AbstractEventInstanceManager? |
rm.GetEventInstanceTrust() | GetEventInstanceTrust() → 非null或抛异常 |
eim.getIntProperty(key) | eim.getIntProperty(key) |
eim.setIntProperty(key, value) | eim.setIntProperty(key, value) |
rm.mapMessage(c, msg) | await mapMessage(c, msg) |
rm.getMap().killAllMonsters(bool) | await getMap().killAllMonsters() (C#无参数) |
rm.getMap().toggleEnvironment(name) | await getMap().toggleEnvironment(name) |
Touch 和 Untouch 脚本必须配对,通过事件实例属性计数器跟踪玩家触碰数量:
火焰/机关开关模式:仅在第一个玩家触碰时触发(ON),最后一个玩家离开时恢复(OFF):
// JS: 火焰切换(完整模式)
var fid = "glpq_f0";
function touch() {
var eim = rm.getEventInstance();
if (eim.getIntProperty(fid) == 0) { // 没人碰 → 开火
action();
}
eim.setIntProperty(fid, eim.getIntProperty(fid) + 1); // 计数器+1
}
function untouch() {
var eim = rm.getEventInstance();
if (eim.getIntProperty(fid) == 1) { // 最后一人离开 → 关火
action();
}
eim.setIntProperty(fid, eim.getIntProperty(fid) - 1); // 计数器-1
}
function action() {
var flames = Array("a1", "a2", "b1", "b2", "c1", "c2");
for (var i = 0; i < flames.length; i++) {
rm.getMap().toggleEnvironment(flames[i]);
}
}
// Touch: 在 ReactorTouchScript 中
public async Task glpqflame0()
{
var eim = getEventInstance();
if (eim == null) return;
var fid = "glpq_f0";
if (eim.getIntProperty(fid) == 0)
{
string[] flames = ["a1", "a2", "b1", "b2", "c1", "c2"];
for (var i = 0; i < flames.Length; i++)
{
await getMap().toggleEnvironment(flames[i]);
}
}
eim.setIntProperty(fid, eim.getIntProperty(fid) + 1);
}
// Untouch: 在 ReactorUntouchScript 中
public async Task glpqflame0()
{
var eim = getEventInstance();
if (eim == null) return;
var fid = "glpq_f0";
if (eim.getIntProperty(fid) == 1)
{
string[] flames = ["a1", "a2", "b1", "b2", "c1", "c2"];
for (var i = 0; i < flames.Length; i++)
{
await getMap().toggleEnvironment(flames[i]);
}
}
eim.setIntProperty(fid, eim.getIntProperty(fid) - 1);
}
阈值模式:计数器达到指定值时触发:
var fid = "glpq_s";
function touch() {
var eim = rm.getEventInstance();
if (eim.getIntProperty(fid) == 5) {
action(); // 5人触碰 → 激活
}
eim.setIntProperty(fid, eim.getIntProperty(fid) + 1);
}
function action() {
rm.mapMessage(6, "All stirges have disappeared.");
rm.getMap().killAllMonsters(true);
eim.setIntProperty(fid, 777);
}
public async Task glpqstrge()
{
var eim = getEventInstance();
if (eim == null) return;
var fid = "glpq_s";
if (eim.getIntProperty(fid) == 5)
{
await mapMessage(6, "All stirges have disappeared.");
await getMap().killAllMonsters();
eim.setIntProperty(fid, 777);
}
eim.setIntProperty(fid, eim.getIntProperty(fid) + 1);
}
rm.getEventInstance() 获取的事件实例用于存储跨触碰的计数器状态,C# 必须对应调用 getEventInstance(),不能省略。getIntProperty / setIntProperty 计数器是核心行为,不能去掉。简单的 await toggleEnvironment() 会导致多人同时触碰时火焰来回闪烁。fid 属性名和计数器,C# 中需要在 ReactorTouchScript 和 ReactorUntouchScript 各自实现对应的加减逻辑。getEventInstance() 返回 nullable,必须判空。部分现有代码使用 GetEventInstanceTrust()(直接抛异常),但 touch/untouch 推荐使用 getEventInstance() + null check。// reactor/9202000.js
function act() {
rm.dropItems();
}
// Reactor: 9202000
public async Task boxItem0()
{
await dropItems();
}
// Reactor: id1, id2, id3touch() 和 untouch() 但没有 act() / hit():说明该 reactor 只响应触碰事件,ReactorActScript.cs 和 ReactorHitScript.cs 中对应的空方法应保留(防止 "不支持的脚本" 异常),但要将 // TODO 改为说明注释JS: /src/Application.Resources/scripts/item/*.js
C#: /src/Application.Plugin.Script/ItemScript.cs(单文件)
item_script_name.js → C# 中间名方法
Item脚本的JS使用 im 上下文,入口函数 start() + action(mode,type,selection),与NPC类似。但C#中 ItemScript 继承自 NPCConversationManager,使用同样的对话方法。
JS im | C# |
|---|---|
im.getMapId() | getMapId() |
im.getMap() | getMap() |
im.getPlayer() | getPlayer() |
im.isQuestStarted(id) | isQuestStarted(id) |
im.isQuestCompleted(id) | isQuestCompleted(id) |
im.startQuest(id) | await startQuest(id) |
im.completeQuest(id) | await completeQuest(id) |
im.removeAll(id) | await removeAll(id) |
im.showInfo(eff) | await showInfo(eff) |
im.dropMessage(c, msg) | await Pink(msg) / await LightBlue(msg) |
im.message(msg) | await playerMessage(5, msg) |
im.dispose() | 不需要 |
// item/killarmush.js
function start(){
if (im.getMapId() == 106020300) {
var portal = im.getMap().getPortal("obstacle");
if (portal != null && portal.getPosition().distance(im.getPlayer().getPosition()) < 240) {
if (!(im.isQuestStarted(100202) || im.isQuestCompleted(100202))) {
im.startQuest(100202);
}
im.removeAll(2430014);
im.showInfo("Effect/OnUserEff/normalEffect/mushroomcastle/chatBalloon2");
im.dropMessage(6,'好像有什么动静...嗯?是结界被消除了');
} else {
im.message('尽可能的接近魔法结界才能将其消除');
}
}
im.dispose();
}
public async Task killarmush()
{
if (getMapId() == 106020300)
{
var portal = getMap().getPortal("obstacle");
if (portal != null && portal.getPosition().distance(getPlayer().getPosition()) < 240)
{
if (!(isQuestStarted(100202) || isQuestCompleted(100202)))
{
await startQuest(100202);
}
await removeAll(2430014);
await showInfo("Effect/OnUserEff/normalEffect/mushroomcastle/chatBalloon2");
await Pink("好像有什么动静...嗯?是结界被消除了");
}
else
{
await playerMessage(5, "尽可能的接近魔法结界才能将其消除");
}
}
else
{
await playerMessage(5, "这里似乎没有需要消除的魔法结界");
}
}
C# 中有两种地图脚本,没有对应的JS文件:
当玩家进入地图时触发,对应WZ中的 OnUserEnter 属性。
映射方式:方法名以 go{mapId} 命名,或用 [ScriptName("name")] 特性。
// Map: 10000
public async Task go10000()
{
await unlockUI();
await mapEffect("maplemap/enter/10000");
}
当第一个玩家进入地图时触发,对应WZ中的 OnFirstUserEnter 属性。
// Map: 103000800, 103000801, 103000802, 103000803, 103000804
public async Task StageMsg_together()
{
// TODO
}
| JS | C# | 说明 |
|---|---|---|
cm.sendOk | await SayOK | 纯文本对话框 |
cm.sendNext | await SayNext | "下一步"按钮对话框 |
cm.sendYesNo | await AskYesNo | 是/否选择,返回 bool |
cm.sendAcceptDecline | await SayAcceptDecline | 接受/拒绝,返回 bool |
cm.sendSimple | await AskMenu | 列表选择,返回 int |
cm.sendStyle | await AskAvatar | 发型/外观选择,返回 int |
cm.sendGetNumber | await AskNumber | 数字输入,返回 int |
cm.sendGetText | await AskText | 文本输入,返回 string |
cm.sendNext + sendPrev | await SaySpeech | 多页对话 |
cm.sendNext + sendOk | await SayNext + await SayOK | 连续对话 |
| JS | C# | 说明 |
|---|---|---|
cm.gainItem(id, n) | await gainItem(id, n) | 获取/移除物品 |
cm.haveItem(id) | haveItem(id) | 检查是否有物品 |
cm.canHold(id) | canHold(id) | 检查背包空间 |
cm.gainMeso(n) | await gainMeso(n) | 增减金币 |
cm.getMeso() | getMeso() | 获取金币数 |
cm.removeAll(id) | await removeAll(id) | 移除所有指定物品 |
| JS | C# | 说明 |
|---|---|---|
cm.isQuestStarted(id) | isQuestStarted(id) | 任务是否进行中 |
cm.isQuestCompleted(id) | isQuestCompleted(id) | 任务是否已完成 |
cm.getQuestStatus(id) | getQuestStatus(id) | 获取任务状态(0/1/2) |
cm.forceStartQuest() | await forceStartQuest() | 强制启动任务 |
cm.forceCompleteQuest() | await forceCompleteQuest() | 强制完成任务 |
cm.startQuest(id) | await startQuest(id) | 启动任务(需判断) |
cm.completeQuest(id) | await completeQuest(id) | 完成任务(需判断) |
| JS | C# | 说明 |
|---|---|---|
cm.warp(map, portal) | await warp(map, portal) | 传送 |
cm.getMapId() | getMapId() | 当前地图ID |
cm.getMap() | getMap() | 当前地图对象 |
| JS | C# | 说明 |
|---|---|---|
cm.getPlayer() | getPlayer() | 玩家对象 |
cm.getLevel() | getLevel() | 等级 |
cm.getJobId() | getJobId() | 职业ID |
cm.getGender() | getGender() | 性别 |
| JS | C# | 说明 |
|---|---|---|
cm.lockUI() | await lockUI() | 锁定UI |
cm.unlockUI() | await unlockUI() | 解锁UI |
cm.mapMessage(c, msg) | await mapMessage(c, msg) | 地图广播消息 |
cm.showInfo(eff) | await showInfo(eff) | 播放特效 |
cm.changeMusic(bgm) | await changeMusic(bgm) | 更换背景音乐 |
/src/Application.Resources/scripts/npc/*.js 和 /src/Application.Resources/scripts/quest/*.js// TODO,则根据JS逻辑重写// TODO,跳过(已处理)/src/Application.Resources/scripts/portal/*.js/src/Application.Resources/scripts/reactor/*.js/src/Application.Resources/scripts/item/*.js| 脚本类型 | 方法命名规则 | 注释标记 |
|---|---|---|
| NPC | n{npc_id} | // Npc: {npc_id} |
| Quest (start) | q{quest_id}s | // Quest: {quest_id} |
| Quest (end) | q{quest_id}e | // Quest: {quest_id} |
| Portal | portal名称(或 [ScriptName]) | 无 |
| Reactor (act) | reactor ID | // Reactor: {id} |
| Reactor (hit) | reactor ID | // Reactor: {id} |
| Item | JS文件名(不含扩展名) | 无 |