본문으로 건너뛰기

dotgimon 제작기 14. 조그레스 전용 완전체 6종 추가

·15 min read·14 / 32

dotgimon의 조그레스는 서로 다른 계통의 몬스터 두 마리를 근거리 통신으로 연결해 합체 진화시키는 기능이다. 두 기기에서 각자 새 몬스터로 진화하고, 어느 쪽도 상대의 몬스터를 가져가지 않는다. 조합마다 양쪽에 결과가 하나씩 필요했는데, 여섯 조합의 결과 12칸에 같은 종이 반복되고 있었다.

날짜는 작업 순서에 맞춰 배치했다. 실제 변경은 9월 30일 전후에 저장소에 한꺼번에 반영했으며, 종 수와 진화 단계는 이 작업 당시 기준이다.

결과는 12칸인데 몬스터는 6종이었다

게임에서 버전은 몬스터의 여섯 시작 계통을 뜻한다. 모든 종은 한 버전에 속하고, 일반 진화는 그 안에서만 이어진다. 조그레스도 서로 다른 버전의 성숙기(stage 3)가 만나 각자 자기 버전의 완전체(stage 4)가 되는 구조였다.

게임의 조합표 FUSION_PAIRS에서 앞의 두 값은 재료 종이고 뒤의 두 값은 각 재료 쪽의 결과다. 결과를 완전체로 정리한 뒤에도 표에는 중복이 남아 있었다.

// src/game/link.ts (변경 전)
export const FUSION_PAIRS = [
  ['sluggo', 'gronk', 'heliox', 'astrox'],
  ['saurn', 'lupo', 'heliox', 'lunarch'],
  ['rexor', 'hornbug', 'heliox', 'lunarch'],
  ['oozor', 'sasquat', 'novadon', 'eclipse'],
  ['spookle', 'shellug', 'astrox', 'nebulon'],
  ['oddball', 'thornip', 'eclipse', 'lunarch'],
]

Heliox와 Lunarch는 세 번씩, Astrox와 Eclipse는 두 번씩 나왔다. Novadon과 Nebulon까지 합쳐도 서로 다른 종은 6종뿐이었다. Sluggo와 Gronk, Saurn과 Lupo, Rexor와 Hornbug가 만나도 한쪽 결과는 늘 Heliox였다. 누구와 연결하느냐에 따라 다른 몬스터를 얻는 재미가 약했다.

도감에서도 같은 문제가 보였다. 조그레스 탭에는 재료 두 종과 양쪽 결과를 보여줬는데, 여러 행에서 같은 결과가 반복됐다. 다른 조합을 채워도 새 종을 모은다는 느낌이 덜했다.

아트 카탈로그의 monsterArt.fusionRecipes도 실제 조합표와 어긋나 있었다. 여기에는 Steelrex와 Bonerex로 Heliox·Lunarch를 얻는 조합이나 Pebbit와 Boltbit처럼 옛 설계의 재료가 남아 있었다. 게임과 카탈로그가 같은 조합표를 쓰도록 함께 고쳤다.

조합을 줄이면 키우던 종이 빠졌다

결과가 6종이니 조합을 세 개로 줄이는 방법도 있었다. 중복은 없어지지만 이미 조그레스에 참가하던 종 일부가 빠진다. 중복을 그대로 두면 그림을 더 만들 필요는 없지만, 연결 상대를 바꾸거나 도감을 채우는 재미도 그대로였다.

원화와 도트, 발사체까지 만드는 작업이 늘어도 결과 종을 12종으로 늘리는 쪽을 골랐다. 조합 수와 참가 자격을 유지하면서 결과를 다르게 만들 수 있었다.

기존 Heliox, Astrox, Lunarch, Eclipse, Novadon, Nebulon은 ID와 소속 버전을 유지했다. 이미 저장된 몬스터나 생애 기록이 업데이트만으로 다른 종이 되는 것은 원하지 않았다. 신규 결과도 재료와 같은 버전에 넣고, 당시 규칙에 맞춰 더 진화하지 않는 완전체로 만들었다.

조그레스로 얻는다는 이유로 전투 수치를 더 주지는 않았다. 자연진화 완전체와 같은 체급을 쓰고, 위압적인 외형과 전투 수치는 따로 정했다.

재료는 그대로 두고 결과 네 줄을 바꿨다

FUSION_PAIRS의 결과를 다시 배정했다. 굵게 표시한 Basalorn, Cindrake, Frostscar, Veilmaw, Briarhelm, Rimepod가 새로 만든 6종이다.

