홈 › 모딩 정보 › Lua 예제
04장

Lua 예제

실제 패키지에서 가져온 작동하는 스크립트 6개: 규칙 하나, 상한이 있는 배율, 진영별 규칙, 게임을 거부할 수 있는 units 훅, features 훅, 레이어로 쌓은 폴더를 쓰는 files 계획.

이 장은 docs/mutators/authoring.md 문서와 docs/mutators/oamut-standard.md 문서를 요약합니다.

mutator-packages에서 복사한 완전한 스크립트 6개입니다. 각 패널은 그 패키지의 oamut.yaml을 먼저, 이어서 mutator.lua를 보여 줍니다. 파일은 쓰인 그대로 보여 주며 번역하지 않았습니다.

옵션이 있는 규칙 하나

Drinken의 Commander Radar는 움직일 수 있는 커맨더의 필드 하나를 바꿉니다. where 블록이 commander = 1과 bmcode = 1을 요구하므로, 커맨더 플래그만 있는 관문은 건드리지 않습니다. radar 옵션은 advanced와 none 중에서 고릅니다. advanced는 radardistance를 3.1c 고급 레이더 탑의 범위인 2880으로, none은 0으로 설정합니다.

매니페스트 · oamut.yaml
oamut: 1
id: "commander-radar"
name: "Commander Radar"
version: "1"
author:
  name: "Drinken"
description: "Commanders gain the advanced radar tower's range, or lose radar entirely. Advanced sets the radar distance to 2880, the range of 3.1c's advanced radar tower, and none sets it to 0. This package replaces Advanced Radar Comm and No Radar Comm, both by Drinken. It also requires a unit that can move, which those originals did not, so Warp Gate 3.1's gates keep their own radar."
tags: ["commanders"]
works-with: ["*"]
requires:
  engine: ">= 0.8.5"
layers: ["units"]
options:
  radar:
    type: choice
    label: "Radar"
    values: ["advanced", "none"]
    default: "advanced"
legacy:
  - name: "Advanced Radar Comm"
    prefix: "{}"
    compatibility: ["*"]
    files: ["{}AdvRadarComm.tam", "{}AdvRadarComm.sm"]
  - name: "No Radar Comm"
    prefix: "{}"
    compatibility: ["*"]
    files: ["{}NoRadarComm.tam", "{}NoRadarComm.sm"]
진입 스크립트 · mutator.lua
function mutate(ctx)
  local distance = ctx.options.radar == "none" and 0 or 2880
  return {
    rules = {
      where { commander = 1, bmcode = 1 }:change {
        radardistance = set(distance),
      },
    },
  }
end

배율과 상한

Immerman의 ÜberComm은 배율 예제입니다. strength 옵션은 full 또는 lite이며, 스크립트는 이 값에 따라 배율을 고릅니다. full은 장갑과 속도 2배, 건설력 6배입니다. lite는 장갑과 속도 1.5배, 건설력 3배입니다. maxdamage는 scale(health, 1, 29900)이며, 새 값을 1과 29900 사이로 제한합니다. builddistance는 범위 없는 scale(2)입니다. 범위는 그것이 적힌 곱셈 하나에만 적용되며, 이후 뮤테이터의 곱셈에는 적용되지 않습니다.

매니페스트 · oamut.yaml
oamut: 1
id: "uber-comm"
name: "ÜberComm"
version: "1"
author:
  name: "Immerman"
description: "All commanders have twice the armor and speed, and six times the building power. This makes the commander a much more central unit, rather than having it become mostly obsolete after the early game. The increased usefulness means that playing \"Game Continues\" is no longer an invitation to bomb the commander. Strength lite is ÜberComm Lite: half again the armor and speed, and three times the building power, for the same reason. This package replaces ÜberComm and PowerComm, both by Immerman. The strength choice is the only difference between those two originals."
tags: ["commanders", "statistics"]
works-with: ["*"]
requires:
  engine: ">= 0.8.5"
layers: ["units"]
options:
  strength:
    type: choice
    label: "Strength"
    values: ["full", "lite"]
    default: "full"
