# UI와 입력

> 캔버스 위의 버튼과 선택 카드를 클릭할 수 있고, 마우스를 올리면 자동으로 강조됩니다. 단축키를 등록하고, 지면을 클릭해 위치를 고르고, 마우스가 가리키는 곳을 읽고, 로컬 플레이어가 무엇을 선택했는지 알 수 있습니다. 클릭, 단축키, 스킬 시전, 채팅 전문, 플레이어 퇴장이 모두 이벤트 스트림에 들어옵니다.

출처: https://war3ai.com/ko/docs/ui-input/

[캔버스](https://war3ai.com/ko/docs/canvas/)로 그린 것을 이제 **클릭할 수 있습니다**. 런타임이 게임 창의 입력을 넘겨받으므로, 외부 프로그램은 다음을 할 수 있습니다.

| 기능 | 한 줄 설명 | 게임에 전달되는가 |
|---|---|---|
| **클릭 가능한 캔버스 항목** | 버튼, 선택 카드, 패널: 클릭하면 `ui.click` 발생, 마우스를 올리면 자동 강조 | 버튼 위를 누른 클릭은 **전달되지 않음** |
| **단축키** | `F5`, `ctrl+shift+Q` 같은 조합을 등록하면, 누를 때 `hotkey` 발생 | 가로챌지 선택 가능(그 키가 만들어 내는 문자까지 포함) |
| **지면 클릭** | 월드를 클릭하면 지면 좌표와 함께 `mouse.world` 발생 | 가로챌지 선택 가능("위치를 클릭해 탑 짓기") |
| **마우스 위치** | 매 프레임 갱신: 화면 픽셀, 가리키는 지면 지점, 마우스가 올라가 있는 캔버스 항목 | — |
| **선택** | 로컬 플레이어가 무엇을 선택했는지. 바뀌면 바로 `selection.changed` 발생 | — |

모두 **로컬 입력 + 로컬 드로잉**입니다. 명령 스트림에 들어가지 않으므로 멀티플레이 게임에서도 안전합니다. 다만 콜백에서 월드를 바꾸면(유닛 생성, 속성 변경) 여전히 싱글플레이 게임에서만 쓸 수 있습니다.

## Python: g.ui

```python
ui = g.ui                                                   # 처음 쓸 때 런타임이 창 입력을 넘겨받음
ui.button("shop", "물약 사기(금 50)", screen=(40, 300), on_click=lambda g, ev: buy(g))
c = ui.choice("레벨 업! 보상을 하나 고르세요", [("힘 +5", "더 튼튼하게"), ("공격 속도 +20%", "더 강하게"), ("늑대 소환", "도우미 하나 추가")],
              pause=True, on_pick=lambda g, i: give(g, i))  # 화면 가운데에 카드 한 줄. pause=True면 고르는 동안 게임 일시 정지
i = c.wait(timeout=30)                                      # 블로킹으로 기다릴 수도 있음(그동안에도 이벤트를 계속 처리하므로 유실 없음)
ui.hotkey("F5", lambda g, ev: g.say(hero, "알겠어요!"))     # 기본값은 가로챔
ui.hotkey("ctrl+shift+Q", on_press=..., swallow=False)
ui.mouse(on_click, capture=True, buttons=("left", "right"))  # 지면 클릭 잡기: 왼쪽·오른쪽 버튼 모두 알리고 가로챔
xy = ui.pick_point("지면을 클릭하세요: 탑을 어디에 지을까요?")  # 블로킹 버전: 다음 왼쪽 버튼 지면 클릭 -> (x, y). Esc나 시간 초과 -> None
ui.cursor()                                                 # {'screen': (x, y), 'world': (x, y, z) 또는 None, 'hover': 'shop'}
ui.toast("3번째 웨이브가 옵니다!", seconds=3)
ui.close()                                                  # 자기 컨트롤과 단축키를 치움. 입력을 쓰는 다른 프로그램이 없을 때만 창 입력을 돌려줌
g.close()                                                   # 또는 연결 전체를 끊음(with Game(...) as g:로 써도 됨)
```

콜백의 인자는 `(g, ev)`이며, `g.events()`를 호출할 때 실행됩니다 — Bot과 [게임플레이 모드](https://war3ai.com/ko/docs/mods/)의 러너는 매 틱 이를 호출합니다. 콜백을 지정하지 않은 클릭은 `ui.clicks`에 쌓입니다. 콜백에서 예외가 나도 로그만 남기며, 다른 콜백과 이벤트에는 영향을 주지 않습니다.

캔버스를 직접 써도 됩니다: `g.canvas.text(..., clickable=True, hover=색)`. 클릭은 이벤트 스트림에서 받으며, `ev.key`는 그릴 때 지정한 key입니다. 클릭 가능한 요소를 그리면 입력이 자동으로 켜지므로, `g.ui`를 먼저 건드릴 필요가 없습니다.

**단축키 표기**: `F1` ~ `F24`, `A` ~ `Z`, `0` ~ `9`, `numpad0` ~ `numpad9`, `space enter esc tab backspace insert delete home end pageup pagedown left up right down`. 앞에 `ctrl+`, `shift+`, `alt+`를 붙일 수 있습니다.

> **주의**
>
> 보조 키 없는 문자와 숫자는 채팅 입력, 게임 단축키와 충돌합니다. F5 ~ F8처럼 게임이 쓰지 않는 키나 조합 키를 우선 쓰세요.

## 새 이벤트

`g.events()`에 다음이 추가되었습니다(전체 필드는 [W3P 프로토콜](https://war3ai.com/ko/docs/protocol/) 참고).

| kind | 언제 | 편의 필드 |
|---|---|---|
| `ui.click` | 상호작용 가능한 캔버스 항목이 클릭됨 | `.key` 캔버스 key, `.button`(`'left'` / `'right'`), `.mods` 보조 키 |
| `ui.hover` | 마우스가 캔버스 항목에 들어가거나 나감 | `.key`(나갈 때는 `None`) |
| `hotkey` | 등록한 단축키가 눌림 | `.key` 단축키 표기, `.mods` |
| `mouse.world` | 지면 클릭을 켰을 때, 월드를 클릭함 | `.x .y` 지면 좌표, `.button`, `.value`(1 = 가로챔) |
| `selection.changed` | 로컬 플레이어의 선택이 바뀜 | 유닛은 `g.selection()`으로 가져옴 |
| `spell.cast` | 유닛이 스킬을 시전함(스킬 쿨다운 시작) | `.spell` 4자 코드, `.b` 레벨, `.value` 쿨다운 초, `.x .y` 시전 지점 |
| `message` | 화면 메시지 영역에 한 줄이 표시됨 | `.text` 전체 텍스트, `.frame` 어느 영역인지, `.chat`(채팅일 때) |
| `player.left` | 플레이어가 나가거나 패배 판정으로 제거됨 | `.player` |
| `game.ended` | 게임에서 나감 | — |

## 채팅과 화면 메시지

플레이어가 채팅창에 입력한 내용은 `message` 이벤트의 `.chat`에서 바로 읽습니다.

```python
for ev in g.events():
    if ev.kind == "message" and ev.chat and ev.chat["text"] == "-follow":
        ...                                    # ev.chat = {'channel': '모두', 'sender': '플레이어 이름', 'text': '-follow'}
```

`g.messages()`는 별도의 커서를 가지며, 게임 안내("농장이 더 필요합니다", "그곳에는 건설할 수 없습니다")도 여기에 들어 있습니다. Bot을 작성할 때 이것을 쓰면 명령이 왜 실패했는지 알 수 있습니다.

## 다른 언어에서 쓰기

- **게이트웨이**: `ui.button`, `ui.choice`, `ui.hotkey`, `ui.mouse`, `ui.cursor` 같은 메서드를 [게이트웨이](https://war3ai.com/ko/docs/gateway/)에서 같은 이름으로 쓸 수 있습니다. 원격에서는 콜백 함수를 넘길 수 없으므로, 클릭과 단축키는 이벤트 푸시로 받습니다(`ui.click` 이벤트에 `key`가 들어 있음).
- **공유 메모리에 직접 쓰기**: 먼저 시맨틱 명령 `input_enable`(W3P 연산 코드 74)을 보내면 런타임이 입력을 넘겨받기 시작합니다. 입력 블록 `Local\War3Input_<pid>`에 클라이언트가 단축키 표와 마우스 스위치를 쓰고, 런타임은 마우스 위치, 가리키는 지면 지점, 마우스가 올라가 있는 항목을 되씁니다. 캔버스 항목의 플래그 비트 `0x40`은 "상호작용 가능"을 뜻합니다. 레이아웃은 [W3P 프로토콜](https://war3ai.com/ko/docs/protocol/)을 참고하세요.

## 여러 프로그램이 동시에 쓸 때

모드, Farsight, MCP, 게이트웨이의 각 세션이 한 게임에 동시에 버튼을 놓고 단축키를 등록해도 서로 간섭하지 않습니다.

- 각 프로그램은 자기 단축키와 지면 클릭 스위치를 등록하고, SDK가 모두의 것을 표 하나로 합쳐 런타임에 넘깁니다. 같은 키는 한 줄만 남기며, 이벤트는 모두에게 보내고 각자 키로 자기 단축키를 알아봅니다.
- `ui.close()`는 자기 것만 거둬들이며, 마지막 프로그램이 떠나야 창 입력을 돌려줍니다.
- 프로그램이 강제로 종료되어 뒷정리를 못 한 경우: 런타임이 2초마다 확인해, 등록한 프로그램이 모두 종료되었으면 그들이 남긴 단축키와 지면 클릭 가로채기를 지우고, 그들이 그린 버튼도 더 이상 클릭을 가로채지 않습니다.

## 실측

2026-09-25, 테스트 인스턴스에서 실게임 확인 16/16:

- 버튼 클릭 → `ui.click` + 콜백, 런타임의 가로채기 카운트 +1(게임은 이 클릭을 받지 못함). 버튼 바깥 클릭은 발생하지 않음
- F6 → `hotkey`. 지면 클릭 → `mouse.world`(가로챔)
- 팔라딘 하나를 생성해 선택 → `selection.changed`, `g.selection()`과 일치. 신성한 보호막 시전 → `spell.cast('AHds', 1, 35.0)`
- 맵 텍스트 → `message`. 채팅 → `message`, `.chat`에서 발신자와 내용을 파싱
- 컴퓨터 패배 처리 → `player.left`. 게임 종료 → `game.ended`

사람이 직접 마우스로 버튼을 클릭하는 것과 마우스를 올렸을 때의 강조도 하나씩 눈으로 확인했습니다.

## 한계와 주의 사항

- **위치는 실제 마우스 기준입니다**: 게임은 시스템 커서로 위치를 읽으므로, 호버와 `cursor()`는 실제 마우스를 반영합니다. 가로채기는 누르는 입력(클릭과 키)에만 적용됩니다.
- **마우스 포인터 아래에 그립니다**: 워크래프트는 포인터를 화면의 일부로 매 프레임 그려 넣습니다. 캔버스와 머리 위 말풍선은 모두 게임이 포인터를 그리는 단계 직전에 그리므로, 게임 UI는 덮고 포인터에는 덮입니다. 해당 프레임에 포인터를 그리지 않을 때(숨겨졌거나 컷신)만 마지막 단계에서 그립니다.
- **시스템 배율**: 직접 테스트를 작성해 창 메시지로 클릭을 보낼 때, DPI를 인식하지 않는 프로세스가 보낸 좌표는 시스템이 확대합니다(150% 배율에서 실측 ×1.5). 테스트 프로그램은 먼저 DPI 인식을 선언하세요. 사람이 직접 클릭하는 경우에는 영향이 없습니다.
- **처음에는 글꼴 워밍업**이 필요하며 약 1초가 걸립니다. 그동안에는 버튼이 아직 그려지지 않아 클릭할 수 없습니다.
- **게임 중이 아닐 때는 지면 클릭을 알리지 않습니다**: 메인 메뉴와 결과 화면에서는 `mouse.world`를 보내지도, 가로채지도 않습니다.
- 눌렀을 때 가로챈 클릭이라도, 떼기 전에 다른 프로그램으로 전환하거나 마우스를 창 밖으로 끌고 나가면 상태가 초기화되므로, 다음 떼기까지 가로채는 일은 없습니다.
- 1.27에는 새 게임 UI 프레임을 만드는 함수가 없습니다(1.31부터 있음). 여기의 버튼과 카드는 모두 런타임이 그린 것이라 스타일은 자유롭지만, 게임 자체의 메뉴 계층에는 나타나지 않습니다.
