콘텐츠로 이동
GameDesign

태그 시스템 초안미구현밸런싱미완료

본 문서는 괴람의 태그 시스템을 정의한다. 태그는 아이템·몬스터·맵 사이의 의미적 시너지·컨셉 결합·세계관 일관성을 만드는 매칭 메커니즘이다. 카테고리·등급·슬롯 같은 분류축은 태그가 아니며, 각자 전용 컬럼으로 운영한다.

태그는 아이템·몬스터·상자·맵에 부여되어, 드랍 후보를 의미적으로 좁히는 메커니즘이다.

  • 같은 지역에서 같은 문화권의 아이템이 같이 등장하도록 만든다.
  • 같은 컨셉(예: 산악·인간형·근접)의 묶음을 만들어 빌드와 세계관 일관성을 보장한다.
  • 절대적인 분류가 아니라 의미적 친화도를 표현한다.

다음은 태그로 운영하지 않는다. 각자 전용 컬럼으로 둔다.

항목전용 컬럼이유
최상위 카테고리category1 (ItemManager.Category1)시스템 전체가 1차 분기로 쓰는 절대 분류
세부 카테고리category2 (ItemManager.Category2)슬롯·자산 규격이 따르는 절대 분류
등급grade (ItemGrade)7단계 절대 축, 보정 대상
파츠 슬롯partsType무기 조립 구조상 절대 슬롯

분리 원칙:

  • 분류는 컬럼, 의미 시너지는 태그.
  • 분류는 1:1·1:N의 절대축, 태그는 다중·계열·매칭축.
  • 한 아이템이 “이 카테고리에 속한다”는 컬럼으로, “이 컨셉과 어울린다”는 태그로.
  • 컬럼이 가질 수 있는 값(OneHandedSword, Rare, MainParts 등)을 태그 문자열로 옮겨 적지 않는다.

아이템 분류 정책 §“태그와의 관계”가 이 분리의 정책 근거다.

태그는 계열로 묶인다. 각 계열은 하나의 의미축을 담당한다.

  • 한 아이템·몬스터는 한 계열 안에서 여러 태그를 동시에 가질 수 있다.
  • 서로 다른 계열의 태그는 다른 의미축을 표현한다.

현재 코드(ItemTag.cs)에 실제 구현된 의미 계열은 다음 4종이다 (어휘는 개발 진행 중 값).

계열 (코드 enum)현재 어휘매칭 의미
AreaBaseCamp, LumberYard, Hamlet등장 지역(맵)
ForcePeasant, Outlaw, Samurai세력
LocationVillage, Mountain, Military위치/장소
TypeTool, Gear, Material, Valuable, Document아이템 종류

본 문서 다른 절의 예시에서 쓰는 region/theme 등은 구조 설명용 가칭이며, 실제 계열명은 위 4종이다.

같은 계열 안은 OR · 다른 계열끼리는 AND · 양쪽 빈 칸은 통과.

조건처리
같은 계열 안 여러 태그OR — 하나라도 겹치면 그 계열 통과
다른 계열끼리AND — 모든 명시 계열이 통과해야 최종 매칭
Query 측 빈 계열그 축 검사 안 함 (skip)
Entity 측 빈 계열그 축 무관 / 범용 — 통과
Query 측 NeverMatch그 쿼리는 어떤 엔티티와도 매칭 안 함 (의도 차단)
Entity 측 Any (옵션)그 축 명시적 범용 — 통과 (빈 칸과 같은 효과)

핵심: 각자 명시한 축만 검사한다. 안 적은 가족 때문에 매칭 실패가 일어나지 않는다.

matches(entity, query):
for each family F:
# 1) Query가 그 축에 관심 없음 → skip
if query[F] is empty: continue
# 2) Query가 의도적 차단
if query[F] contains NeverMatch: return false
# 3) Entity가 그 축 무관 → 통과 (빈 칸 또는 명시적 Any)
if entity[F] is empty: continue
if entity[F] contains Any: continue
# 4) 같은 계열 안 OR
if intersect(entity[F], query[F]) is empty: return false
return true # 모든 명시 계열 통과 → AND 만족
토큰의미사용 측
NeverMatch의도적 매칭 차단 (디버그·임시 차단·테스트)Query 전용
Any명시적 범용 (그 축 무조건 통과). 빈 칸과 효과 동일하나 의도를 명시Entity 전용
Exclude (가칭)그 축에서 명시적으로 제외 — 미도입, 후속 검토 (§11)Entity 전용 후보

