docs: 보스 설계 문서와 Codex YAML 도구 추가
- 프로젝트 AGENTS 지침에 커밋 후 푸시 확인 규칙과 런타임 콘텐츠 에이전트 규칙을 보강 - 보스 설계 플레이북과 멀티플레이 보스 설계 철학 문서를 추가해 사람용 설계 기준을 정리 - Codex-Tools 디렉터리에 Markdown→YAML→검증 파이프라인 스크립트와 프롬프트, 스키마, 설정 파일을 추가
This commit is contained in:
19
AGENTS.md
19
AGENTS.md
@@ -14,7 +14,7 @@ Multiplayer arena game built with **Unity 6000.3.10f1** and **Unity Netcode for
|
||||
Design docs are maintained in Obsidian Vault: `/mnt/smb/Obsidian Vault/Colosseum`
|
||||
|
||||
- Always use the shared Obsidian Vault at `/mnt/smb/Obsidian Vault/Colosseum` for project documentation updates.
|
||||
- **Obsidian Vault 파일은 반드시 파일 도구(read/write/edit)로만 접근한다.** cmd, powershell, bash 등은 UNC 경로를 정상적으로 처리하지 못하므로 사용하지 않는다.
|
||||
- 현재 환경에서 접근 가능한 셸(`bash`, `zsh` 등) 기준으로 Obsidian Vault 파일을 열고 수정해도 된다.
|
||||
- Unless the user explicitly names another file, `체크리스트` means `/mnt/smb/Obsidian Vault/Colosseum/개발/프로토타입 체크리스트.md`.
|
||||
- After completing work, always check whether the prototype checklist should be updated and reflect the new status when needed.
|
||||
- If the work also changes a domain-specific checklist such as boss/player/combat, sync that checklist too.
|
||||
@@ -337,6 +337,7 @@ public class NetworkedComponent : NetworkBehaviour
|
||||
- After Unity-related edits, check the Unity console for errors before proceeding.
|
||||
- For networked play tests, prefer a temporary non-conflicting test port when needed and restore the default port after validation.
|
||||
- The user has a strong project preference that play mode must be stopped before edits because network ports can remain occupied otherwise.
|
||||
- When the user requests a commit, create the commit and show the message, then **ask before pushing**. Only push after explicit confirmation.
|
||||
- Commit messages should follow the recent project history style: use a type prefix such as `feat:`, `fix:`, or `chore:` and write the subject in Korean so the gameplay/UI/system change is clear from the log alone.
|
||||
- When the change is substantial, include a blank line after the subject and add Korean bullet points that summarize the work by feature/purpose, similar to commit `0889bb0` (`feat: 드로그 집행 개시 패턴 및 낙인 디버프 추가`).
|
||||
- Prefer the format `type: 한글 요약` for the subject, then `- 변경 사항` bullet lines for the body. Use `feat:` for feature work, `fix:` for bug fixes, and `chore:` for non-feature maintenance such as scene/prefab cleanup, asset reorganization, or other miscellaneous upkeep. The body should mention key implementation scope, affected systems/assets, and validation or rollback notes when relevant.
|
||||
@@ -348,3 +349,19 @@ public class NetworkedComponent : NetworkBehaviour
|
||||
- 프로젝트 용어로는 `필살기`보다 `고위력 기술`을 우선 사용한다.
|
||||
- 보호막은 단일 값이 아니라 `타입별 독립 인스턴스`로 취급한다. 같은 타입은 자기 자신만 갱신되고, 서로 다른 타입은 공존하며, 흡수 순서는 적용 순서를 따른다.
|
||||
- 보스 시그니처 전조는 가능한 한 정확한 진행 수치 UI보다 명확한 모션/VFX로 읽히게 한다. 차단 진행도나 정확한 누적 수치 노출은 명시 요청이 없으면 피한다.
|
||||
|
||||
# Runtime Content Agent Rules
|
||||
|
||||
Purpose:
|
||||
Keep boss YAML compatible with game runtime expectations.
|
||||
|
||||
Always:
|
||||
- validate boss YAML shape before commit
|
||||
- preserve `boss_id` stability
|
||||
- keep runtime field names machine-readable and snake_case
|
||||
- prefer additive changes over destructive changes
|
||||
|
||||
Never:
|
||||
- change mechanic intent to satisfy schema formatting
|
||||
- silently rename IDs
|
||||
- auto-fill required combat values without a design source
|
||||
|
||||
309
Boss_Design_Playbook.md
Normal file
309
Boss_Design_Playbook.md
Normal file
@@ -0,0 +1,309 @@
|
||||
# Boss & Encounter Design Playbook
|
||||
|
||||
이 문서는 **4인 파티 협동 / 자유 역할 비율 / 논타겟 액션 / 보스 러쉬 / 점진 성장형 로그라이트** 게임을 위한 사람용 설계 문서다.
|
||||
|
||||
핵심 철학:
|
||||
- 보스는 체력 덩어리가 아니라 **역할 수행과 패턴 해결을 시험하는 협동 퍼즐**이다.
|
||||
- 전투 난이도는 운 좋은 고성능 빌드보다 **역할 분배, 포지셔닝, 기믹 처리, 개인 수행**을 우선 시험해야 한다.
|
||||
- 시그니처 패턴은 형태보다 **문제와 해법이 명확해야** 한다.
|
||||
- 긴 전투(약 10분)는 **3~4 페이즈 이상**과 **주기적 새 메커닉 도입**이 필요하다.
|
||||
|
||||
---
|
||||
|
||||
## 1. 게임 전제
|
||||
|
||||
### 파티 구조
|
||||
- 4인 파티 고정 상정
|
||||
- 자유 빌드형 역할 분배
|
||||
- 큰 축: 탱커 / 딜러 / 지원
|
||||
- 각 플레이어는 역할 비율로 기여 가능
|
||||
- 예: Tank 0.8 / DPS 0.2 / Support 0.0
|
||||
|
||||
### 역할 의미
|
||||
- **탱커**: 최고 위협 유지, 보스 방향/위치 안정화, 이동 후 재정렬
|
||||
- **딜러**: 지속 DPS, 순간 DPS 체크, 기믹 처리 화력
|
||||
- **지원**: 힐, 보호막, 버프, 자원 회복, 상태이상 해제, 부활
|
||||
|
||||
### 전투 구조
|
||||
- 자유 이동, 3인칭, 논타겟 액션
|
||||
- 공용 회피(구르기) + 약간의 무적 시간
|
||||
- 블록은 완전 방어보다 피해 감소 성격
|
||||
- 보스 타겟 선정은 기본적으로 Aggro/Threat 기반, 일부 패턴은 랜덤/거리 기반
|
||||
- 작은 패턴의 지속 압박 + 큰 패턴의 시험 구조
|
||||
- 전투 중 부활 존재, 부활은 긴 캐스팅을 요구
|
||||
- 명시적 시간 제한은 지양하지만, 무한 전투 방지를 위해 보스는 점진 강화 가능
|
||||
|
||||
### 런 구조
|
||||
- 기본 세팅 상점
|
||||
- [보스 전투 → 랜덤/고정 보상 획득 → 상점/성장] * n
|
||||
- 보스 수 3~4
|
||||
- 보스 1회 전투 약 10분 목표
|
||||
- 스테이지는 순차 증가
|
||||
- 스테이지 1개 = 기본적으로 보스 전투 1회
|
||||
- 한 번의 보스 전투는 단일 개체가 아닐 수 있음 (adds, 다중 보스, 군집형 가능)
|
||||
|
||||
### 성장 구조
|
||||
- 고정 보상: 재화, 패시브 스킬 포인트
|
||||
- 랜덤 보상: 장비 드랍, 상점 품목, 선택형 강화 등
|
||||
- 빌드 변화 강도: Medium
|
||||
- 역할 전환은 가능하나, 시작 역할에서 완전히 벗어나기는 어려움
|
||||
- 파워 그래프는 점진적 증가
|
||||
- 보스는 운빨이 아니라 **역할 분배와 수행 능력**을 검증해야 함
|
||||
|
||||
---
|
||||
|
||||
## 2. 설계 철학 요약
|
||||
|
||||
### 2.1 보스 = Encounter
|
||||
이 게임에서 보스는 단일 개체가 아니라 **전투 상황 전체**다.
|
||||
|
||||
### 2.2 Role Pressure
|
||||
좋은 보스는 역할을 이름으로 요구하지 않고, **각 역할이 해결해야 하는 문제를 지속적으로 만든다.**
|
||||
|
||||
- Tank Pressure: 고정 공격, 위협 유지, 위치 안정화
|
||||
- Support Pressure: 광역 피해 복구, 상태이상 해제, 지속 유지력 소모 대응
|
||||
- DPS Pressure: 지속 화력, 순간 화력, 기믹 대상 파괴
|
||||
|
||||
### 2.3 Single-Core Solve Pattern
|
||||
시그니처 패턴은 여러 종류를 뒤섞기보다,
|
||||
- 문제 1개
|
||||
- 해법 1개
|
||||
- 실패 이유 1개
|
||||
처럼 명확해야 한다.
|
||||
|
||||
### 2.4 Sustain + Burst
|
||||
좋은 보스는 둘 다 필요하다.
|
||||
- **Sustain Pressure**: 쉬지 않는 작은 압박
|
||||
- **Burst Check**: 큰 패턴, 순간 DPS, 즉사급 실패 위험
|
||||
|
||||
### 2.5 Revive is Risk
|
||||
부활은 무료 복구가 아니라 위험 행동이어야 한다.
|
||||
|
||||
### 2.6 Soft Enrage
|
||||
명시적 시간 제한 대신,
|
||||
- 공격 속도 증가
|
||||
- 피해 증가
|
||||
- 패턴 밀도 증가
|
||||
같은 점진 강화로 무한 지속을 방지한다.
|
||||
|
||||
---
|
||||
|
||||
## 3. Signature-first 설계 순서
|
||||
|
||||
보스 설계는 아래 순서로 진행한다.
|
||||
|
||||
1. **시그니처 패턴**
|
||||
2. 역할 압박 정의
|
||||
3. 페이즈 구조
|
||||
4. 지속 압박과 순간 시험
|
||||
5. 부활 상호작용
|
||||
6. 시간 경과 강화
|
||||
7. 개체 구성 (단일 / adds / 다중)
|
||||
8. 난이도 목표
|
||||
|
||||
---
|
||||
|
||||
## 4. 설계 템플릿
|
||||
|
||||
```md
|
||||
# BOSS DESIGN DOCUMENT
|
||||
|
||||
Boss Name:
|
||||
Boss ID:
|
||||
|
||||
Stage:
|
||||
Estimated Duration:
|
||||
Entity Type:
|
||||
- Single
|
||||
- Boss + Adds
|
||||
- Multi Boss
|
||||
- Swarm
|
||||
|
||||
---
|
||||
|
||||
# 1. Signature Pattern
|
||||
|
||||
## Signature Name:
|
||||
|
||||
## Core Concept
|
||||
이 패턴이 만드는 전투 상황.
|
||||
|
||||
## Player Problem
|
||||
플레이어가 해결해야 하는 문제.
|
||||
|
||||
## Expected Solution
|
||||
정답. 역할별 책임을 가능하면 명시.
|
||||
|
||||
## Failure Result
|
||||
틀렸을 때 발생하는 결과.
|
||||
|
||||
## Why This Pattern Matters
|
||||
이 패턴이 왜 이 보스의 정체성인지.
|
||||
|
||||
---
|
||||
|
||||
# 2. Role Pressure Design
|
||||
|
||||
## Tank Pressure
|
||||
지속적으로 요구되는 행동:
|
||||
|
||||
Tank Failure Result:
|
||||
|
||||
## Support Pressure
|
||||
지속적으로 요구되는 행동:
|
||||
|
||||
Support Failure Result:
|
||||
|
||||
## DPS Pressure
|
||||
지속적으로 요구되는 행동:
|
||||
|
||||
DPS Failure Result:
|
||||
|
||||
---
|
||||
|
||||
# 3. Phase Structure
|
||||
|
||||
## Phase 1 — Learn
|
||||
목표:
|
||||
새 요소:
|
||||
|
||||
## Phase 2 — Pressure
|
||||
목표:
|
||||
새 요소:
|
||||
|
||||
## Phase 3 — Twist
|
||||
목표:
|
||||
새 요소:
|
||||
|
||||
## Phase 4 — Final
|
||||
목표:
|
||||
새 요소:
|
||||
|
||||
---
|
||||
|
||||
# 4. Sustained Pressure Design
|
||||
|
||||
## Passive Damage Sources
|
||||
|
||||
## Movement Pressure
|
||||
|
||||
---
|
||||
|
||||
# 5. Burst Pressure Design
|
||||
|
||||
## Major Mechanics
|
||||
|
||||
## DPS Check
|
||||
Yes / No
|
||||
|
||||
---
|
||||
|
||||
# 6. Revive Interaction Design
|
||||
|
||||
Revive Trigger Reaction:
|
||||
|
||||
Revive Risk Level:
|
||||
Low / Medium / High
|
||||
|
||||
---
|
||||
|
||||
# 7. Time Escalation (Soft Enrage)
|
||||
|
||||
Escalation Type:
|
||||
|
||||
Escalation Interval:
|
||||
|
||||
---
|
||||
|
||||
# 8. Multi-Entity Behavior
|
||||
|
||||
Entity Count:
|
||||
|
||||
Add Role:
|
||||
|
||||
---
|
||||
|
||||
# 9. Expected Party Role Demand
|
||||
|
||||
Required Tank Pressure:
|
||||
|
||||
Required DPS Pressure:
|
||||
|
||||
Required Support Pressure:
|
||||
|
||||
---
|
||||
|
||||
# 10. Difficulty Intent
|
||||
|
||||
Expected Attempts:
|
||||
|
||||
Primary Failure Cause:
|
||||
|
||||
Secondary Failure Cause:
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 역할 압박을 만드는 방법
|
||||
|
||||
### 탱커 압박
|
||||
- 최고 위협 대상에게 지속 고정 공격
|
||||
- 보스 방향 유지 실패 시 파티 피해 증가
|
||||
- 이동 패턴 후 재고정 요구
|
||||
- 위협 유지 실패 시 후열 붕괴
|
||||
|
||||
### 지원 압박
|
||||
- 탱커 유지력 소모
|
||||
- 광역 피해 누적
|
||||
- 상태이상 해제 필요
|
||||
- 부활 시전 자체가 위험 유발
|
||||
|
||||
### 딜러 압박
|
||||
- 탱커 외 대상에게도 위협이 가해짐
|
||||
- 순간 DPS 요구 기믹
|
||||
- 적절한 시간 내 처치 요구
|
||||
- 지속 화력 부족 시 전투 피로도 급증
|
||||
|
||||
---
|
||||
|
||||
## 6. 긴 보스전 체크리스트
|
||||
|
||||
약 10분 전투라면 최소한 아래를 점검한다.
|
||||
|
||||
- 3개 이상 페이즈가 있는가
|
||||
- 90~120초마다 새 메커닉이 들어오는가
|
||||
- 시그니처 패턴이 후반부에 변형되는가
|
||||
- 지속 압박과 순간 시험이 모두 있는가
|
||||
- 탱커가 계속 필요한가
|
||||
- 지원이 계속 필요한가
|
||||
- 딜러가 지속/순간 DPS를 둘 다 요구받는가
|
||||
- 부활이 안전하지 않은가
|
||||
- 시간 경과 강화가 있는가
|
||||
- adds나 다중 개체가 있다면 명확한 역할이 있는가
|
||||
|
||||
---
|
||||
|
||||
## 7. 나쁜 보스의 전형적 징후
|
||||
|
||||
- 모든 패턴을 같은 타이밍 Dodge로 해결 가능
|
||||
- 탱커가 잠깐만 필요하고 대부분 무시 가능
|
||||
- 지원이 없어도 큰 차이가 없음
|
||||
- 딜러는 평딜만 하면 되고 순간 화력 체크가 없음
|
||||
- 부활이 사실상 무료
|
||||
- 시간만 끌면 결국 깨짐
|
||||
- adds가 있지만 의미가 없음
|
||||
- 시그니처 패턴이 모호해서 실패 이유가 불명확
|
||||
|
||||
---
|
||||
|
||||
## 8. 추천 실무 루프
|
||||
|
||||
1. 시그니처 패턴 1개를 먼저 적는다
|
||||
2. 그 패턴이 어떤 역할을 필요하게 만드는지 적는다
|
||||
3. 그 패턴이 어느 페이즈에서 어떻게 변형되는지 적는다
|
||||
4. 작은 패턴으로 유지력 압박을 설계한다
|
||||
5. 부활/시간경과 리스크를 넣는다
|
||||
6. YAML로 변환한다
|
||||
7. Codex로 검증한다
|
||||
8. 보고서를 보고 문서를 다시 고친다
|
||||
|
||||
18
Codex-Tools/AGENTS.md
Normal file
18
Codex-Tools/AGENTS.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Codex Automation Agent Rules
|
||||
|
||||
Purpose:
|
||||
Run the design-to-data pipeline for boss encounters.
|
||||
|
||||
Pipeline:
|
||||
1. Read Markdown boss design doc from the design vault.
|
||||
2. Convert it to boss YAML.
|
||||
3. Validate the YAML using boss design rules.
|
||||
4. Write a validation report next to the design doc.
|
||||
5. Write runtime YAML into the game project.
|
||||
|
||||
Rules:
|
||||
- Markdown is the source of truth.
|
||||
- YAML is derived data.
|
||||
- Validation should never modify the source Markdown.
|
||||
- Do not invent mechanics.
|
||||
- If a field is missing, record that as uncertainty or an issue.
|
||||
22
Codex-Tools/README.md
Normal file
22
Codex-Tools/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Codex Boss Pipeline Tools
|
||||
|
||||
This directory contains helper scripts for a Markdown -> YAML -> Validation workflow.
|
||||
|
||||
## Files
|
||||
- `skills/generate_boss_yaml.py`: generate runtime YAML from a Markdown boss design document.
|
||||
- `skills/validate_boss_yaml.py`: validate a generated boss YAML and emit a Markdown report.
|
||||
- `skills/run_boss_pipeline.py`: run both steps together.
|
||||
- `config/paths.example.yaml`: copy and edit into `paths.yaml` for your environment.
|
||||
|
||||
## Recommended flow
|
||||
1. Write `/mnt/smb/Obsidian Vault/Colosseum/보스/<boss>/<boss> 기획.md`
|
||||
2. Run `run_boss_pipeline.py`
|
||||
3. Review `<boss>_validation.md`
|
||||
4. Edit the Markdown source, then rerun.
|
||||
|
||||
## Example
|
||||
```bash
|
||||
python skills/run_boss_pipeline.py \
|
||||
--md "/mnt/smb/Obsidian Vault/Colosseum/보스/드로그/드로그 기획.md" \
|
||||
--config config/paths.yaml
|
||||
```
|
||||
8
Codex-Tools/config/paths.yaml
Normal file
8
Codex-Tools/config/paths.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
# Fixed configuration keys for the boss pipeline.
|
||||
# These names are now the canonical version used by all Python skills.
|
||||
|
||||
design_root: "/mnt/smb/Obsidian Vault/Colosseum"
|
||||
game_project_root: "/home/dal4segno/Colosseum"
|
||||
schema_path: "/home/dal4segno/Colosseum/tools/schemas/boss_schema_v1.yaml"
|
||||
runtime_boss_output_dir: "/home/dal4segno/Colosseum/content/bosses"
|
||||
validation_report_suffix: "_validation.md"
|
||||
11
Codex-Tools/prompts/generate_yaml_prompt.md
Normal file
11
Codex-Tools/prompts/generate_yaml_prompt.md
Normal file
@@ -0,0 +1,11 @@
|
||||
Read the provided Markdown boss design document and convert it into YAML using the boss schema.
|
||||
|
||||
Rules:
|
||||
- Do not invent mechanics.
|
||||
- Preserve signature mechanic intent.
|
||||
- Preserve tank / support / dps role pressure intent.
|
||||
- If a field is missing, leave it empty or null rather than fabricating.
|
||||
- Prefer machine-readable snake_case values.
|
||||
- Preserve uncertainty in `source_notes` or `notes` fields.
|
||||
|
||||
Return only valid YAML.
|
||||
8
Codex-Tools/prompts/validate_prompt.md
Normal file
8
Codex-Tools/prompts/validate_prompt.md
Normal file
@@ -0,0 +1,8 @@
|
||||
Validate the provided boss YAML against AGENTS.md and the project design philosophy.
|
||||
|
||||
Return a Boss Validation Report with:
|
||||
- PASS / WARNING / FAIL
|
||||
- scores
|
||||
- detected issues
|
||||
- suggested fixes
|
||||
- explicit note of any missing source information
|
||||
82
Codex-Tools/schemas/boss_schema_v1.yaml
Normal file
82
Codex-Tools/schemas/boss_schema_v1.yaml
Normal file
@@ -0,0 +1,82 @@
|
||||
# Boss YAML Schema v1
|
||||
# Authoring target for Codex-readable encounter data.
|
||||
|
||||
boss:
|
||||
metadata:
|
||||
boss_id: "example_boss_id"
|
||||
boss_name: "Example Boss"
|
||||
stage: 1
|
||||
expected_duration_sec: 600
|
||||
entity_type: "single" # single | boss_plus_adds | multi_boss | swarm
|
||||
|
||||
signature:
|
||||
name: "Example Signature"
|
||||
concept: >
|
||||
One-sentence summary of the signature encounter concept.
|
||||
player_problem: >
|
||||
What players must solve.
|
||||
expected_solution:
|
||||
tank: []
|
||||
support: []
|
||||
dps: []
|
||||
failure_result:
|
||||
type: "major_damage" # major_damage | wipe | mechanic_repeat | positioning_loss | other
|
||||
description: "What happens on failure."
|
||||
|
||||
roles:
|
||||
tank:
|
||||
pressure_sources: []
|
||||
failure_results: []
|
||||
required_pressure: 1.0
|
||||
support:
|
||||
pressure_sources: []
|
||||
failure_results: []
|
||||
required_pressure: 1.0
|
||||
dps:
|
||||
pressure_sources: []
|
||||
failure_results: []
|
||||
required_pressure: 1.0
|
||||
|
||||
phases:
|
||||
- phase_id: 1
|
||||
name: "Learn"
|
||||
duration_estimate_sec: 120
|
||||
new_mechanics: []
|
||||
role_pressure_multiplier:
|
||||
tank: 1.0
|
||||
support: 1.0
|
||||
dps: 1.0
|
||||
|
||||
pressure:
|
||||
sustained:
|
||||
sources: []
|
||||
burst:
|
||||
mechanics: []
|
||||
dodge_types_required: []
|
||||
|
||||
revive:
|
||||
trigger_response:
|
||||
enabled: true
|
||||
reactions: []
|
||||
revive_risk_level: "medium" # low | medium | high
|
||||
|
||||
escalation:
|
||||
enabled: true
|
||||
escalation_interval_sec: 60
|
||||
escalation_types: []
|
||||
|
||||
entities:
|
||||
total_entities: 1
|
||||
composition:
|
||||
main_boss: 1
|
||||
add_behavior:
|
||||
role: "none"
|
||||
respawn: false
|
||||
|
||||
difficulty:
|
||||
expected_attempts: 3
|
||||
primary_failure_causes: []
|
||||
secondary_failure_causes: []
|
||||
design_intent: >
|
||||
Short note on what the fight is trying to test.
|
||||
|
||||
371
Codex-Tools/skills/generate_boss_yaml.py
Executable file
371
Codex-Tools/skills/generate_boss_yaml.py
Executable file
@@ -0,0 +1,371 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate boss YAML from a Markdown design document.
|
||||
|
||||
Canonical assumptions:
|
||||
- Markdown is the source of truth.
|
||||
- YAML is derived runtime data.
|
||||
- Missing data should remain empty rather than invented.
|
||||
|
||||
This parser is intentionally conservative and template-friendly.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import yaml
|
||||
|
||||
HEADING_RE = re.compile(r"^(#{1,6})\s+(.*?)\s*$", re.MULTILINE)
|
||||
KV_LINE_RE = re.compile(r"^([A-Za-z0-9_가-힣 /()\-]+):\s*(.*?)\s*$")
|
||||
|
||||
SIGNATURE_SECTION_ALIASES = ["signature_pattern", "시그니처_패턴"]
|
||||
CORE_CONCEPT_ALIASES = ["core_concept", "concept", "컨셉", "핵심_개념", "핵심_컨셉"]
|
||||
PLAYER_PROBLEM_ALIASES = ["player_problem", "문제", "플레이어_문제", "플레이어_과제"]
|
||||
FAILURE_RESULT_ALIASES = ["failure_result", "실패_결과"]
|
||||
EXPECTED_SOLUTION_ALIASES = ["expected_solution", "해결_방법", "정답", "기대_해법", "예상_해법"]
|
||||
|
||||
ROLE_ALIASES = {
|
||||
"tank": ["tank", "탱커"],
|
||||
"support": ["support", "지원", "서포트"],
|
||||
"dps": ["dps", "딜러"],
|
||||
}
|
||||
|
||||
SUSTAINED_PRESSURE_SECTION_ALIASES = ["sustained_pressure_design", "지속_압박_설계"]
|
||||
BURST_PRESSURE_SECTION_ALIASES = ["burst_pressure_design", "순간_압박_설계", "고점_압박_설계"]
|
||||
TIME_ESCALATION_SECTION_ALIASES = ["time_escalation", "시간_압박", "시간_경과_강화"]
|
||||
REVIVE_SECTION_ALIASES = ["revive_interaction_design", "부활_상호작용", "부활_상호작용_설계"]
|
||||
DIFFICULTY_SECTION_ALIASES = ["difficulty_intent", "난이도_의도"]
|
||||
EXPECTED_ATTEMPTS_LABELS = ["Expected Attempts", "예상 시도 횟수"]
|
||||
PRIMARY_FAILURE_CAUSE_LABELS = ["Primary Failure Cause", "주 실패 원인"]
|
||||
SECONDARY_FAILURE_CAUSE_LABELS = ["Secondary Failure Cause", "보조 실패 원인"]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Section:
|
||||
level: int
|
||||
title: str
|
||||
content: str
|
||||
|
||||
|
||||
def slugify(text: str) -> str:
|
||||
text = text.strip().lower()
|
||||
text = re.sub(r"\s+", "_", text)
|
||||
text = re.sub(r"[^a-z0-9_가-힣]", "", text)
|
||||
return text or "boss"
|
||||
|
||||
|
||||
def normalize_title(title: str) -> str:
|
||||
t = title.strip().lower().replace("—", "-")
|
||||
t = re.sub(r"[^a-z0-9가-힣]+", "_", t)
|
||||
return t.strip("_")
|
||||
|
||||
|
||||
def maybe_int(text: Optional[str]) -> Optional[int]:
|
||||
if not text:
|
||||
return None
|
||||
m = re.search(r"(\d+)", text)
|
||||
return int(m.group(1)) if m else None
|
||||
|
||||
|
||||
def parse_sections(markdown: str) -> List[Section]:
|
||||
matches = list(HEADING_RE.finditer(markdown))
|
||||
if not matches:
|
||||
return [Section(level=1, title="document", content=markdown.strip())]
|
||||
|
||||
sections: List[Section] = []
|
||||
for i, match in enumerate(matches):
|
||||
start = match.end()
|
||||
end = matches[i + 1].start() if i + 1 < len(matches) else len(markdown)
|
||||
sections.append(
|
||||
Section(
|
||||
level=len(match.group(1)),
|
||||
title=match.group(2).strip(),
|
||||
content=markdown[start:end].strip(),
|
||||
)
|
||||
)
|
||||
return sections
|
||||
|
||||
|
||||
def section_map(sections: List[Section]) -> Dict[str, Section]:
|
||||
mapping: Dict[str, Section] = {}
|
||||
for section in sections:
|
||||
key = normalize_title(section.title)
|
||||
if key not in mapping:
|
||||
mapping[key] = section
|
||||
return mapping
|
||||
|
||||
|
||||
def find_line_value(markdown: str, labels: List[str]) -> Optional[str]:
|
||||
normalized = {normalize_title(label) for label in labels}
|
||||
for raw in markdown.splitlines():
|
||||
line = raw.strip().lstrip("- ")
|
||||
if not line or ":" not in line:
|
||||
continue
|
||||
left, right = line.split(":", 1)
|
||||
if normalize_title(left) in normalized:
|
||||
value = right.strip()
|
||||
return value or None
|
||||
return None
|
||||
|
||||
|
||||
def first_section(smap: Dict[str, Section], aliases: List[str]) -> Optional[Section]:
|
||||
return next((smap[alias] for alias in aliases if alias in smap), None)
|
||||
|
||||
|
||||
def first_content_value(section: Optional[Section]) -> Optional[str]:
|
||||
if not section:
|
||||
return None
|
||||
for raw in section.content.splitlines():
|
||||
line = raw.strip().lstrip("-* ").strip()
|
||||
if line:
|
||||
return line
|
||||
return None
|
||||
|
||||
|
||||
def collect_child_sections(sections: List[Section], parent_aliases: List[str]) -> List[Section]:
|
||||
normalized_aliases = set(parent_aliases)
|
||||
for index, section in enumerate(sections):
|
||||
if normalize_title(section.title) not in normalized_aliases:
|
||||
continue
|
||||
|
||||
children: List[Section] = []
|
||||
parent_level = section.level
|
||||
for child in sections[index + 1 :]:
|
||||
if child.level <= parent_level:
|
||||
break
|
||||
children.append(child)
|
||||
return children
|
||||
return []
|
||||
|
||||
|
||||
def parse_inline_list(section: Optional[Section]) -> List[str]:
|
||||
if not section:
|
||||
return []
|
||||
items: List[str] = []
|
||||
for raw in section.content.splitlines():
|
||||
line = raw.strip()
|
||||
if not line:
|
||||
continue
|
||||
if line.startswith(("-", "*")):
|
||||
items.append(slugify(line.strip("-* "))[:64])
|
||||
return items
|
||||
|
||||
|
||||
def extract_role_pressure(smap: Dict[str, Section]) -> Dict[str, dict]:
|
||||
config = {
|
||||
"tank": ["tank_pressure", "탱커_압박", "tank"],
|
||||
"support": ["support_pressure", "지원_압박", "support"],
|
||||
"dps": ["dps_pressure", "딜러_압박", "dps"],
|
||||
}
|
||||
output: Dict[str, dict] = {}
|
||||
for role_name, aliases in config.items():
|
||||
section = next((smap[a] for a in aliases if a in smap), None)
|
||||
output[role_name] = {
|
||||
"required_pressure": None,
|
||||
"pressure_sources": parse_inline_list(section),
|
||||
"failure_results": [],
|
||||
"notes": section.content.strip() if section else None,
|
||||
}
|
||||
return output
|
||||
|
||||
|
||||
def extract_signature(markdown: str, sections: List[Section], smap: Dict[str, Section]) -> dict:
|
||||
signature_section = first_section(smap, SIGNATURE_SECTION_ALIASES)
|
||||
signature_name = find_line_value(markdown, ["Signature Name", "Signature Pattern", "시그니처 패턴", "시그니처 이름"])
|
||||
core_concept = None
|
||||
player_problem = None
|
||||
failure_description = None
|
||||
expected_solution: Dict[str, List[str]] = {}
|
||||
|
||||
if signature_section:
|
||||
sub_sections = collect_child_sections(sections, SIGNATURE_SECTION_ALIASES)
|
||||
sub_map = section_map(sub_sections)
|
||||
signature_name = signature_name or first_content_value(
|
||||
first_section(sub_map, ["signature_name", "시그니처_이름"])
|
||||
)
|
||||
core_concept = first_section(sub_map, CORE_CONCEPT_ALIASES)
|
||||
player_problem = first_section(sub_map, PLAYER_PROBLEM_ALIASES)
|
||||
failure_result = first_section(sub_map, FAILURE_RESULT_ALIASES)
|
||||
expected = first_section(sub_map, EXPECTED_SOLUTION_ALIASES)
|
||||
|
||||
core_concept = core_concept.content.strip() if core_concept else None
|
||||
player_problem = player_problem.content.strip() if player_problem else None
|
||||
failure_description = failure_result.content.strip() if failure_result else None
|
||||
|
||||
if expected:
|
||||
current_role: Optional[str] = None
|
||||
role_items: Dict[str, List[str]] = {"tank": [], "support": [], "dps": []}
|
||||
for raw in expected.content.splitlines():
|
||||
line = raw.strip()
|
||||
if not line:
|
||||
continue
|
||||
role_key = normalize_title(line.rstrip(":"))
|
||||
for canonical_role, aliases in ROLE_ALIASES.items():
|
||||
if role_key in {normalize_title(alias) for alias in aliases}:
|
||||
current_role = canonical_role
|
||||
break
|
||||
else:
|
||||
current_role = current_role
|
||||
if current_role and role_key in {
|
||||
normalize_title(alias)
|
||||
for aliases in ROLE_ALIASES.values()
|
||||
for alias in aliases
|
||||
}:
|
||||
continue
|
||||
if current_role and line.startswith(("-", "*")):
|
||||
role_items[current_role].append(line.strip("-* "))
|
||||
elif current_role:
|
||||
role_items[current_role].append(line)
|
||||
expected_solution = {k: v for k, v in role_items.items() if v}
|
||||
|
||||
return {
|
||||
"name": signature_name,
|
||||
"concept": core_concept,
|
||||
"player_problem": player_problem,
|
||||
"expected_solution": expected_solution,
|
||||
"failure_result": {
|
||||
"type": None,
|
||||
"description": failure_description,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def extract_phases(sections: List[Section]) -> List[dict]:
|
||||
phases: List[dict] = []
|
||||
for section in sections:
|
||||
normalized = normalize_title(section.title)
|
||||
if normalized.startswith("phase") or normalized.startswith("페이즈"):
|
||||
phase_id = maybe_int(section.title) or len(phases) + 1
|
||||
mechanics = parse_inline_list(Section(section.level, section.title, section.content))
|
||||
phases.append(
|
||||
{
|
||||
"phase_id": phase_id,
|
||||
"name": section.title,
|
||||
"duration_estimate_sec": None,
|
||||
"new_mechanics": mechanics,
|
||||
"role_pressure_multiplier": {"tank": None, "support": None, "dps": None},
|
||||
"notes": section.content.strip() or None,
|
||||
}
|
||||
)
|
||||
return phases
|
||||
|
||||
|
||||
def extract_escalation(markdown: str, smap: Dict[str, Section]) -> dict:
|
||||
section = first_section(smap, TIME_ESCALATION_SECTION_ALIASES)
|
||||
interval = maybe_int(find_line_value(markdown, ["Escalation Interval Sec", "Escalation Interval", "강화 간격", "시간 압박 간격"]))
|
||||
types = parse_inline_list(section)
|
||||
return {
|
||||
"enabled": bool(section),
|
||||
"escalation_interval_sec": interval,
|
||||
"escalation_types": types,
|
||||
"notes": section.content.strip() if section else None,
|
||||
}
|
||||
|
||||
|
||||
def extract_revive(markdown: str, smap: Dict[str, Section]) -> dict:
|
||||
section = first_section(smap, REVIVE_SECTION_ALIASES)
|
||||
level = find_line_value(markdown, ["Revive Risk Level", "부활 위험도"])
|
||||
return {
|
||||
"trigger_response": {
|
||||
"enabled": bool(section),
|
||||
"reactions": parse_inline_list(section),
|
||||
},
|
||||
"revive_risk_level": level,
|
||||
"notes": section.content.strip() if section else None,
|
||||
}
|
||||
|
||||
|
||||
def generate_boss_yaml(markdown: str, md_path: str, boss_id_override: Optional[str] = None) -> dict:
|
||||
sections = parse_sections(markdown)
|
||||
smap = section_map(sections)
|
||||
|
||||
boss_name = find_line_value(markdown, ["Boss Name", "보스 이름"]) or os.path.splitext(os.path.basename(md_path))[0]
|
||||
boss_id = boss_id_override or find_line_value(markdown, ["Boss ID", "boss_id", "보스 ID", "보스 아이디"]) or slugify(boss_name)
|
||||
stage = maybe_int(find_line_value(markdown, ["Stage", "스테이지"]))
|
||||
duration_sec = maybe_int(find_line_value(markdown, ["Estimated Duration Sec", "Estimated Duration", "전투 시간"]))
|
||||
entity_type = find_line_value(markdown, ["Entity Type", "개체 타입"])
|
||||
|
||||
sustained_section = first_section(smap, SUSTAINED_PRESSURE_SECTION_ALIASES)
|
||||
burst_section = first_section(smap, BURST_PRESSURE_SECTION_ALIASES)
|
||||
difficulty_section = first_section(smap, DIFFICULTY_SECTION_ALIASES)
|
||||
|
||||
data = {
|
||||
"boss": {
|
||||
"metadata": {
|
||||
"boss_id": boss_id,
|
||||
"boss_name": boss_name,
|
||||
"stage": stage,
|
||||
"expected_duration_sec": duration_sec,
|
||||
"entity_type": entity_type,
|
||||
"source_document": md_path,
|
||||
},
|
||||
"signature": extract_signature(markdown, sections, smap),
|
||||
"roles": extract_role_pressure(smap),
|
||||
"phases": extract_phases(sections),
|
||||
"pressure": {
|
||||
"sustained": {
|
||||
"sources": parse_inline_list(sustained_section),
|
||||
"notes": sustained_section.content.strip() if sustained_section else None,
|
||||
},
|
||||
"burst": {
|
||||
"mechanics": parse_inline_list(burst_section),
|
||||
"notes": burst_section.content.strip() if burst_section else None,
|
||||
},
|
||||
"dodge_types_required": [],
|
||||
},
|
||||
"revive": extract_revive(markdown, smap),
|
||||
"escalation": extract_escalation(markdown, smap),
|
||||
"entities": {
|
||||
"total_entities": None,
|
||||
"composition": {},
|
||||
"add_behavior": {"role": None, "respawn": None},
|
||||
},
|
||||
"difficulty": {
|
||||
"expected_attempts": maybe_int(find_line_value(markdown, EXPECTED_ATTEMPTS_LABELS)),
|
||||
"primary_failure_causes": [
|
||||
value
|
||||
for value in [find_line_value(markdown, PRIMARY_FAILURE_CAUSE_LABELS)]
|
||||
if value
|
||||
],
|
||||
"secondary_failure_causes": [
|
||||
value
|
||||
for value in [find_line_value(markdown, SECONDARY_FAILURE_CAUSE_LABELS)]
|
||||
if value
|
||||
],
|
||||
"notes": difficulty_section.content.strip() if difficulty_section else None,
|
||||
},
|
||||
"source_notes": {
|
||||
"unparsed_sections": [
|
||||
{"title": section.title, "content": section.content[:500]}
|
||||
for section in sections
|
||||
if section.level <= 3
|
||||
]
|
||||
},
|
||||
}
|
||||
}
|
||||
return data
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Generate boss YAML from Markdown")
|
||||
parser.add_argument("--md", required=True, help="Path to boss markdown design document")
|
||||
parser.add_argument("--output", required=True, help="Path to output YAML")
|
||||
parser.add_argument("--boss-id", default=None, help="Optional boss_id override")
|
||||
args = parser.parse_args()
|
||||
|
||||
with open(args.md, "r", encoding="utf-8") as file:
|
||||
markdown = file.read()
|
||||
|
||||
data = generate_boss_yaml(markdown, args.md, boss_id_override=args.boss_id)
|
||||
os.makedirs(os.path.dirname(args.output), exist_ok=True)
|
||||
with open(args.output, "w", encoding="utf-8") as file:
|
||||
yaml.safe_dump(data, file, allow_unicode=True, sort_keys=False, width=100)
|
||||
|
||||
print(args.output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
91
Codex-Tools/skills/run_boss_pipeline.py
Executable file
91
Codex-Tools/skills/run_boss_pipeline.py
Executable file
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run the full Markdown -> YAML -> Validation pipeline.
|
||||
|
||||
Canonical config keys used everywhere:
|
||||
- design_root
|
||||
- game_project_root
|
||||
- schema_path
|
||||
- runtime_boss_output_dir
|
||||
- validation_report_suffix
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
import yaml
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
|
||||
REQUIRED_CONFIG_KEYS = [
|
||||
"design_root",
|
||||
"game_project_root",
|
||||
"schema_path",
|
||||
"runtime_boss_output_dir",
|
||||
"validation_report_suffix",
|
||||
]
|
||||
|
||||
|
||||
def load_config(path: str) -> Dict[str, Any]:
|
||||
with open(path, "r", encoding="utf-8") as file:
|
||||
data = yaml.safe_load(file) or {}
|
||||
missing = [key for key in REQUIRED_CONFIG_KEYS if key not in data]
|
||||
if missing:
|
||||
raise SystemExit(f"Missing config keys in {path}: {', '.join(missing)}")
|
||||
return data
|
||||
|
||||
|
||||
def infer_boss_id(md_path: str) -> str:
|
||||
stem = Path(md_path).stem
|
||||
stem = stem.replace(" 기획", "").replace("_기획", "")
|
||||
stem = stem.strip()
|
||||
return stem.lower().replace(" ", "_")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Run boss pipeline")
|
||||
parser.add_argument("--md", required=True, help="Path to boss Markdown document")
|
||||
parser.add_argument("--config", required=True, help="Path to paths.yaml config")
|
||||
parser.add_argument("--boss-id", default=None, help="Optional runtime boss_id / yaml filename override")
|
||||
args = parser.parse_args()
|
||||
|
||||
config = load_config(args.config)
|
||||
md_path = os.path.abspath(args.md)
|
||||
if not os.path.exists(md_path):
|
||||
raise SystemExit(f"Markdown file not found: {md_path}")
|
||||
|
||||
boss_id = args.boss_id or infer_boss_id(md_path)
|
||||
yaml_output = os.path.join(config["runtime_boss_output_dir"], f"{boss_id}.yaml")
|
||||
report_output = os.path.join(
|
||||
os.path.dirname(md_path),
|
||||
f"{boss_id}{config['validation_report_suffix']}",
|
||||
)
|
||||
|
||||
schema_path = config["schema_path"]
|
||||
if not os.path.exists(schema_path):
|
||||
print(f"Warning: schema_path does not exist yet: {schema_path}")
|
||||
|
||||
gen_script = str(HERE / "generate_boss_yaml.py")
|
||||
val_script = str(HERE / "validate_boss_yaml.py")
|
||||
|
||||
subprocess.run(
|
||||
[sys.executable, gen_script, "--md", md_path, "--output", yaml_output, "--boss-id", boss_id],
|
||||
check=True,
|
||||
)
|
||||
subprocess.run(
|
||||
[sys.executable, val_script, "--yaml", yaml_output, "--output", report_output],
|
||||
check=True,
|
||||
)
|
||||
|
||||
print("Pipeline complete")
|
||||
print(f"Markdown: {md_path}")
|
||||
print(f"YAML: {yaml_output}")
|
||||
print(f"Report: {report_output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
184
Codex-Tools/skills/validate_boss_yaml.py
Executable file
184
Codex-Tools/skills/validate_boss_yaml.py
Executable file
@@ -0,0 +1,184 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate boss YAML using the project's multiplayer boss rules."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from typing import Any, Dict, List, Tuple
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
def load_yaml(path: str) -> Dict[str, Any]:
|
||||
with open(path, "r", encoding="utf-8") as file:
|
||||
return yaml.safe_load(file) or {}
|
||||
|
||||
|
||||
def score_to_label(issues: List[Tuple[str, str]]) -> str:
|
||||
severities = [severity for severity, _ in issues]
|
||||
if "ERROR" in severities:
|
||||
return "FAIL"
|
||||
if "WARNING" in severities:
|
||||
return "WARNING"
|
||||
return "PASS"
|
||||
|
||||
|
||||
def validate(data: Dict[str, Any]) -> Tuple[Dict[str, int], List[Tuple[str, str]], List[str]]:
|
||||
boss = data.get("boss", {})
|
||||
issues: List[Tuple[str, str]] = []
|
||||
fixes: List[str] = []
|
||||
|
||||
scores = {
|
||||
"Role Integrity Score": 100,
|
||||
"Pattern Diversity Score": 100,
|
||||
"Execution Fairness Score": 100,
|
||||
"Revive Risk Score": 100,
|
||||
"Time Pressure Score": 100,
|
||||
}
|
||||
|
||||
roles = boss.get("roles", {})
|
||||
for role_name in ["tank", "support", "dps"]:
|
||||
role = roles.get(role_name)
|
||||
if not role:
|
||||
issues.append(("ERROR", f"Missing role definition: {role_name}"))
|
||||
scores["Role Integrity Score"] -= 25
|
||||
fixes.append(f"Add `{role_name}` role pressure definition.")
|
||||
continue
|
||||
if not role.get("notes") and not role.get("pressure_sources"):
|
||||
issues.append(("WARNING", f"No pressure details defined for {role_name}"))
|
||||
scores["Role Integrity Score"] -= 12
|
||||
fixes.append(f"Describe how the boss pressures {role_name} players.")
|
||||
|
||||
signature = boss.get("signature", {})
|
||||
if not signature.get("name"):
|
||||
issues.append(("ERROR", "Missing signature mechanic name"))
|
||||
scores["Pattern Diversity Score"] -= 30
|
||||
fixes.append("Add a named signature mechanic.")
|
||||
if not signature.get("player_problem"):
|
||||
issues.append(("WARNING", "Signature mechanic has no clear player problem"))
|
||||
scores["Pattern Diversity Score"] -= 15
|
||||
fixes.append("Describe the player problem the signature mechanic creates.")
|
||||
if not signature.get("expected_solution"):
|
||||
issues.append(("WARNING", "Signature mechanic has no clear expected solution"))
|
||||
scores["Pattern Diversity Score"] -= 15
|
||||
fixes.append("Describe the expected solution for the signature mechanic.")
|
||||
if not (signature.get("failure_result") or {}).get("description"):
|
||||
issues.append(("WARNING", "Signature mechanic has no failure result"))
|
||||
scores["Execution Fairness Score"] -= 12
|
||||
fixes.append("Define what happens when the signature mechanic is failed.")
|
||||
|
||||
phases = boss.get("phases") or []
|
||||
if len(phases) < 3:
|
||||
issues.append(("ERROR", f"Insufficient phases: {len(phases)} found, expected at least 3"))
|
||||
scores["Pattern Diversity Score"] -= 30
|
||||
fixes.append("Add at least 3 phases, ideally 4 for a ~10 minute fight.")
|
||||
elif len(phases) < 4:
|
||||
issues.append(("WARNING", "Only 3 phases defined; 4 is recommended for ~10 minute encounters"))
|
||||
scores["Pattern Diversity Score"] -= 8
|
||||
|
||||
for phase in phases:
|
||||
if not phase.get("new_mechanics") and not phase.get("notes"):
|
||||
issues.append(("WARNING", f"Phase {phase.get('phase_id')} has no new mechanics listed"))
|
||||
scores["Pattern Diversity Score"] -= 5
|
||||
fixes.append(f"List at least one new mechanic for phase {phase.get('phase_id')}.")
|
||||
|
||||
pressure = boss.get("pressure", {})
|
||||
dodge_types = pressure.get("dodge_types_required") or []
|
||||
if dodge_types and len(dodge_types) < 3:
|
||||
issues.append(("WARNING", "Fewer than 3 dodge response types defined"))
|
||||
scores["Execution Fairness Score"] -= 10
|
||||
fixes.append("List at least 3 dodge response types, such as timing, directional, and chained.")
|
||||
|
||||
revive = boss.get("revive", {})
|
||||
revive_enabled = (revive.get("trigger_response") or {}).get("enabled")
|
||||
if revive_enabled:
|
||||
if not (revive.get("trigger_response") or {}).get("reactions"):
|
||||
issues.append(("WARNING", "Revive response enabled but no reactions defined"))
|
||||
scores["Revive Risk Score"] -= 20
|
||||
fixes.append("Define at least one revive-triggered boss reaction.")
|
||||
else:
|
||||
issues.append(("WARNING", "No revive-triggered boss response defined"))
|
||||
scores["Revive Risk Score"] -= 25
|
||||
fixes.append("Add a risky revive interaction if revive exists in combat.")
|
||||
|
||||
escalation = boss.get("escalation", {})
|
||||
if not escalation.get("enabled"):
|
||||
issues.append(("ERROR", "No time escalation defined"))
|
||||
scores["Time Pressure Score"] -= 35
|
||||
fixes.append("Add soft-enrage or time-based escalation to prevent infinite sustain.")
|
||||
else:
|
||||
if not escalation.get("escalation_types"):
|
||||
issues.append(("WARNING", "Escalation enabled but escalation types are empty"))
|
||||
scores["Time Pressure Score"] -= 10
|
||||
fixes.append("Specify how the boss escalates over time.")
|
||||
if not escalation.get("escalation_interval_sec"):
|
||||
issues.append(("WARNING", "Escalation interval is not defined"))
|
||||
scores["Time Pressure Score"] -= 8
|
||||
fixes.append("Specify an escalation interval in seconds.")
|
||||
|
||||
for score_name in list(scores.keys()):
|
||||
scores[score_name] = max(0, scores[score_name])
|
||||
|
||||
return scores, issues, fixes
|
||||
|
||||
|
||||
def render_report(yaml_path: str, data: Dict[str, Any], scores: Dict[str, int], issues: List[Tuple[str, str]], fixes: List[str]) -> str:
|
||||
boss = data.get("boss", {})
|
||||
metadata = boss.get("metadata", {})
|
||||
result = score_to_label(issues)
|
||||
lines = [
|
||||
"# Boss Validation Report",
|
||||
"",
|
||||
f"Boss ID: {metadata.get('boss_id')}",
|
||||
f"Boss Name: {metadata.get('boss_name')}",
|
||||
f"Source YAML: `{yaml_path}`",
|
||||
"",
|
||||
f"Result: **{result}**",
|
||||
"",
|
||||
]
|
||||
for score_name, value in scores.items():
|
||||
lines.append(f"- {score_name}: {value}")
|
||||
lines.extend(["", "## Detected Issues"])
|
||||
if issues:
|
||||
for severity, message in issues:
|
||||
lines.append(f"- **{severity}**: {message}")
|
||||
else:
|
||||
lines.append("- None")
|
||||
lines.extend(["", "## Suggested Fixes"])
|
||||
if fixes:
|
||||
seen = set()
|
||||
for item in fixes:
|
||||
if item not in seen:
|
||||
lines.append(f"- {item}")
|
||||
seen.add(item)
|
||||
else:
|
||||
lines.append("- No changes required.")
|
||||
lines.extend([
|
||||
"",
|
||||
"## Notes",
|
||||
"- Markdown source remains the source of truth.",
|
||||
"- Regenerate YAML after editing the Markdown design document.",
|
||||
"",
|
||||
])
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Validate boss YAML")
|
||||
parser.add_argument("--yaml", required=True, help="Path to boss YAML")
|
||||
parser.add_argument("--output", required=True, help="Path to output Markdown report")
|
||||
args = parser.parse_args()
|
||||
|
||||
data = load_yaml(args.yaml)
|
||||
scores, issues, fixes = validate(data)
|
||||
report = render_report(args.yaml, data, scores, issues, fixes)
|
||||
|
||||
os.makedirs(os.path.dirname(args.output), exist_ok=True)
|
||||
with open(args.output, "w", encoding="utf-8") as file:
|
||||
file.write(report)
|
||||
|
||||
print(args.output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
79
Docs/DESIGN_PHILOSOPHY.md
Normal file
79
Docs/DESIGN_PHILOSOPHY.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Multiplayer Boss Design Philosophy
|
||||
|
||||
This project focuses on 4-player cooperative boss encounters.
|
||||
|
||||
Bosses are not just enemies.
|
||||
Bosses are cooperative execution challenges.
|
||||
|
||||
## Core Design Philosophy
|
||||
|
||||
Boss encounters must test:
|
||||
- role execution
|
||||
- positioning
|
||||
- pattern recognition
|
||||
- sustained coordination
|
||||
|
||||
Boss encounters should not primarily test:
|
||||
- raw stat checks
|
||||
- random power spikes
|
||||
- build luck
|
||||
|
||||
## Role Model
|
||||
|
||||
Roles are soft ratios.
|
||||
|
||||
Players are not locked into hard classes.
|
||||
Each player contributes a mixture of:
|
||||
- Tank ratio
|
||||
- DPS ratio
|
||||
- Support ratio
|
||||
|
||||
Bosses should require total role pressure fulfillment, not specific class presence.
|
||||
|
||||
## Combat Identity
|
||||
|
||||
Combat is:
|
||||
- non-target action
|
||||
- directional dodge-based
|
||||
- positioning-focused
|
||||
|
||||
Survival should depend more on execution than on numerical advantage.
|
||||
|
||||
## Signature Pattern Philosophy
|
||||
|
||||
Each boss must contain at least one defining signature mechanic.
|
||||
That mechanic must:
|
||||
- create a clear player problem
|
||||
- have a clear expected solution
|
||||
- produce a clear failure result
|
||||
|
||||
Ambiguous solutions are discouraged.
|
||||
|
||||
## Revive Philosophy
|
||||
|
||||
Revive is allowed, but revive must be risky.
|
||||
Bosses should react to revive attempts.
|
||||
Revive should increase encounter tension, not reduce it.
|
||||
|
||||
## Sustain Philosophy
|
||||
|
||||
There is no hard time limit by default.
|
||||
Instead, bosses should escalate over time.
|
||||
Encounters must become harder if players fail to execute properly.
|
||||
|
||||
## Build Philosophy
|
||||
|
||||
Build strength should support execution.
|
||||
It should not replace execution.
|
||||
Players should adapt builds, not brute-force encounters.
|
||||
|
||||
## Encounter Identity
|
||||
|
||||
Boss equals encounter.
|
||||
|
||||
Bosses may include:
|
||||
- multiple entities
|
||||
- supporting units
|
||||
- positional hazards
|
||||
|
||||
Adds must serve meaningful gameplay roles, never filler.
|
||||
Reference in New Issue
Block a user