legacy:
  - name: "ÜberComm"
    prefix: "{}"
    compatibility: ["*"]
    files: ["{}šberComm.tam", "{}šberComm.sm"]
  - name: "PowerComm"
    prefix: "{}"
    compatibility: ["*"]
    files: ["{}PowerComm.tam", "{}PowerComm.sm"]
진입 스크립트 · mutator.lua
function mutate(ctx)
  local lite = ctx.options.strength == "lite"
  local health = lite and 1.5 or 2
  local worker = lite and 3 or 6
  local move = lite and 1.5 or 2
  local accel = lite and 2.25 or 4
  return {
    rules = {
      where { commander = 1, bmcode = 1 }:change {
        maxdamage = scale(health, 1, 29900),
        builddistance = scale(2),
        workertime = scale(worker),
        maxvelocity = scale(move),
        turnrate = scale(move),
        brakerate = scale(accel),
        acceleration = scale(accel),
      },
    },
  }
end

진영별 규칙

MAC(macraig)의 Super Commander는 where 블록에 side = is("ARM") 또는 side = is("CORE")를 더해, 블록마다 한 진영의 커맨더에만 적용되게 합니다. side 옵션은 all, arm, core 중 하나이며, 스크립트는 지정된 진영마다 블록을 만듭니다. ARM은 maxvelocity에 7을 곱하되 최대 30까지, brakerate와 acceleration에는 49를 곱합니다. CORE는 maxvelocity에 5를 곱하되 최대 30까지, 그 두 필드에는 25를 곱합니다. 두 진영 모두 maxdamage에 1.5를 곱해 1~29900으로 제한하고, builddistance에 2, workertime에 3을 곱하며, turnrate에 5를 곱하되 최대 29900으로 제한합니다. side가 all이면 두 블록을 모두 쓰므로, 제3 진영의 커맨더는 건드리지 않습니다.

매니페스트 · oamut.yaml
oamut: 1
id: "super-commander"
name: "Super Commander"
version: "1"
author: { name: "MAC (macraig)" }
description: "Commanders have half again the armor, build three times as fast and reach twice as far. Arm commanders move seven times as fast, and Core commanders five times as fast.\n\nSuper Commander (Arm) and Super Commander (Core), by MAC (macraig), both promise 50% more armor, 500% more speed, 300% faster construction and 200% more build distance, so the commander stays useful after the early game. The two copies do not use the same speed numbers. Arm multiplies top speed by 7 and braking and acceleration by 49. Core multiplies top speed by 5 and braking and acceleration by 25. Both multiply damage by 1.5, from 1 to 29900, build distance by 2, worker time by 3 and turn rate by 5, up to 29900. This version keeps each side's numbers. The idea is Immerman's."
tags: ["commanders", "statistics"]
works-with: ["*"]
requires:
  engine: ">= 0.8.5"
layers: ["units"]
options:
  side:
    type: choice
    values: ["all", "arm", "core"]
    default: "all"
    label: "Side"
legacy:
  - name: "[MAC] Super Commander (Arm)"
    prefix: "{}"
    compatibility: ["*"]
    files: ["{}MAC Super Commander (Arm).tam", "{}MAC Super Commander (Arm).sm"]
  - name: "[MAC] Super Commander (Core)"
    prefix: "{}"
    compatibility: ["*"]
    files: ["{}MAC Super Commander (Core).tam", "{}MAC Super Commander (Core).sm"]
진입 스크립트 · mutator.lua
function mutate(ctx)
  local side = ctx.options.side
  local rules = {}

  local function add(side_name, speed, accel)
    rules[#rules + 1] = where {
      commander = 1,
      bmcode = 1,
      side = is(side_name),
    }:change {
      maxdamage = scale(1.5, 1, 29900),
      builddistance = scale(2),
      workertime = scale(3),
      turnrate = scale(5, 0, 29900),
      maxvelocity = scale(speed, 0, 30),
      brakerate = scale(accel),
      acceleration = scale(accel),
    }
  end

  -- Both texts promise the same percentages. The copies use different speed
  -- factors. Side all is both blocks, so another side's commander is left
  -- alone, as the two originals leave it.
  if side ~= "core" then
    add("ARM", 7, 49)
  end
  if side ~= "arm" then
    add("CORE", 5, 25)
  end
  return { rules = rules }
