Lua 스크립팅 요리책

Lua API에 익숙해지고 사용 방법을 배우는 데 도움이 되는 코드 조각들입니다. 이 예제들을 테스트하려면 ` (백틱) 키를 눌러 게임 내 Lua 콘솔에 코드를 붙여넣으세요.

아이템

인벤토리에서 장착 및 착용한 모든 아이템 목록 가져오기

local you = gapi.get_avatar()
local items = you:all_items(false)

for _, item in pairs(items) do
  local status = ""
  if you:is_wielding(item) then
    status = "들고 있음: "
  elseif you:is_wearing(item) then
    status = "착용 중: "
  end
  print(status .. item:tname(1, false, 0))
end
예제 출력
들고 있음: 스마트폰
착용 중: 브래지어
착용 중: 팬티
착용 중: 양말
착용 중: 청바지
착용 중: 긴팔 셔츠
착용 중: 운동화
착용 중: 메신저백
착용 중: 손목시계
포켓칼
성냥갑
깨끗한 물 (플라스틱 병)
깨끗한 물

몬스터

플레이어 근처에 개 소환하기

local avatar = gapi.get_avatar()
local coords = avatar:get_pos_ms()
local dog_mtype = MtypeId.new("mon_dog_bcollie")
local doggy = gapi.place_monster_around(dog_mtype, coords, 5)
if doggy == nil then
    gdebug.log_info("개를 스폰할 수 없습니다 :(")
else
    gdebug.log_info(string.format("개를 %s 위치에 스폰했습니다", doggy:get_pos_ms()))
end

전투

전투 기술 사용 시 세부 정보 출력하기

먼저 함수를 정의합니다.

on_creature_performed_technique = function(params)
  local char = params.char
  local technique = params.technique
  local target = params.target
  local damage_instance = params.damage_instance
  local move_cost = params.move_cost
  gdebug.log_info(
          string.format(
                  "%s가(이) %s을(를) %s에게 사용했습니다 (DI: %s , MC: %s)",
                  char:get_name(),
                  technique.name,
                  target:get_name(),
                  damage_instance:total_damage(),
                  move_cost
          )
  )
end

그런 다음 훅을 함수에 한 번만 연결합니다.

game.add_hook("on_creature_performed_technique", function(...) return on_creature_performed_technique(...) end)
예제 출력
Ramiro Waters가(이) zombie에게 Power Hit을(를) 사용했습니다 (DI: 27.96 , MC: 58)

아이템 내구도

아이템 손상도 확인 및 수정하기

local you = gapi.get_avatar()
local wielding = you:all_items(false)[1]
print(wielding:get_damage())
print(wielding:get_damage_level(4))
wielding:mod_damage(2000)
print(wielding:get_damage_level(4))

몬스터

몬스터 인벤토리에 아이템 추가하기

local target_monster = -- [[ 당신의 몬스터 참조 ]]
local scraps = gapi.create_item(ItypeId.new("scrap"), 3)
target_monster:as_monster():add_item(scraps)

몬스터 상호작용을 무작위로 막기

Lua 콘솔에 아래 코드를 붙여 넣으면 몬스터 상호작용이 50% 확률로 실패하게 할 수 있습니다:

game.add_hook("on_try_monster_interaction", function(params)
    local monster = params.monster

    gapi.add_msg(string.format("당신은 %s에게 말을 걸어보려 합니다", monster:get_name()))
    if math.random(2) == 1 then
        gapi.add_msg(MsgType.warning, string.format("당신은 %s(와)과 상호작용하기엔 너무 수줍습니다!", monster:get_name()))
        return false
    end
end)

false 를 반환하면 기본 펫, 메카, 우호적 몬스터 상호작용이 막히고, true 를 반환하면 그대로 진행됩니다.

NPC

NPC 생성 및 삭제

local player = gapi.get_avatar()
local map = gapi.get_map()
local player_pos = player:get_pos_ms()
local place_point = player_pos:xy() + Point.new(0, 2)
local new_npc = map:place_npc(place_point, "thug")

-- 나중에 NPC를 조용히 삭제할 수 있습니다
new_npc:erase()

NPC 조종 전환에 반응하기

플레이어가 동료 NPC 조종으로 전환한 뒤 모드 상태를 갱신해야 할 때 on_control_npc 훅을 사용합니다. 이 훅은 전환 후 실행되므로 현재 조종 중인 캐릭터는 gapi.get_avatar()로 읽습니다.

local mod = game.mod_runtime[game.current_mod]
game.add_hook("on_control_npc", function(...) return mod.on_control_npc(...) end)

mod.on_control_npc = function(params)
    local controlled = gapi.get_avatar()

    gapi.add_msg(MsgType.good, string.format("이제 %s을(를) 조종합니다.", controlled:get_name()))
end

차원

현재 차원 확인하기

local map = gapi.get_map()

print("game dimension:", gapi.get_current_dimension_id())
print("map dimension:", map:get_bound_dimension())
print("is far-away point out of bounds:", map:is_out_of_bounds(coords.tripoint_bub_ms(500, 500, 0)))

포켓 디멘션에 들어가고 다시 들어가기

새 포켓 디멘션을 만들 때는 world_type 과 두 경계를 모두 넘겨야 합니다. 선택 사항인 overmap_terrain 은 bounds_min_omt 기준 z/y/x 테이블입니다. 그 디멘션이 현재 세션에 로드된 상태라면 dimension_id 와 target_omt 만으로 다시 들어갈 수 있습니다.

home_dimension = "sky_island_home"
overworld_pos = gapi.get_avatar():abs_pos()
home_omt = overworld_pos:to_omt()
local home_bounds_radius = coords.tripoint_rel_omt(2, 2, 0)

local entered = gapi.place_player_dimension_at({
  dimension_id = home_dimension,
  target_omt = home_omt,
  world_type = "pocket_dimension",
  bounds_min_omt = home_omt - home_bounds_radius,
  bounds_max_omt = home_omt + home_bounds_radius,
  boundary_terrain = "t_pd_border",
  boundary_overmap_terrain = "pd_border",
  overmap_terrain = {
    {
      { "forest", "field", "forest" },
      { "field", "field", "field" },
      { "forest", "field", "forest" },
    },
  },
})

if entered then
  gapi.add_msg("Pocket home loaded.")
end

오버월드로 돌아가기

들어가기 전에 저장한 overworld_pos 를 사용하면 정확한 칸으로 돌아갑니다.

gapi.place_player_dimension_at({
  dimension_id = "",
  target_ms = overworld_pos,
})

돌아온 뒤에는 로드된 포켓 디멘션의 ID와 목적지만 지정해서 다시 들어갑니다.

local reentered = gapi.place_player_dimension_at({
  dimension_id = home_dimension,
  target_omt = home_omt,
})

원정 디멘션 초기화 또는 삭제하기

통합 대상인 CBN-Sky-Island 원정 흐름은 이슈 #9589를 해결하려면 원정 지형을 새로 생성해야 합니다. 원정에 기본 디멘션이 아닌 ID를 지정하고, 오버월드로 돌아와 모드 상태를 갱신한 다음 생성 데이터를 초기화합니다.

local expedition_dimension = "sky_island_expedition"
local storage = game.mod_storage[game.current_mod]
local returned = gapi.place_player_dimension_at({
  dimension_id = "",
  target_ms = overworld_pos,
})

if returned then
  storage.is_away_from_home = false
  gapi.reset_dimension(expedition_dimension)
end

기본 오버월드는 제거할 수 없으므로 두 정리 함수 모두 ""를 거부합니다. reset_dimension은 재진입에 필요한 디멘션 메타데이터를 유지하지만, delete_dimension을 사용하면 다음 진입 시 생성 옵션을 모두 다시 넘겨야 합니다. 정리하기 전에 전체 저장을 수행하므로, 영구 Lua 상태를 먼저 갱신해야 합니다.

날씨 훅

날씨 변화에 반응하기

먼저 preload.lua에서 훅을 설정합니다:

local mod = game.mod_runtime[game.current_mod]
game.add_hook("on_weather_changed", function(...) mod.weather_changed_alert(...) end)
game.add_hook("on_weather_updated", function(...) mod.weather_report(...) end)

그 다음 main.lua에서 핸들러를 정의합니다:

local mod = game.mod_runtime[game.current_mod]

-- 날씨가 변할 때 호출됨 (예: 맑음 -> 비)
mod.weather_changed_alert = function(params)
    local msg = string.format(
        "날씨가 %s에서 %s로 변경되었습니다!",
        params.old_weather_id,
        params.weather_id
    )
    gdebug.log_info(msg)
end

-- 5분마다 현재 날씨 데이터와 함께 호출됨
mod.weather_report = function(params)
    local msg = string.format(
        "현재 날씨: %s, 온도: %.1f°C, 바람: %d, 습도: %d%%",
        params.weather_id,
        params.temperature,
        params.windspeed,
        params.humidity
    )
    gdebug.log_info(msg)
end

원거리 전투

발사된 총과 던져진 아이템에 반응하기

먼저 preload.lua에서 훅을 설정합니다:

local mod = game.mod_runtime[game.current_mod]
game.add_hook("on_shoot", function(...) return mod.on_shoot_fun(...) end)
game.add_hook("on_throw", function(...) return mod.on_throw_fun(...) end)

그 다음 main.lua에서 핸들러를 정의합니다:

local mod = game.mod_runtime[game.current_mod]

mod.on_shoot_fun = function(params)
    ---@type Item
    local gun = params.gun
    ---@type Item
    local ammo_item = params.ammo
    local ammo = ItypeId.NULL_ID()
    if not ammo_item then
        ammo = gun:ammo_current()
    else
        ammo = ammo_item:get_type()
    end
    local shoot_noise = ammo:obj():slot_ammo().loudness
    gdebug.log_info(string.format("총소리: %d.", shoot_noise))
end

mod.on_throw_fun = function(params)
    ---@type Character
    local thrower = params.thrower
    ---@type Item
    local thrown = params.thrown
    if thrown:is_gun() then
        gdebug.log_info("어라! 총은 던지는 것이 아닙니다!")
    end
end

오버맵 쿼리

오버맵에서 아이템 찾기 및 조작하기

-- 특정 위치의 오버맵에서 모든 아이템 찾기
local om_pos = OmPos.new(0, 0, 0)
local items = gapi.overmap_find_items_around(om_pos, 0)

-- 맵에서 아이템을 가져와서 맵이 언로드되어도 Lua에서 유지하기
local map_pos = MapPos.new(100, 100, 0)
local item = gapi.get_map():find_item_at(map_pos)
if item then
    local detached = gapi.create_detached_item(item)
    -- 나중에 다시 위치에 부착할 수 있습니다
    local reattached = gapi.reattach_item(detached, map_pos)
end

-- 같은 맵 내에서 아이템 이동하기
local source_pos = MapPos.new(100, 100, 0)
local dest_pos = MapPos.new(110, 110, 0)
gapi.get_map():move_item_at(source_pos, dest_pos)

사망 훅

몬스터 사망 추적하기

-- preload.lua에서
local mod = game.mod_runtime[game.current_mod]
game.add_hook("on_mon_death", function(...) return mod.on_mon_death(...) end)
-- main.lua에서
local mod = game.mod_runtime[game.current_mod]

mod.on_mon_death = function(params)
    ---@type Creature
    local monster = params.creature
    ---@type Character|nil
    local killer = params.killer

    local killer_name = killer and killer:get_name() or "알 수 없음"
    gdebug.log_info(string.format("%s가(이) %s에게 죽었습니다", monster:get_name(), killer_name))
end

캐릭터 사망 추적하기

-- preload.lua에서
local mod = game.mod_runtime[game.current_mod]
game.add_hook("on_char_death", function(...) return mod.on_char_death(...) end)
-- main.lua에서
local mod = game.mod_runtime[game.current_mod]

mod.on_char_death = function(params)
    ---@type Character
    local char = params.char
    ---@type Character|nil
    local killer = params.killer

    if char == gapi.get_avatar() then
        gdebug.log_info("당신이 죽었습니다!")
    end
end

캐릭터 전투 스탯

공격 및 스태미나 비용 확인하기

local you = gapi.get_avatar()
local items = you:all_items(false)

for _, item in pairs(items) do
    print(
        item:tname(1, false, 0)
        .. " { 공격 비용: " .. item:attack_cost()
        .. ", 스태미나 비용: " .. item:stamina_cost()
        .. ", 근접 스태미나 비용: " .. you:get_melee_stamina_cost(item)
        .. " }"
    )
end

-- 특수 능력 확인
print("Uncanny dodge: " .. (you:uncanny_dodge() and "네" or "아니오"))

캐릭터 마법

새 주문을 배우고 잊기

주문 배우기:

local u = gapi.get_avatar()
local km = u:get_magic()
local ex_sp = SpellTypeId.new("example_template")
km:learn_spell(ex_sp, u, true) -- learn forced
print( km:knows_spell(ex_sp) ) -- check

주문 잊기:

local u = gapi.get_avatar()
local km = u:get_magic()
local ex_sp = SpellTypeId.new("example_template")
km:forget_spell(ex_sp)         -- forget
print( km:knows_spell(ex_sp) ) -- check again

동적 아이템 액션

모든 아이템, 바이오닉, 돌연변이 콜백 테이블은 문자열 ID를 키로 사용하며 선택적 콜백 함수 테이블을 받습니다. 모든 콜백은 이름 있는 필드를 가진 단일 params 테이블을 받습니다.

game.iuse_functions

콜백 params 필드
use user, item, pos
can_use user, item, pos

use는 int(이동 단위 시간 비용)를 반환합니다. can_use는 bool을 반환합니다.

game.iuse_functions["my_custom_item"] = {
    use = function(params)
        local user = params.user
        local item = params.item
        gdebug.log_info("사용 중: " .. item:tname(1))
        return 0  -- 이동 단위로 시간 비용 반환
    end,

    can_use = function(params)
        -- 사용을 허용하려면 true, 방지하려면 false 반환
        return true
    end
}

아이템 수명 주기 콜백

몇 가지 추가 콜백 테이블을 사용하면 아이템 이벤트에 반응할 수 있습니다.

game.iwieldable_functions

콜백 params 필드
on_wield user, item, move_cost
on_unwield, can_wield, can_unwield user, item

game.iwearable_functions

콜백 params 필드
on_wear, on_takeoff, can_wear, can_takeoff user, item

game.iequippable_functions

콜백 params 필드
on_durability_change user, item, old_damage, new_damage
on_repair, on_break user, item

game.istate_functions

콜백 params 필드
on_tick, on_drop user, item, pos
on_pickup user, item

game.imelee_functions

콜백 params 필드
on_melee_attack user, target, item
on_hit user, target, item, damage_instance
on_block user, source, item, damage_blocked
on_miss user, item

game.iranged_functions

콜백 params 필드
on_fire user, item, target_pos, shots
on_reload, can_fire, can_reload user, item

can_* 콜백은 bool을 반환합니다 — 동작을 막으려면 false를 반환하세요.

game.iwieldable_functions["cursed_sword"] = {
    on_wield = function(params)
        gdebug.log_info(params.user:get_name() .. " draws " .. params.item:tname(1))
    end,
    can_unwield = function(params)
        -- Cursed sword can't be put down
        return false
    end
}

바이오닉 콜백

game.bionic_functions는 바이오닉 문자열 ID를 키로 사용합니다. 각 콜백은 단일 params 테이블을 받습니다.

콜백 params 필드 발생 시점
on_activate user, bionic 바이오닉 활성화 후
on_deactivate user, bionic 바이오닉 비활성화 후
on_installed user, bionic_id 바이오닉 설치 후
on_removed user, bionic_id 바이오닉 제거 후
game.bionic_functions["bio_laser"] = {
    on_activate = function(params)
        gdebug.log_info(params.user:get_name() .. " activated bio_laser")
    end,
    on_installed = function(params)
        gdebug.log_info("Installed: " .. tostring(params.bionic_id))
    end
}

돌연변이 콜백

game.mutation_functions는 특성 문자열 ID를 키로 사용합니다.

콜백 params 필드 발생 시점
on_activate user, trait_id 돌연변이 활성화 후
on_deactivate user, trait_id 돌연변이 비활성화 후
on_gain user, trait_id 돌연변이 획득 후
on_loss user, trait_id 돌연변이 상실 후
game.mutation_functions["TRAIT_QUICK"] = {
    on_gain = function(params)
        gdebug.log_info(params.user:get_name() .. " gained " .. tostring(params.trait_id))
    end,
    on_loss = function(params)
        gdebug.log_info(params.user:get_name() .. " lost " .. tostring(params.trait_id))
    end
}

더 많은 전투 훅

회피, 방어 및 기술 이벤트에 반응하기

-- preload.lua에서
local mod = game.mod_runtime[game.current_mod]
game.add_hook("on_creature_dodged", function(...) return mod.on_creature_dodged(...) end)
game.add_hook("on_creature_blocked", function(...) return mod.on_creature_blocked(...) end)
game.add_hook("on_creature_performed_technique", function(...) return mod.on_creature_performed_technique(...) end)
game.add_hook("on_creature_melee_attacked", function(...) return mod.on_creature_melee_attacked(...) end)
-- main.lua에서
local mod = game.mod_runtime[game.current_mod]

mod.on_creature_dodged = function(params)
    ---@type Character
    local char = params.char
    ---@type Creature
    local source = params.source
    local difficulty = params.difficulty
    gdebug.log_info(string.format("%s가(이) %s를(을) 회피했습니다 (DC: %d)", char:get_name(), source:get_name(), difficulty))
end

mod.on_creature_blocked = function(params)
    ---@type Character
    local char = params.char
    ---@type Creature
    local source = params.source
    local bodypart_id = params.bodypart_id
    local damage_blocked = params.damage_blocked
    gdebug.log_info(string.format(
        "%s가(이) %s를(을) %s에서 방어했습니다 (방어함: %.1f 데미지)",
        char:get_name(),
        source:get_name(),
        bodypart_id,
        damage_blocked
    ))
end

mod.on_creature_melee_attacked = function(params)
    ---@type Character
    local char = params.char
    ---@type Creature
    local target = params.target
    if params.success then
        gdebug.log_info(string.format("%s가(이) %s를(을) 맞췄습니다", char:get_name(), target:get_name()))
    else
        gdebug.log_info(string.format("%s가(이) %s를(을) 빗맞혔습니다", char:get_name(), target:get_name()))
    end
end

캐릭터 함정 인식

함정 확인 및 기억하기

먼저 함정을 설치합니다:

local u = gapi.get_avatar()
local m = gapi.get_map()
local pos = u:get_pos_ms()
local pos4x = pos + Tripoint.new(4, 0, 0)
-- tr_landmine_buried는 가시성이 20입니다. 찾기 매우 어렵습니다.
local mine = TrapId.new("tr_landmine_buried"):int_id()
m:set_trap_at(pos4x, mine)
print(tostring(u:knows_trap(pos4x)))

그 다음 캐릭터가 함정을 인식하도록 합니다:

local u = gapi.get_avatar()
local m = gapi.get_map()
local pos = u:get_pos_ms()
local pos4x = pos + Tripoint.new(4, 0, 0)
u:add_known_trap(pos4x, m:get_trap_at(pos4x))
print(tostring(u:knows_trap(pos4x)))

두 번째 스크립트를 실행한 후에는 함정을 밟지 않고도 함정이 설치된 위치를 볼 수 있습니다.

시간과 공간

태양과 달, 실내와 실외

local u_pos = gapi.get_avatar():get_pos_ms()
local map = gapi.get_map()
local now = gapi.current_turn()

-- Found the key name from MoonPhase entries
local moon = ""
for name, num in pairs(MoonPhase) do
   if num == now:moon_phase() then
      moon = name
   end
end

print( "Are you outside?: " .. tostring(map:is_outside(u_pos)) )
print( "Are you sheltered?: " .. tostring(map:is_sheltered(u_pos)) )
print( "Today moon phase is: " .. moon )
print( "Sunset time is: " .. now:sunset():to_string_time_of_day() )

아이템 타입 정보

ItypeId를 통한 아이템 타입 속성 쿼리

local item_type = ItypeId.new("9mm")

-- 아이템 타입 객체(ItypeRaw) 가져오기
local itype_raw = item_type:obj()

-- 아이템 타입별 데이터 접근 (예: 탄약)
if itype_raw:slot_ammo() then
    local ammo_data = itype_raw:slot_ammo()
    print("탄약 데미지: " .. ammo_data.damage)
    print("탄약 범위: " .. ammo_data.range)
end

-- 컨테이너의 경우
if itype_raw:slot_container() then
    local container_data = itype_raw:slot_container()
    print("수용량: " .. container_data.capacity)
end

-- 도구의 경우
if itype_raw:slot_tool() then
    local tool_data = itype_raw:slot_tool()
    print("도구 품질: " .. tool_data.quality)
end