조합재료 A (버전)재료 B (버전)A 쪽 결과B 쪽 결과
1Sluggo (1)Gronk (3)HelioxAstrox
2Saurn (1)Lupo (2)BasalornLunarch
3Rexor (1)Hornbug (2)CindrakeFrostscar
4Oozor (5)Sasquat (4)NovadonEclipse
5Spookle (3)Shellug (6)VeilmawNebulon
6Oddball (4)Thornip (2)BriarhelmRimepod

조합표에서는 네 줄의 결과만 바뀌었다. 결과를 고르는 resolveFusion은 내 종을 첫 인자로 받아 내 버전의 결과를 돌려준다.

// src/game/link.ts (변경 후)
/** [재료 A, 재료 B, A 버전 전용 결과, B 버전 전용 결과]. */
export const FUSION_PAIRS: readonly [string, string, string, string][] = [
  ['sluggo', 'gronk', 'heliox', 'astrox'],
  ['saurn', 'lupo', 'basalorn', 'lunarch'],
  ['rexor', 'hornbug', 'cindrake', 'frostscar'],
  ['oozor', 'sasquat', 'novadon', 'eclipse'],
  ['spookle', 'shellug', 'veilmaw', 'nebulon'],
  ['oddball', 'thornip', 'briarhelm', 'rimepod'],
]
 
export function canFuse(id: string): boolean {
  return BY_ID[id]?.stage === 3 && FUSION_PAIRS.some(([a, b]) => a === id || b === id)
}
 
export function resolveFusion(idA: string, idB: string): string | null {
  if (!canFuse(idA) || !canFuse(idB)) return null
  if (!MONSTER_VERSION[idA] || MONSTER_VERSION[idA] === MONSTER_VERSION[idB]) return null
  for (const [a, b, resultA, resultB] of FUSION_PAIRS) {
    if (a === idA && b === idB) return resultA
    if (b === idA && a === idB) return resultB
  }
  return null
}

상대 기기에서는 인자 순서가 반대다. 누가 호스트인지와 상관없이 같은 표에서 각자의 결과를 얻는다. 같은 버전끼리는 MONSTER_VERSION 비교에서, 성숙기가 아닌 종은 canFuse에서 걸렀다.

신규 종은 dex.ts에도 추가했다. 진화 대상 목록인 evo를 비워 둔 완전체 데이터였다.

// src/game/dex.ts (추가)
{ id: 'basalorn',  name: 'Basalorn',  stage: 4, attr: 'Sun',  evo: [] },
{ id: 'cindrake',  name: 'Cindrake',  stage: 4, attr: 'Sun',  evo: [] },
{ id: 'frostscar', name: 'Frostscar', stage: 4, attr: 'Moon', evo: [] },
{ id: 'veilmaw',   name: 'Veilmaw',   stage: 4, attr: 'Star', evo: [] },
{ id: 'briarhelm', name: 'Briarhelm', stage: 4, attr: 'Free', evo: [] },
{ id: 'rimepod',   name: 'Rimepod',   stage: 4, attr: 'Moon', evo: [] },

attr의 Sun·Moon·Star·Free는 통신 대전의 상성에 쓰는 속성이다. 신규 종은 일반 진화로 얻을 수 없어서 도감의 도달 가능 종 목록인 REACHABLE 끝에 따로 넣고, 버전별 소속 목록 VERSION_SPECIES에도 추가했다. 버전별 종 수는 17/17/16/16/15/12종이 됐다. 일반 81종과 조그레스 12종을 합쳐 총 93종이었다.

12칸의 중복은 집합 크기로 검사했다

조합표는 손으로 고치는 데이터라 같은 결과를 두 번 넣거나 버전을 잘못 배정하기 쉬웠다. 결과가 모두 다르고 각자 부모의 버전을 유지한다는 조건을 테스트에 넣었다.

// src/game/link.test.ts (발췌)
it('모든 조합의 양쪽 결과는 서로 다른 완전체이며 부모의 버전을 유지한다', () => {
  const results = FUSION_PAIRS.flatMap(([, , a, b]) => [a, b])
  expect(new Set(results).size).toBe(FUSION_PAIRS.length * 2)
  for (const [a, b, resultA, resultB] of FUSION_PAIRS) {
    expect(MONSTER_VERSION[resultA]).toBe(MONSTER_VERSION[a])
    expect(MONSTER_VERSION[resultB]).toBe(MONSTER_VERSION[b])
  }
  for (const id of results) {
    expect(BY_ID[id]?.stage).toBe(4)
    expect(REACHABLE).toContain(id)
  }
})

결과 목록을 Set으로 만들면 중복이 사라진다. 그 크기가 조합 수의 두 배와 같은지 비교하면 12칸 중 하나라도 겹쳤을 때 테스트가 실패한다. 결과 종의 버전과 stage 4 여부, 도감 목록에 들어 있는지도 함께 검사했다.