규칙:

  • NeverMatch는 Entity에 쓰면 검증 에러. Any는 Query에 쓰면 검증 에러 (Query의 “그 축 무관”은 빈 칸으로 표현).
  • 어휘 마스터 표에 없는 문자열은 시트 임포트 단계에서 경고.

태그는 두 가지 역할로 분리된다. 같은 어휘 마스터를 공유하지만 의미·운영이 다르다.

측면Entity (아이템 측)Query (몹·상자 측)
정체성”나는 이 값들에 소속이다” — 자기 선언”나는 이런 후보를 원한다” — 좁히기 요청
시트 컬럼아이템 시트 entityTags (또는 가족별 분리)몹·상자 시트 queryTags_<카테고리>_<가족> (분리 권장)
같은 계열 다중값”이 모든 값에 동시 소속” — 쿼리가 그중 하나만 일치해도 통과”이 중 어느 하나라도 갖는 후보를 받는다” — OR 통과
빈 계열 의미그 축 무관 / 범용 → 통과그 축 검사 안 함 (skip) → 통과
특수 토큰Any (명시적 범용)NeverMatch (의도 차단)
어휘 출처가족별 어휘 마스터 표 (공유)같은 마스터 표 (공유)
운영 책임”이 아이템의 정체” 정의”이 몹·상자 풀의 컨셉” 정의

5.1 의도된 대칭 — 양쪽 빈 칸 모두 통과

섹션 제목: “5.1 의도된 대칭 — 양쪽 빈 칸 모두 통과”

가장 중요한 디자인 결정: 빈 칸은 양쪽 다 통과로 처리한다. 비대칭이 아니다.

  • 디자이너가 아이템 컨셉에 맞는 가족만 태깅하고 나머지는 비워둘 수 있다.
  • 디자이너가 몹 컨셉에 맞는 가족만 명시하고 나머지는 비워둘 수 있다.
  • 한쪽이 적게 적었다고 매칭 실패가 일어나지 않는다.

옵션 비교 — 다른 선택지(엄격형: Entity 빈 칸 = fail)도 가능하지만, 디자이너가 무생물 아이템(조개·골드 등)에 모든 가족을 일일이 Any로 채우는 부담을 피하기 위해 **관대형(빈 칸 = 무관)**을 채택했다.

아이템 시트 (Entity) — 가족별 분리 컬럼 권장:

entity_region: ["바다"]
entity_theme: [] # 무관
entity_culture: ["동양"]
entity_material: ["조개"]

몹·상자 시트 (Query) — 카테고리별 × 가족별 분리:

queryTags_weapon_region: ["산"]
queryTags_weapon_theme: ["인간형"]
queryTags_armor_region: ["산"]
queryTags_misc_region: ["바다"]
...

→ 가족별 분리 컬럼은 시트 검증·자동 완성을 쉽게 하고 어휘 마스터 통제를 강제한다.

태그 매칭은 카테고리 1차 분기 다음 단계에서 작동한다.

1차 (카테고리 분기)
몹/상자가 "무기 1개, 방어구 2개, 잡템 3개"를 요구
→ 각 슬롯은 해당 카테고리 풀에서 후보를 찾는다.
2차 (태그 매칭)
카테고리별 풀 안에서 몹/상자의 의미 태그로 후보를 좁힌다.
→ "산 + 인간형"이면 산악·인간형 컨셉 무기 중에서만 1개 추첨.
3차 (가치·등급 추첨)
가치 가중치(WorthRarity) → 등급 분포(DropTier) 적용.

이 분리가 검색 부하·어휘 통제·풀 운영 모두에서 중요하다 (§7 참조).

연결 문서:

