STUDIO HAZE · jeniapp
Topaz source
Open by house policy. Topaz is our own language.
Topaz 5.20.2 · 63 files
apps/slow-monster/main.tpz
Topaz · 20125 B · Original file
1
// slow.monster 의 진입점입니다. 입력 패킷 하나를 읽어 순수 코어로 다음 상태를 만들고 그 상태와 그리기 목록을 출력합니다.2
// 입력은 SM1, STATE:<직전 STATE 또는 빈칸>, EVENT:<init|resume|up|down|left|right|a|b|start|select|auto>, SHELL:<seed=,demo=,intent=,...> 네 줄이고 출력은 SM1, STATE:<새 STATE>, DRAW:<그리기 목록> 세 줄입니다.3
// 같은 패킷이면 늘 같은 출력입니다. 시드는 init 에서만 SHELL 을 따르고 그 뒤로는 STATE 안의 월드시드를 씁니다. demo 와 intent 도 입력의 일부라 켠 쪽과 끈 쪽이 각각 결정론입니다.4
// 무작위 조우는 없고 마주 본 엔티티에 A 를 눌러야 전투나 휴식이 일어납니다. 읽지 못한 STATE 는 기본 상태로 닫습니다.5
// slow.monster entry: one input packet in, the next STATE and its draw list out.7
import core.state8
import core.drawlist { 드로우리스트 }9
import core.creature10
import core.battle11
import core.encounter { 야생크리처 }12
import core.genome { 야생계통시드, 진화준비, 진화임계, 드리프트가두기 }13
import core.clash14
import core.evolution15
import data.maps { 맵타일들, 타일에서, 워프에서, 워프, 타일_물, 타일_나무 }16
import data.species17
import data.entities18
import core.auto { 자동결정 }19
import core.story { 파생비트, 켜진비트인가, 비트켜기, 비트끄기, 최하위비트, 비트수 }20
import core.util { 범위가두기 }22
let 기본시드 = "SM-SEED-001"24
// 공용 도우미 시작 (줄값 셸값)25
// 여러 도구가 같은 원본을 씁니다. 여기서 고치지 않고 원본에서 고친 뒤 다시 생성합니다.27
// 같은 접두의 줄이 여럿이면 마지막 줄을 씁니다.28
function 줄값(입력: string, 접두: string) -> string {29
let 맞는줄들 = 입력.split("\n").map(줄 => 줄.trim()).filter(줄 => 줄.startsWith(접두))30
if 맞는줄들.length == 0 { return "" }31
let 끝줄 = 맞는줄들[맞는줄들.length - 1]32
끝줄.slice(접두.scalars().length, 끝줄.scalars().length).trim()33
}35
// "키=값,키=값" 에서 = 로 정확히 두 조각인 항만 읽고 키와 값의 앞뒤 공백을 지웁니다. 같은 키가 여럿이면 마지막 값을 씁니다.36
function 셸값(셸: string, 키: string) -> string {37
let 값들 = 셸.split(",").map(항 => 항.split("=")).filter(조각들 => 조각들.length == 2 && 조각들[0].trim() == 키)38
if 값들.length == 0 { "" } else { 값들[값들.length - 1][1].trim() }39
}41
// 공용 도우미 끝43
type 이벤트 = "init" | "resume" | "up" | "down" | "left" | "right" | "a" | "b" | "start" | "select" | "auto" | "unknown"45
function 이벤트파싱(토큰: string) -> 이벤트 {46
match 토큰.trim() {47
case "init" => "init"48
case "resume" => "resume"49
case "up" => "up"50
case "down" => "down"51
case "left" => "left"52
case "right" => "right"53
case "a" => "a"54
case "b" => "b"55
case "start" => "start"56
case "select" => "select"57
case "auto" => "auto"58
case _ => "unknown"59
}60
}62
function 진행입력인가(이벤트값: 이벤트) -> bool {63
match 이벤트값 {64
case "a" | "b" => true65
case "init" | "resume" | "up" | "down" | "left" | "right" | "start" | "select" | "auto" | "unknown" => false66
}67
}69
function 파티슬롯교체(파티: Array<creature.전투원>, 자리: int, 새것: creature.전투원) -> Array<creature.전투원> {70
map(0..<파티.length, i => if i == 자리 { 새것 } else { 파티[i] })71
}73
// 물, 나무, 엔티티 칸은 막혀 제자리에서 방향만 바꿉니다. 옮긴 칸에 워프가 있으면 목적 맵의 목적 칸으로 갑니다.74
function 이동(현재: state.상태, 방향: state.방향) -> state.상태 {75
let 돌아선 = 현재.플레이어{ 방향: 방향 }76
let 새x = state.마주x(돌아선)77
let 새y = state.마주y(돌아선)78
let 타일 = 타일에서(맵타일들(현재.맵아이디), 새x, 새y)79
let 막힘 = 타일 == 타일_물 || 타일 == 타일_나무 || entities.활성엔티티칸인가(현재.맵아이디, 새x, 새y, 현재.처치된것들)80
let 목적x = if 막힘 { 현재.플레이어.x } else { 새x }81
let 목적y = if 막힘 { 현재.플레이어.y } else { 새y }82
let 도착 = match 워프에서(현재.맵아이디, 목적x, 목적y) {83
case Some(대상) => { 맵아이디: 대상.목적맵, x: 대상.목적x, y: 대상.목적y }84
case None => { 맵아이디: 현재.맵아이디, x: 목적x, y: 목적y }85
}86
현재{87
스텝: 현재.스텝 + 1,88
플레이어: { x: 도착.x, y: 도착.y, 방향: 방향 },89
맵아이디: 도착.맵아이디,90
장면: "field",91
야생: state.빈야생(),92
}93
}95
// 야생이면 그 종과 레벨로 압력전을 열고, 인물이면 쉬어 파티 전체를 채웁니다.96
function 상호작용(현재: state.상태) -> state.상태 {97
let 대상 = entities.활성엔티티에서(현재.맵아이디, state.마주x(현재.플레이어), state.마주y(현재.플레이어), 현재.처치된것들)98
match 대상.종류 {99
case 야생(종아이디, 레벨) => {100
let 적종 = species.종찾기(종아이디)101
현재{102
장면: "battle",103
야생: 야생크리처 { 종아이디: 적종.아이디, 이름: 적종.이름, 레벨: 레벨, 체력: 적종.기본체력, 스프라이트: 적종.스프라이트, 속성: 적종.속성 },104
배틀: clash.압력전시작(종아이디, 레벨),105
}106
}107
case 인물(_) => state.파티회복(현재)108
case 없음 => 현재109
}110
}112
// 전투 중에는 플레이어가 움직이지 않으므로 마주 본 칸이 여전히 그 야생입니다.113
function 처치적용(현재: state.상태) -> state.상태 {114
let 대상 = entities.활성엔티티에서(현재.맵아이디, state.마주x(현재.플레이어), state.마주y(현재.플레이어), 현재.처치된것들)115
if entities.야생인가(대상) { 현재{ 처치된것들: state.처치추가(현재.처치된것들, 대상.아이디) } } else { 현재 }116
}118
// SELECT 버튼과 Shift 키는 같은 select 이벤트를 보냅니다.119
function 필드적용(이벤트값: 이벤트, 직전: state.상태) -> state.상태 {120
match 이벤트값 {121
case "up" => 이동(직전, "위")122
case "down" => 이동(직전, "아래")123
case "left" => 이동(직전, "왼쪽")124
case "right" => 이동(직전, "오른쪽")125
case "a" => 상호작용(직전)126
case "start" => 직전{ 장면: "party", 파티커서: 0 }127
case "select" => 직전{ 장면: "dex" }128
case "init" | "resume" | "b" | "auto" | "unknown" => 직전129
}130
}132
// 복원한 커서는 범위를 벗어날 수 있어 한쪽 끝만 넘어갈 때 반대쪽으로 돕니다.133
function 이전커서(커서: int, 길이: int) -> int {134
if 커서 - 1 < 0 { 길이 - 1 } else { 커서 - 1 }135
}137
function 다음커서(커서: int, 길이: int) -> int {138
if 커서 + 1 >= 길이 { 0 } else { 커서 + 1 }139
}141
// A 는 커서의 동료를 초점으로 삼아 이제 그 한 마리가 배틀하고 진화합니다.142
function 파티적용(이벤트값: 이벤트, 직전: state.상태) -> state.상태 {143
let 길이 = 직전.파티.length144
match 이벤트값 {145
case "b" | "start" => 직전{ 장면: "field", 파티커서: 0 }146
case "up" => if 길이 <= 0 { 직전 } else { 직전{ 장면: "party", 파티커서: 이전커서(직전.파티커서, 길이) } }147
case "down" => if 길이 <= 0 { 직전 } else { 직전{ 장면: "party", 파티커서: 다음커서(직전.파티커서, 길이) } }148
case "a" => if 길이 <= 0 { 직전 } else { 직전{ 초점: 범위가두기(직전.파티커서, 0, 길이 - 1) } }149
case "init" | "resume" | "left" | "right" | "select" | "auto" | "unknown" => 직전150
}151
}153
// 커서는 발견 순서로 돌고 닫았다 열어도 같은 노드를 가리킵니다.154
function 도감적용(이벤트값: 이벤트, 직전: state.상태) -> state.상태 {155
let 길이 = 직전.계통노드들.length156
match 이벤트값 {157
case "b" | "select" => 직전{ 장면: "field" }158
case "left" | "up" => if 길이 <= 0 { 직전 } else { 직전{ 장면: "dex", 도감커서: 이전커서(직전.도감커서, 길이) } }159
case "right" | "down" => if 길이 <= 0 { 직전 } else { 직전{ 장면: "dex", 도감커서: 다음커서(직전.도감커서, 길이) } }160
case "init" | "resume" | "a" | "start" | "auto" | "unknown" => 직전161
}162
}164
function 배틀갱신(직전: state.상태, 새배틀: battle.배틀상태, 새활성: creature.전투원) -> state.상태 {165
직전{ 장면: "battle", 파티: 파티슬롯교체(직전.파티, 직전.초점, 새활성), 배틀: 새배틀 }166
}168
// 패배하면 체력을 채우되 계통시드, 드리프트, 진화 형태는 지우지 않습니다.169
function 배틀종료(직전: state.상태, 새활성: creature.전투원, 패배: bool) -> state.상태 {170
let 남길전투원 = if 패배 { creature.회복(새활성) } else { 새활성 }171
직전{ 장면: "field", 야생: state.빈야생(), 파티: 파티슬롯교체(직전.파티, 직전.초점, 남길전투원), 배틀: battle.빈배틀() }172
}174
// 같은 시드로 같은 스텝, 칸, 종에서 잡으면 같은 계통시드라 같은 게놈입니다.175
function 포획성공처리(직전: state.상태, 새볼: int, 새포획카운터: int) -> state.상태 {176
let 계통시드 = 야생계통시드(직전.월드시드, 직전.스텝, 직전.플레이어.x, 직전.플레이어.y, 직전.야생.종아이디)177
let 잡은것 = creature.종에서전투원(직전.야생.종아이디, 직전.야생.레벨, 계통시드)178
let 추가 = state.파티추가(직전.파티, 직전.보관함, 잡은것)179
직전{180
장면: "field",181
야생: state.빈야생(),182
스포어볼: 새볼,183
포획카운터: 새포획카운터,184
파티: 추가.파티,185
보관함: 추가.보관함,186
계통노드들: state.노드추가(직전.계통노드들, state.시작노드(잡은것, 직전.스텝)),187
배틀: battle.빈배틀(),188
}189
}191
// 후보는 저장하지 않고 부모 스냅샷에서 다시 유도합니다. 부모고유번호는 고정한 뒤 자식 노드에 찍혀 계통수의 가지를 잇습니다.192
function 진화장면진입(직전: state.상태, 활성: creature.전투원) -> state.상태 {193
// 진화 상태 전체에 타입을 달면 인터프리터와 웹 런타임이 게놈 타입의 모듈을 찾지 못해 단계만 좁혀 둡니다. 토파즈 과제 '구조 타입 주석 속 다른 모듈 레코드' 의 우회입니다.194
let 고르기: state.진화단계 = "choose"195
let 새진화 = {196
단계: 고르기,197
부모형태: if 활성.형태이름 != "" { 활성.형태이름 } else { species.종찾기(활성.종아이디).이름 },198
부모게놈: 활성.게놈,199
부모계통시드: 활성.계통시드,200
부모진화카운터: 활성.진화카운터,201
부모세대: 활성.세대,202
부모고유번호: evolution.계통고유번호(활성.계통시드),203
선택커서: 0,204
잠긴분기: 0,205
결과형태: "",206
결과게놈: 활성.게놈,207
}208
직전{ 장면: "evolve", 야생: state.빈야생(), 진화: 새진화, 배틀: battle.빈배틀() }209
}211
// 메뉴와 고정이 같은 함수로 후보를 보므로 늘 같은 후보입니다.212
function 진화후보들(직전: state.상태) -> Array<evolution.후보> {213
let 진 = 직전.진화214
evolution.후보들유도(진.부모게놈, evolution.계통고유번호(진.부모계통시드), 진.부모계통시드, 진.부모진화카운터)215
}217
// 계통은 패배에서 배웁니다. 드리프트를 얻고 체력을 채우며, 준비되면 그 자리에서 진화해 더 강해져 다시 붙습니다. 야생은 맵에 남습니다.218
// 그래서 약한 옛 저장도 죽음의 반복 대신 적응으로 빠져나옵니다.219
function 배틀패배처리(직전: state.상태, 활성: creature.전투원) -> state.상태 {220
let 학습 = creature.드리프트설정(활성, 드리프트가두기(활성.드리프트 + 진화임계))221
if 진화준비(학습.드리프트) { 진화장면진입(배틀갱신(직전, battle.빈배틀(), 학습), 학습) } else { 배틀종료(직전, 학습, true) }222
}224
// 진화장면진입은 키운 전투원이 이미 초점 자리에 들어간 상태를 받아야 합니다.225
function 배틀결과단계적용(이벤트값: 이벤트, 직전: state.상태, 활성: creature.전투원, 데모: bool) -> state.상태 {226
if !진행입력인가(이벤트값) { return 직전 }227
match 직전.배틀.결과 {228
case "caught" => 처치적용(포획성공처리(직전, 직전.스포어볼 - 1, 직전.포획카운터 + 1))229
case "win" => {230
let 성장 = battle.경험적용(활성, creature.획득경험(직전.야생.레벨), 데모)231
let 키운 = 성장.전투원232
if 성장.오른레벨 > 0 && 진화준비(키운.드리프트) {233
처치적용(진화장면진입(배틀갱신(직전, battle.빈배틀(), 키운), 키운))234
} else {235
처치적용(배틀종료(직전, 키운, false))236
}237
}238
case "lose" | "fled" | "" => 배틀패배처리(직전, 활성)239
}240
}242
// 숲의 뿌리가 몇 그루 생길 때까지 파티 세 마리 미만이면 포획을 원하고, 그 뒤로는 쓰러뜨려 초점을 키웁니다.243
function 배틀비트단계적용(이벤트값: 이벤트, 직전: state.상태, 활성: creature.전투원) -> state.상태 {244
if !진행입력인가(이벤트값) { return 직전 }245
let 결과 = clash.비트진행(직전.월드시드, 직전.배틀, 활성, 직전.야생.종아이디, 직전.야생.레벨, 직전.스포어볼, 직전.파티.length < 3)246
배틀갱신(직전, 결과.배틀, 결과.플레이어)247
}249
// 옛 메뉴 전투의 menu, move, message 단계도 압력전 비트로 이어집니다.250
function 배틀적용(이벤트값: 이벤트, 직전: state.상태, 데모: bool) -> state.상태 {251
let 활성 = state.초점전투원(직전)252
match 직전.배틀.단계 {253
case "result" => 배틀결과단계적용(이벤트값, 직전, 활성, 데모)254
case "clash" | "menu" | "move" | "message" => 배틀비트단계적용(이벤트값, 직전, 활성)255
}256
}258
function 진화커서설정(직전: state.상태, 커서: int) -> state.상태 {259
직전{ 장면: "evolve", 진화: 직전.진화{ 선택커서: 커서 } }260
}262
// 고정은 되돌릴 수 없습니다. 진화 하위 상태를 morph 로 바꿔 한 프레임 동안 페이지가 모핑 장면을 재생하게 합니다.263
function 진화고정(직전: state.상태) -> state.상태 {264
let 진 = 직전.진화265
let 분기 = 범위가두기(진.선택커서, 0, 진화후보들(직전).length - 1)266
let 결과 = evolution.분기진화(state.초점전투원(직전), 분기, 진.부모세대, 진.부모진화카운터)267
let 모핑: state.진화단계 = "morph"268
let 새진화 = 진{269
단계: 모핑,270
선택커서: 분기,271
잠긴분기: 결과.분기,272
결과형태: 결과.형태이름,273
결과게놈: 결과.전투원.게놈,274
}275
직전{276
장면: "evolve",277
야생: state.빈야생(),278
파티: 파티슬롯교체(직전.파티, 직전.초점, 결과.전투원),279
계통노드들: state.노드추가(직전.계통노드들, evolution.노드빌드(결과, 진.부모고유번호, 직전.스텝)),280
진화: 새진화,281
배틀: battle.빈배틀(),282
}283
}285
function 진화종료(직전: state.상태) -> state.상태 {286
직전{ 장면: "field", 야생: state.빈야생(), 진화: state.빈진화(), 배틀: battle.빈배틀() }287
}289
// 모핑 프레임 뒤에는 어떤 키든 진화를 끝냅니다. none 은 생기지 않지만 생기면 필드로 돌아갑니다.290
function 진화적용(이벤트값: 이벤트, 직전: state.상태) -> state.상태 {291
let 진 = 직전.진화292
match 진.단계 {293
case "morph" | "none" => 진화종료(직전)294
case "choose" => {295
let 수 = 진화후보들(직전).length296
match 이벤트값 {297
case "left" | "up" => 진화커서설정(직전, 이전커서(진.선택커서, 수))298
case "right" | "down" => 진화커서설정(직전, 다음커서(진.선택커서, 수))299
case "a" => 진화고정(직전)300
case "init" | "resume" | "b" | "start" | "select" | "auto" | "unknown" => 직전301
}302
}303
}304
}306
// 도달했지만 아직 보지 않은 이정표를 본것과 대기에 켭니다. 본것은 큐에 넣을 때 켜서 저장과 재유도에서 겹치지 않습니다.307
// 필드에서만 이야기 카드로 넘어가고 전투, 진화, 도감 중에는 큐에만 쌓았다가 필드로 돌아올 때 띄웁니다.308
function 스토리갱신(현재: state.상태) -> state.상태 {309
let 도달 = 파생비트(현재)310
let 새비트들 = filter(0..<비트수, k => 켜진비트인가(도달, k) && !켜진비트인가(현재.스토리.본것, k))311
let 새본것 = 새비트들.reduce(현재.스토리.본것, (비트셋, k) => 비트켜기(비트셋, k))312
let 새대기 = 새비트들.reduce(현재.스토리.대기, (비트셋, k) => 비트켜기(비트셋, k))313
let 새장면: state.장면 = if 현재.장면 == "field" && 새대기 != 0 { "story" } else { 현재.장면 }314
현재{ 장면: 새장면, 스토리: { 본것: state.비트가두기(새본것), 대기: state.비트가두기(새대기) } }315
}317
// 지금 카드를 큐에서 빼고 남은 카드가 없으면 필드로 갑니다.318
function 스토리진행(직전: state.상태) -> state.상태 {319
let 비트 = 최하위비트(직전.스토리.대기)320
let 새대기 = if 비트 >= 0 { 비트끄기(직전.스토리.대기, 비트) } else { 직전.스토리.대기 }321
let 새장면: state.장면 = if 새대기 != 0 { "story" } else { "field" }322
직전{ 장면: 새장면, 스토리: { 본것: 직전.스토리.본것, 대기: state.비트가두기(새대기) } }323
}325
// auto 는 코어가 정한 보통 입력 하나를 같은 엔진으로 적용합니다. 정한 입력이 없으면 수동 갈림길처럼 멈춥니다.326
function 적용(이벤트값: 이벤트, 직전: state.상태, 데모: bool, 의도: int) -> state.상태 {327
if 이벤트값 == "auto" {328
let 자동입력 = 이벤트파싱(자동결정(직전, 의도))329
if 자동입력 == "unknown" { return 직전 }330
return 적용(자동입력, 직전, 데모, 의도)331
}332
match 직전.장면 {333
case "story" => 스토리진행(직전)334
case "battle" => 배틀적용(이벤트값, 직전, 데모)335
case "party" => 파티적용(이벤트값, 직전)336
case "evolve" => 진화적용(이벤트값, 직전)337
case "dex" => 도감적용(이벤트값, 직전)338
case "field" => 필드적용(이벤트값, 직전)339
}340
}342
function 역직렬화또는기본(문자열: string, 시드: string) -> state.상태 {343
match state.역직렬화(문자열) {344
case Ok(상태값) => 상태값345
case Err(_) => state.기본상태(시드)346
}347
}349
// 의도 0 은 진화 갈림길에서 멈춰 플레이어가 고르고 1 은 첫 가지, 2 는 무작위 가지입니다.350
// 모든 경로를 스토리갱신으로 감싸 init 의 첫 조우 비트가 오프닝 카드로 열립니다. resume 은 어떤 장면이든 행동 없이 그대로 다시 그립니다.351
function 처리(입력: string) -> string {352
let 이벤트값 = 이벤트파싱(줄값(입력, "EVENT:"))353
let 셸 = 줄값(입력, "SHELL:")354
let 셸시드 = 셸값(셸, "seed")355
let 시드 = if 셸시드 != "" { 셸시드 } else { 기본시드 }356
let 데모 = 셸값(셸, "demo") == "1"357
let 의도 = match 셸값(셸, "intent") {358
case "1" => 1359
case "2" => 2360
case _ => 0361
}362
let 상태문자열 = 줄값(입력, "STATE:")363
let 새상태 = match 이벤트값 {364
case "init" => 스토리갱신(state.기본상태(시드))365
case "resume" => 스토리갱신(역직렬화또는기본(상태문자열, 시드))366
case "up" | "down" | "left" | "right" | "a" | "b" | "start" | "select" | "auto" | "unknown" => 스토리갱신(적용(이벤트값, 역직렬화또는기본(상태문자열, 시드), 데모, 의도))367
}368
"SM1\nSTATE:{state.직렬화(새상태)}\nDRAW:{드로우리스트(새상태)}"369
}371
input() |> 처리 |> printSHA256 34510305cabd580dfa6781f680f3f05e38a4b811de312eca37af2f4113280566