통신으로 받은 데이터도 별도로 확인했다. 조그레스 모드인 m: 'F'에서는 도감에 있는 성숙기가 아닌 종을 모두 넣어 보고 INELIGIBLE로 거부하는지 검사했다. 내 화면에서 참가를 막아도 상대 기기가 보낸 값까지 보장되지는 않기 때문이다. 기존 테스트의 Saurn·Lupo 조합 기대값도 Basalorn·Lunarch로 바꿨다.

같은 부모에서 나와도 체형부터 달랐다

데이터에 종을 추가하는 것보다 그림을 만드는 일이 더 컸다. 몬스터는 AI로 컬러 원화를 만든 뒤 흑백 LCD에 쓸 32×32 도트로 줄였다. 신규 6종도 같은 과정을 거쳤다.

프롬프트에는 완전체다운 묵직한 실루엣과 크고 단순한 덩어리를 요구했다. 32px로 줄였을 때 얼굴과 눈이 구분되도록 얼굴은 밝게, 눈은 어둡게 잡았다. 가늘고 긴 부속물이나 세밀한 질감은 뺐다.

종마다 두 부모의 특징을 섞되 체형을 다르게 정했다. 원화 프롬프트에서 잡은 콘셉트는 다음과 같았다.

종부모원화 콘셉트
BasalornSaurn + Lupo화산암 판갑을 두른 네발 양늑대. 크림색 해골 얼굴과 말린 뿔
CindrakeRexor + Hornbug용광로 공룡과 장수풍뎅이. 갈라진 이마 뿔과 직립한 몸
FrostscarHornbug + Rexor빙하색 사슴벌레 요새 짐승. 여섯 다리와 둥근 등
VeilmawSpookle + Shellug떠다니는 유령 앵무조개. 소용돌이 껍질과 보라색 후드
BriarhelmOddball + Thornip거대한 나무 수호자. 세 장의 잎 관과 씨앗 가면 얼굴
RimepodThornip + Oddball떠다니는 서리 씨앗 수호자. 얼음빛 꽃잎 방패 네 장

Cindrake와 Frostscar는 같은 재료 쌍에서 나오지만 하나는 두 발로 서고 다른 하나는 여섯 다리로 움직이는 갑충으로 잡았다. 색만 다르면 같은 그림을 복사한 것처럼 보였다. Briarhelm과 Rimepod도 나무와 떠다니는 씨앗으로 체형을 갈랐다.

프롬프트에는 “Not a biped”, “Clearly an arthropod, not a dinosaur”처럼 피할 체형도 적었다. 생성한 원화는 두 부모와 나란히 놓은 parent-comparison.png로 비교했다.

도트는 자동 양자화, 곧 원화의 색을 제한된 값으로 줄이는 과정을 거친 뒤 손으로 보정했다. Basalorn을 뺀 다섯 종은 밝은 얼굴과 어두운 눈을 분리하고 복잡한 디테일을 줄였다. 보정 전 결과는 pixels.quantized.txt로 남겼다.

Astrox는 새 원화를 쓰지 못했다

기존 조그레스 종도 아트 목록에서 완전체(stage 4)로 맞췄다. 이전 목록에는 5단계로 적혀 있었다. Heliox, Lunarch, Eclipse, Nebulon 네 종은 원화와 도트를 두 번째 버전인 r002로 교체했다.

Astrox도 다시 그렸지만 가로로 긴 시안은 32px에서 실루엣이 약해 버렸다. 직립한 근육질 유령 도마뱀으로 바꿔 요청한 프롬프트는 이미지 생성기에서 moderation_blocked로 막혔다. Astrox와 Novadon은 기존 r001 원화를 유지했다.

실패 기록은 generation-blocked.json, 버린 시안은 attempts/too-horizontal.png로 남겼다. 지우면 어떤 방향을 이미 시도했는지 몰라 같은 요청을 반복할 수 있었다.

신규 종은 몬스터 이미지를 연결하는 스프라이트 맵과 발사체 맵 projectileMap에도 등록했다. 에셋 목록에는 원화 93/93종, 미제작 0종이 남았다. 도트도 모두 갖췄지만 새 그림을 확인한 범위는 확대 정지 이미지까지였다. 실제 폰 화면에서는 확인하지 못했다.

조합 여섯 개와 성숙기라는 참가 조건은 유지한 채 결과 12칸을 서로 다른 완전체로 채웠다. 기존 종의 ID와 버전을 바꾸지 않아 저장 데이터를 옮길 필요도 없었다. 이미 키운 몬스터와 생애 기록은 그대로 두고, 새로 조그레스하는 몬스터부터 바뀐 결과를 얻었다.