RPG 동료
RPG / 커스텀 맵에서 플레이어에게 AI 동료를 붙여 줍니다. 따라다니고, 사냥을 돕고, 체력이 낮으면 치유해 주고, 말동무가 되어 줍니다. 네 가지 모드가 있으며, 클래스 하나를 상속하고 속성 몇 개만 바꾸면 나만의 동료가 됩니다.
대전만이 아닙니다. RPG나 커스텀 맵에서 나만의 AI 동료를 둘 수 있습니다. 동료는 여러분을 따라다니며 사냥을 돕고, 체력이 낮아지면 치유해 주고, 한가할 때는 말을 건넵니다 — 대사는 로컬 LLM에 연결할 수도 있습니다.
어떻게 쓸지는 여러분이 정합니다. 이 기능은 세 층의 인터페이스로 나뉘어 있으며, 아래층부터 위층까지 어느 층이든 바로 쓸 수 있습니다.
| 층 | 무엇인가 | 적합한 경우 |
|---|---|---|
JASS 채널 g.jass | 맵 제작자가 쓸 수 있는 JASS 함수 1291개를 이름으로 바로 호출(유닛 생성, 동맹 설정, 아이템 지급, 이름 변경, 텍스트 표시, 영웅 부활……) | 게임플레이를 직접 만들고 싶을 때 |
| 편의 API | g.spawn, g.set_alliance, g.player_slots, g.show_text, g.map_data: 자주 쓰는 작업을 묶어 둠 | 나만의 보조 스크립트를 쓸 때 |
| 동료 프레임워크 | openwar3.companion.Companion + openwar3.talk.Talk: 상속하고 속성 몇 개만 바꾸면 따라다니고, 전투를 돕고, 치유하고, 대화하는 동료가 완성됨 | 동료가 필요할 때 |
싱글플레이 또는 LAN에서 직접 만든 게임에서만 사용하세요. 유닛 생성이나 동맹 설정 같은 작업은 이 컴퓨터가 일방적으로 월드를 바꾸는 것입니다. 싱글플레이 게임(컴퓨터 상대)은 문제없지만, 멀티플레이 게임에서는 다른 플레이어와 동기화가 깨집니다. 그래서 멀티플레이 게임에서는 JASS 채널이 읽기 전용 함수만 허용하고, 동료는 자동으로 “말만 하기”로 물러납니다.
가장 빠르게 시작하기: Farsight에서 바로 켜기
- 맵 선택: Farsight “인스턴스” 페이지 → “다음 게임 설정” → 맵에서 RPG 맵을 고릅니다(게임 폴더
Maps아래Scenario,Download에 있는 맵이 모두 표시됩니다. 예:(4)WarChasers). - 스킴 선택: 인스턴스 카드의 “AI 스킴” 드롭다운에서 동료 예제(buddy)를 고르고 → “선택”을 누릅니다.
- 테스트 시작: 게임이 뜨면 게임 창에서 직접 플레이하세요. 동료 — “빛나”라는 이름의 팔라딘 — 가 여러분 곁에 나타납니다.
명령줄로도 할 수 있습니다.
python tools/play.py --bot brains/examples/buddy.py --inst 20 --rpg --map "<게임 폴더>\Maps\Scenario\(4)WarChasers.w3m"
--rpg(스킴 매니페스트에서는 "judge": false)는 대전 규칙으로 승패를 판정하지 않는다는 뜻입니다. RPG에서는 영웅이 죽어도 부활할 수 있고, “건물이 전부 없어지면 패배”라는 규칙도 없습니다. 많은 RPG 맵은 로딩이 끝나면 “계속하려면 아무 키나 누르세요” 화면에서 멈춥니다. SDK는 “게임 중인데 게임 시계가 계속 0”인 상태를 발견하면 스스로 스페이스 키를 한 번 누릅니다(g.press_to_continue(). 게임 창에 키 메시지만 보내며 포커스를 빼앗지 않습니다).
나만의 동료 만들기
from openwar3.companion import Companion
from openwar3.talk import Talk
class MyBuddy(Companion):
mode = "ally" # 모드, 아래 표 참고
unit = "Hpal" # 무엇을 만들지: 아무 4자 코드나 가능, 맵 커스텀 유닛도 가능
nickname = "빛나"
heal = ("holybolt", "AHhb", 0.55) # (시전 오더 이름, 배울 스킬, 주인 체력이 이 비율 아래면 치유); None = 치유 안 함
follow_distance = 350
talk = Talk(persona="명랑한 꼬마 팔라딘, 주인을 응원하길 좋아함")
네 가지 모드
| mode | 동료는 누구인가 | 설명 |
|---|---|---|
ally(기본값) | 빈 플레이어 슬롯 하나를 차지해 여러분의 동맹이 됨 | 자기 색과 이름이 있음(점수판, 동맹 패널에 nickname 표시). 여러분이 명령할 수는 없고 스스로 싸움. 프레임워크가 자동으로 동맹 + 시야 공유로 설정 |
own | 여러분 소유로 생성 | 언제든 직접 지휘할 수 있음. 여러분이 손대지 않을 때는 AI가 대신 조작 |
adopt | 맵에 이미 있는 유닛을 넘겨받음 | adopt(g)를 재정의해 그 유닛을 반환(맵이 준 펫, 수행원) |
voice | 유닛을 만들지 않고 말만 함 | 말동무, 알림. 월드를 바꾸지 않으므로 멀티플레이 게임에서도 사용 가능 |
빈 슬롯이 없으면 ally는 자동으로 own으로 물러나고, 멀티플레이 게임이거나 유닛을 만들 수 없으면 자동으로 voice로 물러납니다.
ally 모드의 동료는 유닛 하나에 고정되지 않고 “그 슬롯에서 지금 가장 좋은 유닛”(영웅 우선)을 자기 몸으로 삼습니다. 실측에서 어떤 맵은 동료를 실제 플레이어로 취급해 팔라딘을 지우고 맵 영웅을 하나 지급했습니다 — 동료는 그 영웅을 바로 넘겨받고, 맵이 그 영웅에게 정해 둔 스킬도 배웁니다. 영웅이 죽으면 우선 제자리에서 부활시키고, 맵이 알아서 부활시키면 그대로 이어서 씁니다.
매 틱 하는 일
순서대로 확인해, 조건이 맞는 항목을 실행합니다.
| 순서 | 행동 | 조건 | 조정 |
|---|---|---|---|
| 1 | 후퇴 | 자기 체력이 25% 미만이고 근처에 적이 있음: 주인 뒤로 물러남 | retreat_at |
| 2 | 치유 | 주인 체력이 설정값 미만, 스킬 쿨다운이 끝남, 거리 900 이내 | heal(None이면 끔) |
| 3 | 전투 지원 | 주인 주변에 적이 있음: 주인을 공격 중인 적 > 주인이 공격 중인 적 > 가장 가까운 적 | assist_radius, 또는 pick_target 재정의 |
| 4 | 따라가기 | 주인과 너무 멀어지면 따라감. 일정 거리 이상 멀어지면 싸움을 끊고 바로 돌아옴 | follow_distance, leash |
| 5 | 잡담 | 적이 없을 때 1 ~ 2.5분마다 한 마디 | 대사 표 |
“적”은 게임 안의 동맹 관계로 판단합니다(20초마다 갱신). RPG 맵에는 동맹이 여럿인 경우가 많아서, “나를 제외한 플레이어”를 전부 적으로 취급할 수는 없습니다.
재정의할 수 있는 훅: find_master(누가 주인인지. 기본값은 로컬 플레이어의 레벨이 가장 높은 영웅), adopt, pick_target, on_poke(주인이 동료를 우클릭함), 그리고 Bot의 on_start / on_tick / on_event / on_end. 치유, 전투 지원, 처치, 따라가기, 후퇴, 말하기, 부활 횟수는 모두 self.stats에 기록되고 끝날 때 출력됩니다.
부르는 법
- 채팅 명령: 채팅창에
-follow따라오기,-stay제자리 지키기,-heal즉시 치유,-hi인사를 입력합니다. 명령 목록을 바꾸려면commands를, 반응을 바꾸려면on_command를 재정의하세요. - 동료 우클릭:
on_poke가 호출됩니다. 예제의 반응은 이렇습니다. 주인의 체력이 가득 차 있지 않으면 한 번 치유해 주고, 그렇지 않으면 한 마디 합니다. - 초상화 대사: 인사, 주인 쓰러짐, 주인 레벨 업, 동료 복귀 때의 대사는 게임 자체의 초상화 대사로 말합니다(하단 초상화가 동료로 바뀌고 화면에 자막이 나옴). 나머지는 머리 위 말풍선으로 띄웁니다.
- 상태 패널: 화면 왼쪽의 패널에 동료의 체력 바, 지금 하는 일, 기분(기쁨 / 흥분 / 긴장 / 두려움 / 슬픔), 처치 수와 치유 횟수를 표시합니다. 캔버스로 그리므로 멀티플레이 게임에서도 안전합니다.
말하기, 그리고 로컬 LLM
Talk는 이벤트에 따라 대사를 골라 머리 위 말풍선으로 띄웁니다. voice 모드이거나 말풍선을 띄울 수 없으면 화면 왼쪽 아래에 표시합니다. 모든 대사는 스킴 로그에도 기록되므로, 나중에 무슨 말을 했는지 확인할 수 있습니다.
| 이벤트 | 언제 | 이벤트 | 언제 |
|---|---|---|---|
hello | 막 도착했을 때 | master_low | 주인 체력이 낮을 때 |
poke | 주인이 동료를 우클릭했을 때 | master_levelup | 주인 레벨 업 |
fight | 전투 시작 | master_died / master_back | 주인 쓰러짐 / 부활 |
kill | 몬스터 하나를 처치(몬스터 이름을 말함) | buddy_low / buddy_died / buddy_back | 동료 자신의 체력 낮음 / 쓰러짐 / 복귀 |
healed | 주인을 치유함 | idle / item | 잡담 / 아이템 획득 |
대사에는 {master}, {me}, {map}, {enemy}, {level}, {item} 같은 자리표시자를 쓸 수 있습니다. 대사는 talk.lines에서, 쿨다운은 talk.cooldown에서 바꿉니다.
로컬 LLM 연결: Talk(llm=LocalLLM(url, model)). OpenAI 호환 API라면 무엇이든 됩니다(LM Studio, Ollama……). 모델은 백그라운드 스레드에서 답하고, 답이 도착해야 말합니다. 모델이 꺼져 있거나 시간이 초과되거나 오류가 나면 고정 대사를 말하므로 게임이 멈추지 않습니다. 요청은 여러분이 지정한 로컬 주소로만 보내며, 내용은 게임 안에서 일어난 일(주인 이름, 어떤 몬스터를 잡았는지)입니다.
커스텀 맵의 유닛 이름
RPG 맵의 유닛, 아이템, 영웅은 대부분 맵이 새로 만든 것이라(4자 코드가 HC07, I00A 같은 형태) 내장 이름표에서 찾을 수 없습니다. g.map_data는 현재 게임의 맵 파일을 직접 읽습니다.
md = g.map_data
md.name_of("HC07") # 'Optimus Primo' — 맵에서 바꾼 이름이 우선
md.hero_names("HC07") # 칭호 목록
md.hero_skills("OC10") # 맵이 이 영웅에게 정한 스킬
md.tooltip("I00A") # 설명 텍스트
보호되거나 최적화된 맵(인기 RPG 중 상당수)에는 표준 오브젝트 데이터 파일이 없으므로, 이름을 맵 안의 텍스트 데이터에서 읽습니다. 실측 결과 이 컴퓨터에 있는 RPG / 커스텀 맵 38개를 모두 파싱했고, 그중 37개에서 유닛 이름을 얻었습니다.
스킴으로 만들어 공유하기
동료는 openwar3.Bot의 하위 클래스일 뿐이므로 AI 스킴으로 만들어 다른 사람과 공유할 수 있습니다. 매니페스트에 두 항목을 더 씁니다.
{"id": "my-buddy", "name": "나의 동료", "entry": "my_buddy.py", "fair": false, "judge": false}
"fair": false: JASS 채널(유닛 생성, 동맹 설정)을 쓰기 위해 필요합니다. "judge": false: 대전 규칙으로 승패를 판정하지 않습니다.
실측 기록
2026-09-24, 테스트 인스턴스, WarChasers 맵, 2배속:
- JASS 채널 검사 18개 모두 통과: 플레이어 슬롯, 유닛과 핸들의 상호 변환, 실수 반환값, 문자열 인자, 빈 슬롯에 유닛 생성, 동맹 설정, 이름 변경, 유닛 삭제. 플레이어 레인에서 호출한 경우와 인자 개수가 틀린 경우는 올바르게 거부됨.
- 동료: 스스로 “계속하려면 아무 키나 누르세요”를 넘김 → 주인 곁에 나타나 인사 → 영웅 선택용 서클로 따라 들어갔다가 맵에서 영웅을 지급받아 넘겨받음 → 따라가기(주인과 200 ~ 400 거리) → 사냥, 한 마리를 잡고 “멋져!”라고 말함 → 체력이 낮아져 후퇴 → 전사 후 맵이 부활시켜 다시 따라감.
아직 안 된 것
- 플레이어가 입력한 임의의 채팅 문장은 읽을 수 없습니다. 정해진 채팅 명령은 이미 동작합니다. 동료가 정말로 자유롭게 대화하려면 문장 자체를 얻어야 합니다.
- 동료는 특정 맵의 게임 방식(퀘스트, 상점, 스토리)을 모릅니다. 범용적인 따라가기, 전투 지원, 치유만 합니다. 특정 맵을 이해시키려면 하위 클래스에서 그 맵에 맞게 작성하세요 —
g.map_data로 이름을 찾고,g.jass로 어떤 함수든 호출할 수 있습니다. 바로 이 부분이 여러분이 직접 정하도록 남겨 둔 몫입니다.