7. 분류 만능 태그의 함정 — 왜 분리해야 하는가

섹션 제목: “7. 분류 만능 태그의 함정 — 왜 분리해야 하는가”

태그를 “드랍 필터 만능 도구”로 사용하려는 충동은 흔하다. 시트 컬럼이 줄고 코드 분기가 줄어드는 것처럼 보이기 때문이다. 그러나 다음의 비용이 따른다.

  • 분류를 카테고리 컬럼으로 두면 풀은 카테고리별로 사전 분기된다 — 매칭은 좁혀진 풀 안에서만.
  • 분류를 태그에 합치면 풀이 한 통에 섞인다 — 매번 전체 아이템에 대해 비트 매칭을 돌려야 한다.
  • 비트마스크 자체는 빠르지만 풀 크기가 곱연산으로 늘어난다.
  • 풀이 커질수록 매 드랍 추첨의 매칭 비용이 누적된다 (몹 N마리 × 풀 M개 × 계열 K축).
  • 카테고리는 enum/컬럼으로 통제되어 오타·중복이 시스템적으로 차단된다.
  • 분류를 태그(문자열)로 옮기면 Sword, sword, OneHandedSword 같은 변형이 시트마다 섞인다.
  • 의미 시너지 태그도 이 오염에 휩쓸려 더 이상 의미축으로 기능하지 못한다.
  • 한 계열에 분류·슬롯·등급·컨셉이 다 섞이면 “이 태그는 무엇을 의미하는가”가 모호해진다.
  • 매칭 결과가 “왜 이 아이템이 같이 나왔는가”를 설명하지 못한다.
  • 디자이너의 의도(세계관·시너지)가 시스템에서 사라진다.
  • 분류를 enum으로 박으면 신규 카테고리 추가 시 코드 변경·재컴파일 필요.
  • 분류는 컬럼·enum으로, 의미는 시트의 통제된 어휘로 운영해야 데이터 단독 변경이 가능하다.

8. 현재 구현과의 차이 (AS-IS vs TO-BE)

섹션 제목: “8. 현재 구현과의 차이 (AS-IS vs TO-BE)”

코드 분석 기준 정리. ItemTag.cs, ItemDataBase.cs 검토 결과 (2026-05-28 기준 — 의미 계열 4종이 추가된 상태).

지난 진단(의미 계열 0개) 이후 개발자가 의미 계열 4종(Area/Force/Location/Type)을 추가했다. 현재 코드에는 9개 계열이 공존한다.

계열 (코드 enum)성격
MainCategoryWeapon/Armor/Parts/Utility… 9종분류 중복Category1과 1:1. BuildEntityTags가 자동 주입.
SubCategoryOneHandedSword/Head/Potion… 28종분류 중복Category2와 1:1. 자동 주입.
TierTier1~Tier8분류 중복 — grade 축.
PartsBlade/Tang/Guard/Grip/Pommel/Scabbard파츠 슬롯 (분류성).
MonsterTypeBandit/Ashigaru/Man/Dog몹 종류 (분류성).
AreaBaseCamp, LumberYard, Hamlet의미 — 지역(맵)
ForcePeasant, Outlaw, Samurai의미 — 세력
LocationVillage, Mountain, Military의미 — 위치/장소
TypeTool, Gear, Material, Valuable, Document의미 — 아이템 종류

결론: 의미 시너지 계열이 4종 생겼다 (의도 방향 검증됨). 다만 분류 중복 5종이 그대로 남아 있어 충돌·자동주입 부작용이 발생한다 (§8.1.1).

8.1.1 Material 어휘 충돌 (실제 버그)

섹션 제목: “8.1.1 Material 어휘 충돌 (실제 버그)”

Material3개 계열에 동시 존재한다: MainCategory.Material, SubCategory.Material, Type.Material.