end

게임을 거부할 수 있는 units 훅

Krogoth Commander는 units 훅에서 weapons 컬렉션을 읽습니다. CORKROG_FIRE, CORKROG_ROCKET, CORKROG_HEAD 중 하나라도 없으면 플레이어에게 보일 이유를 넣어 ctx.incompatible을 호출합니다. weapons를 읽는 데는 별도 레이어가 필요하지 않습니다. 훅 옆의 규칙은 크로고스의 능력치입니다. 다음 장에서 원래 아카이브부터 이 패키지를 따라가며, 거기서 Immerman을 원제작자로 밝힙니다.

매니페스트 · oamut.yaml
oamut: 1
id: "kroggy-comm"
name: "Krogoth Commander"
version: "1"
author:
  name: "Immerman"
description: "Use an amphibious Krogoth as a commander. Turn it off to disable the radar, the sonar and the head laser in exchange for 7 energy. The D-gun aims the head laser. Arm and Core are both named because one script is shipped for each commander. This package replaces KroggyComm 1.1 by Immerman. The model and the scripts stay as files, and the Krogoth's stats are rules, including the cloak and bad-target removals that belong to that design. Heal time, the override adjust, the default mission and the localized names stay, because the 2001 unit files left them out. It needs Core Contingency's Krogoth weapons."
tags: ["commanders", "new-units"]
works-with: ["*"]
requires:
  engine: ">= 0.8.5"
conflicts-with: ["anni-comm", "special-tech-comm", "commander-blast", "tribe-commander"]
layers: ["files", "units"]
legacy:
  - name: "KroggyComm 1.1"
    prefix: "}{"
    compatibility: ["TACC", "UH"]
    files: ["}{KroggyComm11.tam", "}{KroggyComm11.ccx"]
진입 스크립트 · mutator.lua
-- HealTime, OvrAdjust, DefaultMissionType and the localized names stay.
-- The 2001 unit files predate those 3.1c fields, so their absence is not the Krogoth's design.
local function krogoth_changes(sound)
  local changes = {
    acceleration = set(0.09),
    badtargetcategory = remove(),
    brakerate = set(0.19),
    buildcostenergy = set(116664),
    buildcostmetal = set(29489),
    builddistance = set(120),
    buildtime = set(382193),
    cantbetransported = set(1),
    cloakcost = remove(),
    cloakcostmoving = remove(),
    corpse = set("corkrog_dead"),
    downloadable = set(1),
    energymake = set(32),
    energyuse = set(7),
    explodeas = set("NUCLEAR_MISSILE"),
    footprintx = set(3),
    footprintz = set(3),
    maxdamage = set(29918),
    maxslope = set(17),
    maxvelocity = set(1),
    maxwaterdepth = set(12),
    mincloakdistance = remove(),
    movementclass = set("TANKDH3"),
    noautofire = set(0),
    objectname = set("KROGCOM"),
    onoffable = set(1),
    selfdestructas = set("NUCLEAR_MISSILE"),
    sightdistance = set(255),
    turnrate = set(960),
    weapon1 = set("CORKROG_FIRE"),
    weapon2 = set("CORKROG_ROCKET"),
    weapon3 = set("CORKROG_HEAD"),
    wpri_badtargetcategory = remove(),
  }
  if sound ~= nil then
    changes.soundcategory = set(sound)
  end
  return changes
end

function mutate(ctx)
  return {
    files = { add = { "files" } },
    rules = {
      named("ARMCOM"):change(krogoth_changes("KROGOTH")),
      named("CORCOM"):change(krogoth_changes(nil)),
    },
    units = function(units, ctx)
      local weapons = ctx.weapons
      if weapons["CORKROG_FIRE"] == nil or weapons["CORKROG_ROCKET"] == nil or weapons["CORKROG_HEAD"] == nil then
        ctx.incompatible("KroggyComm needs Core Contingency's Krogoth weapons")
      end
    end,
  }
end

features 훅

