# 介绍

零散记录。


# 魔兽地图开发笔记

骑老登的魔兽地图开发笔记。

* [内置Lua / 内置JAPI](https://war3rpg.top)

## Button - Sync Click

```lua
button = {
    type = 'button',
    sync_key = 'UNIQUE-SYMBOL',
    on_sync_button_clicked = function(self, player)
        ... stuffs here
    end,
}
```


# 介绍

没什么好介绍的，一点微小的经验。


# 数据格式

{% embed url="<https://github.com/ChiefOfGxBxL/WC3MapTranslator#file-support>" %}

魔兽争霸3地图解包后的各项数据文件格式。

<figure><img src="https://3306617230-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LNJ-AD_h935Wy7ZBS5v%2Fuploads%2Ff7BbvDxKt6FSSzi5vote%2Fmisc_04.png?alt=media&amp;token=81c1fa28-3ce9-4b6d-9000-d56d196dd55c" alt=""><figcaption></figcaption></figure>


# 英雄经验值公式

（AI生成，未经检验）

魔兽争霸3中，英雄在L级时升级所需的经验值计算公式为：

```scss
f(x) = A + B + (L-1) × C + D × (英雄(L-1)级时升级所需经验值)
```

其中：

* A、B为固定因素
* C为等级因素
* D为上一个值因素
* L为当前英雄等级

这个公式在游戏的 **高级-游戏平衡性常数** 中可以找到相关参数设置，勾选 **自定义数据** 后可以查看和修改这些参数。

## 参考

* [魔兽地图 英雄EXP需求 公式](https://zhidao.baidu.com/question/207460601.html?qbpn=2_2\&tx=\&word=%E9%AD%94%E5%85%BD%E5%9C%B0%E5%9B%BE%20%E8%8B%B1%E9%9B%84EXP%E9%9C%80%E6%B1%82%20%E5%85%AC%E5%BC%8F\&fr=)


# 常规制图

使用常规触发就能够完成的事情，以及一些要点和提示。


# 修改特效角度

通常情况下使用 `绕Z轴旋转` 即可实现常规的设置特效面向角度效果。

![](https://3306617230-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LNJ-AD_h935Wy7ZBS5v%2Fuploads%2Fgit-blob-192f816cba30423bc252f1cb07ab77198df6ee34%2Fmisc_05.png?alt=media)


# 禁止单位攻击物品

通过物编修改单位允许攻击的目标。

![](https://3306617230-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LNJ-AD_h935Wy7ZBS5v%2Fuploads%2Fgit-blob-12c423074ca118729572b7c7232f6329d6b09d81%2Fmisc_06.png?alt=media)


# 多重射击参数设置

设置多重射击的攻击目标数量。

![](https://3306617230-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LNJ-AD_h935Wy7ZBS5v%2Fuploads%2Fgit-blob-7fd436f6b0aac6b173fa6ed2eb7e0f39b8e7d766%2Fmisc_07.png?alt=media)


# 修改建筑建造速度

`单位 - 设置建筑建造进度条`

可以直接通过这条动作设置建筑的建造进度。

需要更精细的控制时，可以自行模拟速度。


# 内置Lua制图

使用内置Lua / 内置JAPI框架进行开发的经验。


# 黑窗控制台

使用内置Lua打开黑窗控制台以后，如果使用左键选中了控制台内容，此时游戏会被阻断。

因此如果在运行游戏的同时会显示控制台的情况下，如果你发现游戏卡住了，先尝试用鼠标在控制台按下右键，以清空当前的选择。

## 开启黑窗控制台

```lua
local console = require 'jass.console'
console.enable = true
```

## 关闭黑窗控制台

```lua
local console = require 'jass.console'
console.enable = false
```


# 获取对象物编数据

```lua
local slk = require 'jass.slk'

--获取单位名称
slk.unit['hfoo'].Name

--获取物品图标
slk.item['I000'].Art
```


# 技能冷却渲染机制

原生冷却时间遮罩的100%对应的是技能的基础冷却时间。\
当基础冷却时间为10秒时，如果设置当前剩余冷却时间为20，则前10秒会始终保持100%进度，后10秒才会开始可见。

当CD遮罩动画正在“过程中”时，修改基础冷却时间不会影响本次播放。\
当播放结束后（或开始前），修改基础冷却时间为20秒，可以修正前述的前10秒进度问题。

（注：修改基础冷却时间之类涉及物编的数据时，需要重新触发一次单位GUI刷新才可以看到效果。）

修改指定单位持有技能的基础冷却时间：

```lua
local gid = s2gid(gid_s)
local lv = 1 --影响的物编技能等级
local time = 5 --单位：秒
local data_id = 105 --物编：魔法施放时间间隔 - Cool1 Cool2 Cool3 .. Cool{lv}
YDWESetUnitAbilityDataReal(j_unit, gid, lv, data_id, time)
```

修改指定单位持有技能的当前剩余冷却时间：

```lua
local gid = s2gid(gid_s)
japi.EXSetAbilityState(japi.EXGetUnitAbility(j_unit, gid), 1, time_remains)
```


# 异步计时器

## game.loop

game.wait | game.loop | game.timer

以上三者会在本地状态下运行，应当仅用于实现异步渲染逻辑。

Sample 1

```lua
game.loop(10, function()
    local plr = get_local_player()
    if plr:is_self() then
        -- 本地渲染动作
    end
end)
```


# UI异步行为转同步

使用内置Lua进行制图时常常需要处理各类本地UI操作，这些操作发生在某个玩家的本机上。

此时为了让这个操作能够被所有玩家都接收到，需要进行消息转发操作。

### 准备工作

`package\plugin\script\ui\server\trigger.lua`

检查上述文件中的同步逻辑是否为**使用网易同步**，如果没有，则修改为使用网易同步，否则会无效。

### 写法

```lua
-- 开头代码：必要的工具引入
local client = require 'ui.client.util'

-- 中间代码：各类UI组件和逻辑
-- （这里略）

-- 结尾代码：注册点击回调事件
local server = require 'ui.server.util'

local event = {
    ['action_select_card_button'] = function(被点击的卡片ID)
        local 玩家句柄 = server.player.handle
        local 玩家ID = GetConvertedPlayerId(玩家句柄)

        print('同步-点击按钮', 被点击的卡片ID, '玩家ID', 玩家ID)
    end,
}

server.register_event('gui_74_3in1', event)

-- 调用处代码：
client.send_message({
    type = 'gui_74_3in1',
    func_name = 'action_select_card_button',
    params = { 被点击的卡片ID }
}, false)
```


# UI界面显示与隐藏，菜单按钮的实现

假设存在 `panel` 对象，我们可以通过以下方式来控制它的显示与隐藏。

```lua
local panel = class.panel:builder
{
    type = 'panel',
    x = 0,
    y = 0,
    w = 1920,
    h = 1080,
}

panel:hide() --通过脚本创建的UI，默认是显示的，因此需要在此调用 :hide() 使其隐藏。
```

现在假设存在两个按钮用于控制其显示和隐藏。

```lua
local btnA = class.button:builder
{
    type = 'button',
    x = 100,
    y = 100,
    w = 60,
    h = 20,
    
    m_text = {
        type = 'text',
        x = 0,
        y = 0,
        w = 60,
        h = 20,
        text = '显示'
    },
    
    on_button_clicked = function(self)
        on_click_button(1)
    end,
}
local btnB = class.button:builder
{
    type = 'button',
    x = 100,
    y = 100,
    w = 60,
    h = 20,
    
    m_text = {
        type = 'text',
        x = 0,
        y = 0,
        w = 60,
        h = 20,
        text = '隐藏'
    },
    
    on_button_clicked = function(self)
        on_click_button(2)
    end,
}
```

其中 `on_button_clicked` 是点击按钮以后，在游戏内产生的回调方法。

可以在这里设置按钮的功能，此处我们调用 `on_click_button` 函数。

```lua
local function on_click_button(menu_id)
    if menu_id == 1 then
        panel:show() --显示
    elseif menu_id == 2 then
        panel:hide() --隐藏
    end
end
```

这样，就可以通过点击按钮来控制 `panel` 对象的显示与隐藏了。

倘若我们需要做菜单按钮，当玩家点击的时候会切换页面，而非显示/隐藏页面，可以做如下修改。

```lua
--修改按钮文本
btnA.m_text:set_text('页面1')
btnB.m_text:set_text('页面2')

--假设存在 page1 / page2 ，对应两个页面，类型为 panel
local page1 = ...
local page2 = ...

local function on_click_button(menu_id)
    if menu_id == 1 then
        page1:show()
        page2:hide()
    elseif menu_id == 2 then
        page1:hide()
        page2:show()
    end
end
```

通过上述修改，当玩家点击按钮的时候会切换页面，而非显示/隐藏页面。

如果我们需要更智能地去支持多个菜单按钮的切换，可以用如下写法：

```lua
--假设存在多个按钮
local btnA, btnB, btnC, btnD, ...
--与之对应，存在多个页面
local page1, page2, page3, page4, ...

--首先注册所有列表
local list_btn = { btnA, btnB, btnC, btnD, ... }
--所有页面
local list_page = { page1, page2, page3, page4, ... }

local function on_click_button(menu_id)
    local page = list_page[menu_id]
    if page then
        --显示目标页面
        page:show()
        
        --隐藏除了目标页面以外的其他页面
        for i = 1, #list_page do
            local other_page = list_page[i]
            if other_page ~= page then
                other_page:hide()
            end
        end
    end
end
```

可以批量的为所有按钮注册ID，并注册点击事件。

```lua
for i = 1, #list_btn do
    local btn = list_btn[i]
    btn.x_id = i
    btn.m_text:set_text('页面' .. i)
    btn.on_button_clicked = function(self)
        on_click_button(self.x_id)
    end
end
```

倘若我们需要在切换页面时刷新，可以为按钮被点击的事件添加刷新页面的逻辑。

```lua
local function refresh_page(menu_id)
    local page = list_page[menu_id]
    local btn = list_btn[menu_id]
    
    if page and btn then
        ... 在此处编写刷新逻辑 ...
    end
end

local function on_click_button(menu_id)
    local page = list_page[menu_id]
    if page then
        --显示目标页面
        page:show()
        
        --隐藏除了目标页面以外的其他页面
        for i = 1, #list_page do
            local other_page = list_page[i]
            if other_page ~= page then
                other_page:hide()
            end
        end
        
        --刷新目标页面
        refresh_page(menu_id)
    end
end
```


# 狼魂Lua框架

基于狼魂Lua框架的经验知识。


# 控制台UI调整

![](https://3306617230-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LNJ-AD_h935Wy7ZBS5v%2Fuploads%2Fgit-blob-08bd55f9c86bff7c2cce1f2084947fe09b3be119%2Fmisc_01.png?alt=media)

`scripts/系统/基础界面/属性.lua`

在上述文件调整属性相关图标的显示和隐藏，比如力量敏捷智力。


# 字符串转魔兽ID

![](https://3306617230-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LNJ-AD_h935Wy7ZBS5v%2Fuploads%2Fgit-blob-08bd55f9c86bff7c2cce1f2084947fe09b3be119%2Fmisc_01.png?alt=media)

通过 `string.unpack(">I4", "u000")` 可以将字符串 `"u000"` 转换为魔兽整数ID，对应到Jass中的写法是 `'u000'` 。

要注意**单双引号的差别**，以及单双引号在Lua和Jass中**不同的含义**。

也可以使用 `FourCC` 函数来等效替代。

## 原理

魔兽里， `'u000'` 这样的写法实际上对应的是一串整数。\
在Lua中，想要让输入的字符串 `"u000"` 与Jass中的写法 `'u000'` 相同，需要使用 `string.unpack` 函数。


# 创建单位

1. 在 `package/table/unit.ini` 中确保存在相关单位物编。
2. 在 `table/单位/囚犯/unit[单位].ini` 中确保存在相关额外配置数据。
3. 使用狼魂框架提供的创建单位函数来正常创建单位。


# 编辑自定义属性

![](https://3306617230-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LNJ-AD_h935Wy7ZBS5v%2Fuploads%2Fgit-blob-4c1ffeb3e598dc21a5e1fa7b12bcdb9e0e4dcbc4%2Fmisc_03.png?alt=media)

在 `scripts/core/types/attribute.lua` 中找到相关代码自行学习。


# 平台BUG

## 会导致崩溃的BUG

### 【确认中】服务器存储整数导致崩溃

![](https://3306617230-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LNJ-AD_h935Wy7ZBS5v%2Fuploads%2Fgit-blob-c220981e0b6bc9052dbea90123c447a8a7ab88e0%2Fdzapi_bug_1.png?alt=media)


# 平台工具

[地图逻辑脚本服务 (MLS)](https://gitee.com/dzpt/mls): 网易云存档脚本服务。


# 效率工具

改进工作管线，从各方面提升工作效率的工具。


# 模型批量改名

nodejs

```js
const fs = require('fs');
const path = require('path');

// 配置参数
const source_dir = '.';
const source_files_pattern = /\.(mdx|mdl)$/i; // 匹配 .mdx 或 .mdl 文件
const target_name = 'NewModel_%d';

// 读取目录中的文件
fs.readdir(source_dir, (err, files) => {
  if (err) {
    console.error('读取目录出错:', err);
    return;
  }

  // 过滤出符合条件的文件
  const matchedFiles = files.filter(file => source_files_pattern.test(file));

  console.log(`找到 ${matchedFiles.length} 个匹配的文件`);

  // 重命名文件
  matchedFiles.forEach((file, index) => {
    const fileExt = path.extname(file); // 获取文件扩展名
    const newName = target_name.replace('%d', index + 1) + fileExt;
    const oldPath = path.join(source_dir, file);
    const newPath = path.join(source_dir, newName);

    fs.rename(oldPath, newPath, err => {
      if (err) {
        console.error(`重命名 ${file} 失败:`, err);
      } else {
        console.log(`${file} -> ${newName}`);
      }
    });
  });
});
```


# 装饰物物编生成

## nodejs 生成 doodad.ini 数据

自行填写代码中 `list` 的参数来生成数据。

```javascript
const fs = require('fs');

const tpl = `[AAA]
_parent = "ZOrt"
-- 名字
Name = "BBB"
-- 迷雾中显示动画
animInFog = 1
-- 类别
category = "S"
-- 默认比例
defScale = 1.0
-- 模型文件
file = "BBB"
-- 样式总数
numVar = 1
-- 路径纹理
pathTex = ""
-- 地形设置
tilesets = "*"
`;

let list = [
    { gid: "D000", name: "NewModel00.mdl" },
    { gid: "D001", name: "NewModel01.mdl" },
    { gid: "D002", name: "NewModel02.mdl" },
    { gid: "D003", name: "NewModel03.mdl" },
    { gid: "D004", name: "NewModel04.mdl" },
    { gid: "D005", name: "NewModel05.mdl" },
    { gid: "D006", name: "NewModel06.mdl" },
    { gid: "D007", name: "NewModel07.mdl" },
    { gid: "D008", name: "NewModel08.mdl" },
    { gid: "D009", name: "NewModel09.mdl" },
    { gid: "D00A", name: "NewModel10.mdl" },
    { gid: "D00B", name: "NewModel11.mdl" },
    { gid: "D00C", name: "NewModel12.mdl" },
    { gid: "D00D", name: "NewModel13.mdl" },
    { gid: "D00E", name: "NewModel14.mdl" },
    { gid: "D00F", name: "NewModel15.mdl" },
];

// 创建一个空字符串来存储所有内容
let allContent = '';

// 为每个列表项生成内容并添加到 allContent
list.forEach(item => {
    // 替换模板中的 AAA 和 BBB
    let content = tpl.replace(/\[AAA\]/g, `[${item.gid}]`)
                     .replace(/BBB/g, item.name);

    // 添加到总内容
    allContent += content + '\n';
});

// 写入单个文件
fs.writeFileSync('combined_output.txt', allContent);
console.log('已生成合并文件: combined_output.txt');
```

## 手动填写

填写后把 `|` 移除并换行。

```plaintext
[D000] | _parent = "ZPsh" | Name = "导入模型-1" | file = "自定义模型路径-1.mdl" | maxScale = 5. | minScale = 0.1 | numVar = 1
[D001] | _parent = "ZPsh" | Name = "导入模型-2" | file = "自定义模型路径-2.mdl" | maxScale = 5. | minScale = 0.1 | numVar = 1
[D002] | _parent = "ZPsh" | Name = "导入模型-3" | file = "自定义模型路径-3.mdl" | maxScale = 5. | minScale = 0.1 | numVar = 1
[D003] | _parent = "ZPsh" | Name = "导入模型-4" | file = "自定义模型路径-4.mdl" | maxScale = 5. | minScale = 0.1 | numVar = 1
[D004] | _parent = "ZPsh" | Name = "导入模型-5" | file = "自定义模型路径-5.mdl" | maxScale = 5. | minScale = 0.1 | numVar = 1
[D005] | _parent = "ZPsh" | Name = "导入模型-6" | file = "自定义模型路径-6.mdl" | maxScale = 5. | minScale = 0.1 | numVar = 1
[D006] | _parent = "ZPsh" | Name = "导入模型-7" | file = "自定义模型路径-7.mdl" | maxScale = 5. | minScale = 0.1 | numVar = 1
[D007] | _parent = "ZPsh" | Name = "导入模型-8" | file = "自定义模型路径-8.mdl" | maxScale = 5. | minScale = 0.1 | numVar = 1
[D008] | _parent = "ZPsh" | Name = "导入模型-9" | file = "自定义模型路径-9.mdl" | maxScale = 5. | minScale = 0.1 | numVar = 1
[D009] | _parent = "ZPsh" | Name = "导入模型-10" | file = "自定义模型路径-10.mdl" | maxScale = 5. | minScale = 0.1 | numVar = 1
```


# 代码片段

## Code Pieces

pieces of code here.

## A

### Build Game Unit

```lua
local MainQuest = {}
MainQuest.Units = {}

local u_enemy = CreateUnit(uid, GetPlayerPoint(player, 'MainEnemyPoint'), 270, 'Enemy')

function GetPlayerPoint(player, label)
    local pid = player:GetID()
    if label == 'MainEnemyPoint' then
        return {{-512,0}, {512,0}, {0,512}, {0,-512}}[pid]
    end
end

function CreateUnit(uid, point, face, type)
    local unit = ... stuffs here ...
    Matrix.SetupUnitPower(u, type)
    return unit
end

function Matrix.CalcUnitPower(u, type)
    local info = {}
    if type == 'Enemy' then
        info.ATK = G_DIFFICULT * G_CHAPTER * 300
        info.DEF = G_DIFFICULT * G_CHAPTER * 50
        info.HP = G_DIFFICULT * 1000 + G_CHAPTER * 200
    end
    return info
end

function Matrix.AssignUnitPower(u, info)
    if info.ATK then u:SetAttr('ATK', info.ATK) end
    if info.DEF then u:SetAttr('DEF', info.DEF) end
    if info.HP then u:SetAttr('HP', info.HP) end
    if info.MP then u:SetAttr('MP', info.MP) end
    if info.SPD then u:SetAttr('SPD', info.SPD) end
    if info.RNG then u:SetAttr('RNG', info.RNG) end
    if info.SIGHT then u:SetAttr('SIGHT', info.SIGHT) end
end

function Matrix.SetupUnitPower(u, type)
    local info = Matrix.CalcUnitPower(u, type)
    Matrix.AssignUnitPower(info)
end
```