FromTags의 파싱 순서가 MainCategory(4번째) → … → Type(9번째)이라, Material 토큰은 항상 MainCategory.Material로 먼저 잡히고 Type.Material에 도달하지 못한다.

  • MiscDropTagMaterialquery.mainCategory = Material (Type 아님).
  • BuildEntityTags가 모든 아이템에 자기 Category1을 mainCategory로 자동 주입 → 포션(Utility) vs query(Material) → (Utility & Material)=0탈락.
  • → 디자이너는 “재료 종류(Type:Material)“를 의도했으나 실제로는 Category1이 Material인 아이템만 통과, 포션·돈·잡화 전부 제외. 의도 완전 상실.

이것이 §7.2 “어휘 통제 실패”의 실물 사례. 분류 중복 계열 제거 또는 어휘 네임스페이스 분리로 해소해야 한다.

bool tierMatch = queryTags.tier == Tier.All || (tier & queryTags.tier) != 0;
// ... 9개 계열 동일 패턴 (tier/parts/monsterType/mainCategory/subCategory/area/force/location/type)
return tierMatch && partsMatch && ... && areaMatch && forceMatch && locationMatch && typeMatch;
  • 같은 계열 안: 비트 AND (!= 0) → OR 매칭 ✓ 의도와 일치.
  • 다른 계열끼리: && 연결 → AND 매칭 ✓ 의도와 일치.
  • 매칭 엔진은 의도 그대로. 단 9계열 전부 AND라, Entity 빈 칸 fail 버그(§8.3)와 겹치면 매칭 대량 실패 위험이 계열 수만큼 커진다.

8.3 빈 칸 처리 — 코드의 비대칭 vs 의도된 대칭

섹션 제목: “8.3 빈 칸 처리 — 코드의 비대칭 vs 의도된 대칭”

의도(§5.1)는 **양쪽 빈 칸 모두 통과(대칭)**다. 그러나 현재 코드는 한쪽만 통과하는 비대칭이다.

Query.FromTags 끝부분에서 9개 계열 각각을 None일 때 All로 전환:

if (tier == Tier.None) tier = Tier.All;
// ... parts/monsterType/mainCategory/subCategory/area/force/location/type 모두 동일

같은 처리가 3군데: Query.FromTags(...) (빈 계열 → All), Query.FromTags(null) (전체 All), Query.Empty/기본 생성자 (전체 All).

반면 Entity.FromTags는 None을 그대로 유지한다. Entity의 None은 매칭에서 (None & query) == 0이 되어 fail 처리된다.

빈 칸의 코드 동작의도(§5.1)일치?
Query (몹/상자)자동 All → 통과skip → 통과✅ 결과 동일
Entity (아이템)None 유지 → fail무관 → 통과❌ 어긋남

버그 지점은 Entity 쪽. Query는 우연히 의도와 같은 결과(통과)지만, Entity 빈 칸이 fail로 처리되어 §5.1의 대칭 규칙이 깨진다.

위험 시나리오 — Entity 빈 칸이 fail이라서 생기는 누락

섹션 제목: “위험 시나리오 — Entity 빈 칸이 fail이라서 생기는 누락”
  1. 몹 X: Location:[Mountain], Force:[Samurai].
  2. 아이템 조개: Location:[Village] 정도만, Force 칸 비어 있음.
  3. 의도대로면 Force 축은 조개가 무관(통과) → Location에서만 판정.
  4. 그러나 코드는 조개의 Force None을 fail 처리 → 조개가 Force 명시한 모든 몹 풀에서 부당하게 누락.
  5. 계열을 새로 도입할수록(현재 9계열) 기존 아이템들이 그 축에서 일제히 누락된다.
  • Entity 빈 칸을 “그 축 통과”로 처리 (§4.3 의사코드 3번 if entity[F] is empty: continue).
  • Query의 None → All 자동 채움은 결과가 같지만, “그 축 skip” 의미로 명시 재작성 권장 (가독성·신규 계열 안전).
  • NeverMatch(line 104108, 228236)는 의도 차단 토큰으로 유지. Any는 Entity 명시 범용 토큰으로 추가.