MAC(macraig)의 No Scars는 maps-and-features 그룹에 속한 features 훅 예제입니다. 모든 지형지물에서 키 두 개를 지웁니다. 훅은 each로 컬렉션을 돌며 featuredead와 featurereclamate를 nil로 설정하므로, 지형지물, 유닛, 건물을 파괴하거나 회수해도 흔적이 남지 않고, 잔해도 더미를 남기지 않습니다. 매니페스트가 features 레이어를 선언하므로 쓰기가 허용됩니다.

매니페스트 · oamut.yaml
oamut: 1
id: "no-scars"
name: "No Scars"
version: "2"
author: { name: "MAC (macraig)" }
description: "Destroying or reclaiming a feature, a unit or a building leaves no scar and no smudge. Wrecks leave no heap either, because those two keys are what names the heap.\n\nNo Fugly Scars, by MAC (macraig), named 41 feature folders and cleared featuredead and featurereclamate on every feature in them. This version clears those two keys on every feature."
tags: ["gameplay"]
works-with: ["*"]
requires:
  engine: ">= 0.8.5"
layers: ["features"]
legacy:
  - name: "[MAC] No Fugly Scars"
    prefix: "{}"
    compatibility: ["*"]
    files: ["{}MAC No Fugly Scars.tam", "{}MAC No Fugly Scars.sm"]
진입 스크립트 · mutator.lua
function mutate(ctx)
  return {
    features = function(features, ctx)
      features:each(function(feature)
        feature.featuredead = nil
        feature.featurereclamate = nil
      end)
    end,
  }
end

레이어로 쌓은 파일 폴더

Immerman의 Warp Gate는 files 계획을 돌려줍니다. arrival 옵션은 at-gate, near, near-twin, chosen, chosen-twin 중 하나입니다. 스크립트는 files/common을 추가하고, 이어서 files/plain 또는 files/target을, arrival이 near-twin이면 files/near를, 마지막으로 files/ 뒤에 arrival 이름을 붙인 폴더를 추가합니다. 앞 폴더에도 있는 경로는 나중 폴더가 이깁니다. 같은 계획에서 커맨더의 건설 비용을 잠그고 sides와 menus를 다시 씁니다. 이 레이어들은 files와 함께 선언되어 있습니다.

매니페스트 · oamut.yaml
oamut: 1
id: "warpgate"
name: "Warp Gate"
version: "2"
author:
  name: "Immerman"
description: "Start from a gate and teleport in the commander you choose.\n\nArm starts with the Normal gate and Core with the Easy gate, which makes 40 energy and 3 metal and stores 3000 energy and 1000 metal. With a chosen arrival, Arm's gate is the Target gate: attack-order the ground and the commander is built there. Core's gate is the Random gate, which picks the spot. When the battle room says Game ends, losing the gate ends your game. Some computer profiles may not use the gate. If the match does not start on a gate, that profile is the likely cause. With a chosen arrival, a computer player on Arm's side never gets a commander: the Target gate builds only where a player attack-orders. On Core's side the Random gate works for a computer.\n\nCommander arrives: at-gate puts the commander on the gate, as Warp Gate 3.1 did, and the gate is itself a commander. near puts the commander near the gate, as 4.0 and 4.1 did, and the gate's script places the build point 8 to 30 units out. near-twin gives two commanders, as TwinComm did. chosen builds the commander where you choose, as Targetting did. chosen-twin gives two commanders, at a chosen spot or a random one, as TwinTarget did.\n\nThis package replaces * Warp Gate 3.1 *, * Warp Gate 4.0 *, * Warp Gate 4.0, Targetting *, * Warp Gate 4.0, TwinComm *, * Warp Gate 4.0, TwinTarget * and * Warp Gate 4.1 *, all by Immerman. It follows 4.1. One package holds the six versions, the sides keep their names and prefixes, TwinTarget's two commanders may both exist, and the battle-room screens, their art and the instruction plate are left out."
tags: ["gameplay", "new-units"]
works-with: ["*"]
requires:
  engine: ">= 0.8.5"
layers: ["files", "units", "sides", "menus"]
clashes: "replace"
options:
  arrival:
    type: choice
    values: ["at-gate", "near", "near-twin", "chosen", "chosen-twin"]
    default: "near"
    label: "Commander arrives"
legacy:
  - name: "* Warp Gate 3.1 *"
    prefix: "{}"
    compatibility: ["*"]
    files: ["{}Warpgate31.tam", "{}Warpgate31.sm", "{}Warpgate31(1).ccx"]
  - name: "* Warp Gate 4.0 *"
    prefix: "{}"
    compatibility: ["*"]
    files: ["{}Warpgate4_0.tam", "{}Warpgate4_0.sm", "{}Warpgate4_0(1).ccx"]
  - name: "* Warp Gate 4.0, Targetting *"
    prefix: "{}"
    compatibility: ["*"]
    files:
      - "{}Warpgate4_0-Targetting.tam"
      - "{}Warpgate4_0-Targetting.sm"
      - "{}Warpgate4_0-Targetting(1).ccx"
  - name: "* Warp Gate 4.0, TwinComm *"
    prefix: "{}"
    compatibility: ["*"]
    files:
      - "{}Warpgate4_0-TwinComm.tam"
      - "{}Warpgate4_0-TwinComm.sm"
      - "{}Warpgate4_0-TwinComm(1).ccx"
  - name: "* Warp Gate 4.0, TwinTarget *"
    prefix: "{}"
    compatibility: ["*"]
    files:
      - "{}Warpgate4_0-TwinTarget.tam"
      - "{}Warpgate4_0-TwinTarget.sm"
      - "{}Warpgate4_0-TwinTarget(1).ccx"
  - name: "* Warp Gate 4.1 *"
    prefix: "{}"
    compatibility: ["*"]
    files: ["{}Warpgate4_1.tam", "{}Warpgate4_1.sm", "{}Warpgate4_1(1).ccx"]
진입 스크립트 · mutator.lua
-- Arrival chooses one archive's files. 4.1's side rule runs for every arrival.
-- The originals' download files say MENU=2 BUTTON=0 for ARMCOM and BUTTON=1
-- for CORCOM. A player's page is MENU minus 1 and a slot is BUTTON plus 1,
-- so those places are page 1 slot 1 and page 1 slot 2. Slot 0 is not a place
-- this hook can name. The gates have no build page of their own, so the
-- slots stay where they are asked.
-- Gates have BMCode 0, so the commander selector does not treat them as
-- commanders. near-twin and chosen-twin clear the commander flag. TwinTarget
-- shipped no such line; TwinComm's line was a plain assignment.
local function offer(menus, gate)
  menus:add(gate, "ARMCOM", { page = 1, slot = 1 })
  menus:add(gate, "CORCOM", { page = 1, slot = 2 })
end

function mutate(ctx)
  local arrival = ctx.options.arrival
  local buildtime = 100
  if arrival == "at-gate" then
    buildtime = 1
  end
  local commanders = {
    buildcostenergy = lock(0),
    buildcostmetal = lock(0),
    buildtime = lock(buildtime),
    extractsmetal = lock(0.001),
  }
  if arrival == "near-twin" or arrival == "chosen-twin" then
    commanders.commander = set(0)
  end
  local gate = "plain"
  if arrival == "chosen" or arrival == "chosen-twin" then
    gate = "target"
  end
  -- Shared folders first. A later folder replaces a path an earlier one
  -- added. near and near-twin share unit files; near-twin then replaces
  -- the scripts those files sit beside.
  local add = { "files/common", "files/" .. gate }
  if arrival == "near-twin" then
    table.insert(add, "files/near")
  end
  table.insert(add, "files/" .. arrival)
  local plan = {
    files = { add = add },
    rules = {
      where { commander = 1, bmcode = 1 }:change(commanders),
      all():change { side = lock(0) },
    },
    sides = function(sides, ctx)
      if ctx.units.ARMCOM == nil or ctx.units.CORCOM == nil then
        ctx.incompatible("Warp Gate needs the Arm and Core commanders, ARMCOM and CORCOM")
        return
      end
      sides:each(function(side)
        local commander = side.commander
        if commander == nil then
          return
        end
        local folded = commander:lower()
        if folded == "armcom" then
          side.commander = "CORGATE"
        elseif folded == "corcom" then
          side.commander = "ARMGATE"
        end
      end)
    end,
    menus = function(menus, ctx)
      offer(menus, "ARMGATE")
      offer(menus, "CORGATE")
    end,
  }
  return plan
end