항목현재 (AS-IS)의도 (TO-BE)
계열 구성분류 중복 5종 + 의미 4종(Area/Force/Location/Type)분류 중복 5종 제거(컬럼으로), 의미 계열 유지·확장
Material 어휘 충돌3계열 중복 → 파싱 순서가 MainCategory로 가로챔분류 중복 제거 또는 계열 네임스페이스 분리
매칭 알고리즘같은 계열 OR / 다른 계열 AND (9계열)유지 (이미 일치)
Query 빈 칸자동 All → 통과그 축 skip → 통과 (결과 동일, 표현만 명시)
Entity 빈 칸Nonefail그 축 무관 → 통과 (대칭, §5.1) — 수정 필요
Entity 범용 토큰없음Any 명시 토큰 도입
NeverMatch운영 토큰유지 (디버그·임시 차단용)

확정 시점부터 적용할 운영 규칙. 마이그레이션은 §10.

  1. 분류는 태그에 넣지 않는다. category1·category2·grade·partsType은 전용 컬럼만 사용한다.
  2. 태그 계열은 의미 시너지 축만 둔다. 신규 계열을 추가할 때는 본 문서와 어휘 표를 함께 갱신한다.
  3. 빈 칸은 양쪽 다 “그 축 통과”로 처리한다 (§5.1 대칭). Query 빈 칸 = 검사 안 함, Entity 빈 칸 = 무관. 빈 칸 때문에 매칭 실패가 일어나지 않는다.
  4. 누락 안전망은 룰이 아니라 린트로 챙긴다. “모든 계열이 다 빈 아이템”처럼 디자이너 실수로 의심되는 경우만 빌드 단계 경고. 정상 운영(일부 계열만 채움)은 경고하지 않는다.
  5. 태그 어휘는 enum이 아닌 시트로 통제한다. 어휘 마스터 표를 두고 시트가 그 표를 참조한다. 마스터에 없는 값은 임포트 경고.
  6. 토큰 권한을 분리한다. NeverMatch는 Query 전용(의도 차단), Any는 Entity 전용(명시 범용). 반대편 사용은 검증 에러.

코드 수정 작업은 본 문서가 아니라 별도 작업 항목으로 다룬다. 본 절은 정리 방향 메모.

  • ItemTag.MainCategory/SubCategory enum 제거 검토 — 본가 ItemManager.Category1/Category2는 유지. 분류 매칭은 category1/category2 컬럼 동등 비교로 이전.
  • ItemTag.Tier enum 검토 — 본가 ItemManager.ItemGrade 유지. grade/worth 컬럼으로 이전 가능한지 점검.
  • BuildEntityTags의 카테고리 자동 OR 부여 제거 (분류 중복 + Material 충돌의 직접 원인).
  • Material 어휘 충돌 해소 — 분류 중복 계열(MainCategory/SubCategory) 제거가 근본책. 임시로는 계열별 접두어 네임스페이스(Type.Material 등) 도입 검토.
  • Entity 빈 칸 fail → 통과로 수정 (§8.3 핵심 버그). Entity.Matches에서 빈 계열은 skip.
  • Query 측 None → All 자동 전환은 결과 동일하나 “skip” 의미로 명시 재작성 권장. Any는 Entity 전용 토큰으로 도입.
  • 의미 계열은 이미 Area/Force/Location/Type로 신설됨 — 어휘 마스터 표로 통제 이전.
  • 기존 몹·상자·아이템 시트 일괄 마이그레이션 절차 정의.
  • 의미 시너지 계열의 확정 어휘 (region·theme·culture·material 외 무엇이 필요한가).
  • Any 명시 토큰의 정확한 이름과 운영 (계열별 별도 토큰인지 공통 토큰인지).
  • Exclude 토큰(옵션 C) 도입 여부 — “이 아이템은 이 축에서 명시적으로 제외”를 표현. 현재는 미도입(빈 칸 = 무관 통과)이지만, 특정 컨셉 몹에서 특정 아이템을 빼고 싶을 때 필요할 수 있음.
  • 분류 enum 제거 시 기존 코드의 영향 범위 (특히 ItemTagEnumGenerator의 자동 생성 로직).
  • 태그 어휘 마스터 시트의 위치와 운영 주체.
  • 누락 안전망 린트의 판정 기준 (어떤 패턴을 “디자이너 실수”로 볼 것인가).