BurningTimesAi/공유/기획/FirstRising/리뉴얼/tools/build_renewal_v2.py

377 lines
37 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# FirstRising 리뉴얼 기획서 v2 — 라이트 템플릿(PD 레퍼런스: 포트폴리오 요약 라이트) · 내용은 v1과 같음
import os, csv, sys
sys.path.insert(0, os.path.dirname(__file__))
from deckkit_light import *
BASE = r"E:\BurningTimes\공유\기획\FirstRising"
FR = lambda n: os.path.join(BASE, "시스템분석", "img", n)
TB = lambda n: os.path.join(BASE, "레퍼런스_TBH", "img", n)
MK = lambda n: os.path.join(BASE, "리뉴얼", "img", n)
OUT = os.path.join(BASE, "리뉴얼", "2026-09-19_FirstRising_리뉴얼_기획서_v2.pptx")
D = Deck()
def why(sl, x, y, w, h, items, title="왜 바꾸나", size=10):
D.panel(sl, x, y, w, h, radius=0.05)
D.text(sl, x + 0.2, y + 0.14, w - 0.4, 0.26, title, size=10.5, bold=True, color=INK)
yy = y + 0.5
step = (h - 0.62) / max(len(items), 1)
for i, t in enumerate(items, 1):
D.num(sl, x + 0.2, yy + 0.02, i, d=0.22, size=8)
D.text(sl, x + 0.52, yy - 0.02, w - 0.72, step, t, size=size, color=BODY)
yy += step
def asis_tobe(sl, x, y, w, h, asis, tobe, split=0.42):
aw = w * split - 0.15; tw = w - aw - 0.3
D.panel(sl, x, y, aw, h, radius=0.05)
D.pill(sl, x + 0.18, y + 0.16, "지금", fill=DARK, w=0.6)
D.bullets(sl, x + 0.18, y + 0.52, aw - 0.3, h - 0.6, asis, size=10, color=SUB, gap=3)
D.arrow(sl, x + aw + 0.04, y + h / 2, x + aw + 0.26, y + h / 2)
tx = x + aw + 0.3
D.rect(sl, tx, y, tw, h, fill=WHITE, line=RULE, shape=MSO_SHAPE.ROUNDED_RECTANGLE, radius=0.05)
D.pill(sl, tx + 0.18, y + 0.16, "바뀐 뒤", fill=RED, w=0.75)
D.bullets(sl, tx + 0.18, y + 0.52, tw - 0.3, h - 0.6, tobe, size=10.5, color=INK, gap=3)
# 01 표지 ===============================================================================
D.page += 1
sl = D.prs.slides.add_slide(D.blank)
D.rect(sl, 0, 0, 0.19, 7.5, fill=RED)
D.rect(sl, 7.22, 0, 6.11, 7.5, fill=PANEL)
D.text(sl, 7.72, 0.25, 5.27, 0.4, "근거 화면 — FirstRising 실캡처 · TBH 실행 화면 · 목업", size=10, bold=True, color=SUB, align=PP_ALIGN.CENTER)
grid = [(MK("mock_taskbar_concept.png"), "컨셉 목업", "작업표시줄 상주"), (FR("capture_01_wave_combat.jpg"), "FirstRising", "3D 물량 전투"),
(FR("capture_02_battlefence.jpg"), "FirstRising", "필드 기믹·배틀펜스"), (MK("cover_hero.jpg"), "TBH", "디아블로식 장비"),
(MK("cover_trade.jpg"), "TBH", "Steam 교역선"), (MK("cover_rune.jpg"), "TBH", "방치 효율 룬 트리")]
for i, (p, t, s) in enumerate(grid):
gx = 7.61 + (i % 2) * 2.75; gy = 0.86 + (i // 2) * 1.88
D.image(sl, p, gx, gy, 2.58, 1.46, fill=WHITE)
D.caption(sl, gx, gy + 1.52, 2.58, t, s)
D.hline(sl, 7.61, 6.5, 5.33, color=RGBColor(0xDD, 0xDD, 0xDD))
D.text(sl, 7.61, 6.6, 5.33, 0.18, "RENEWAL OVERVIEW", size=7.5, color=SUB)
D.text(sl, 7.61, 6.8, 5.33, 0.32, "3대 핵심 · 9개 변경 영역 · 승인 요청 4건", size=13, bold=True, color=INK)
D.text(sl, 0.81, 0.81, 6.1, 0.2, "FIRSTRISING RENEWAL PROPOSAL", size=9, bold=True, color=SUB)
D.text(sl, 0.81, 1.15, 6.4, 0.3, "개발 승인 요청 · FirstRising 리뉴얼 기획서", size=12.5, color=RED, face=F_MED)
D.text(sl, 0.75, 1.52, 6.45, 1.45, ["작업표시줄 위의", [("3D 다크 판타지", {"color": RED})]], size=34, color=INK, face=F_BLACK, spacing=1.05)
D.text(sl, 0.81, 3.08, 6.1, 0.85, "업무 중 작업표시줄에 상주하며 성장하는 방치형 파밍 게임으로 FirstRising을 리뉴얼합니다. TBH에서 검증된 3요소를 핵심으로 삼고, 3D 물량 전투와 타격감으로 차별화합니다.",
size=10.5, color=BODY, spacing=1.3)
rows = [("핵심 1", "작업표시줄 상주 방치형 — 켜두면 자동 전투·성장"), ("핵심 2", "디아블로식 파밍 — 등급·랜덤 옵션·고유 옵션"), ("핵심 3", "Steam 장터 거래 — 드롭에 현실 가치")]
D.hline(sl, 0.81, 4.03, 6.11)
for i, (k, v) in enumerate(rows):
yy = 4.1 + i * 0.345
D.text(sl, 0.81, yy, 1.17, 0.22, k, size=9.5, bold=True, color=RED)
D.text(sl, 1.97, yy, 4.94, 0.22, v, size=9.5, color=BODY)
D.hline(sl, 0.81, yy + 0.28, 6.11)
D.text(sl, 0.81, 5.33, 1.2, 0.4, "총괄PM", size=18, bold=True, color=INK)
D.text(sl, 2.0, 5.42, 3.0, 0.3, "BurningTimes", size=12.5, color=SUB, face=F_LIGHT)
D.text(sl, 0.81, 5.8, 6.1, 0.2, "2026-09-19 · BT22-Plan", size=9, color=SUB)
D.text(sl, 0.81, 6.02, 6.1, 0.2, "근거 자료 FirstRising 시스템 분석 기획서 · TBH 레퍼런스 분석 v2", size=9, color=RED)
D.text(sl, 0.81, 6.61, 6.25, 0.42, "01 요약 · 02 왜 바꾸나 · 03 컨셉 · 04 변경 9개 영역 · 05 로드맵·리스크 · 06 승인 요청", size=8, color=RED)
# 02 한 장 요약 =========================================================================
sl = D.slide("한 장 요약", "SUMMARY | 한 장 요약", "무엇을 제안하고, 왜 바꾸며, 무엇이 바뀌고, 무엇을 결정해야 하는지 한 장으로 정리했습니다.",
"근거 FirstRising 시스템 분석 · TBH 레퍼런스 분석 v2")
D.panel(sl, X0, Y0, XW, 4.38)
D.hline(sl, 1.9, 2.14, 9.5, color=RED, width=1.0)
cols = [("01", "제안", ["모바일 서바이벌 → Steam PC", "작업표시줄 상주형 3D 방치 파밍", "3요소를 핵심 콘텐츠로"]),
("02", "근거", ["현 구조의 게임성 약점 4가지", "TBH가 3요소 조합을 시장에서 입증", "우리 3D·물량·타격감이 차별점"]),
("03", "바뀌는 것", ["플랫폼 · 코어 루프 · 파티", "아이템 · 순환 허브 · 메타 성장", "경제 · BM · 서버/보안 (9개 영역)"]),
("04", "요청", ["Steam PC 우선 전환", "모바일 BM 폐기", "P0 프로토타입 착수 · 법률 검토"])]
cw = (XW - 0.34 - 0.48) / 4
for i, (n, t, lines) in enumerate(cols):
cx = X0 + 0.17 + i * (cw + 0.16)
D.rect(sl, cx + cw / 2 - 0.3, 1.84, 0.6, 0.6, fill=WHITE, line=RED, lw=1.25, shape=MSO_SHAPE.OVAL)
D.text(sl, cx + cw / 2 - 0.3, 1.84, 0.6, 0.6, n, size=15, bold=True, color=RED, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
D.text(sl, cx, 2.55, cw, 0.3, t, size=12, bold=True, color=INK, align=PP_ALIGN.CENTER)
D.card(sl, cx, 2.95, cw, 2.9)
D.bullets(sl, cx + 0.2, 3.12, cw - 0.4, 2.6, lines, size=10.5, color=BODY, gap=8)
D.banner(sl, "핵심", "새로 만드는 게임이 아니라, 검증된 조합 위에 우리 자산(3D 다크 판타지·ECS 물량·타격감)을 얹는 전환입니다.")
# 03 왜 바꾸나 ==========================================================================
sl = D.slide("왜 바꾸는가 — 지금 구조의 한계", "WHY | 지금의 한계", "짧게 끊어 하는 모바일 서바이벌 구조는 '오래 켜두고 쌓는' 성장 경험과 맞지 않습니다.",
"근거 FirstRising 시스템 분석 — 게임성 평가(부정 요인) · 실캡처 · MapTable · PlayerTable")
D.kpis(sl, X0, Y0, XW, 1.0, [("2종 · 스탯 동일", "플레이어블 캐릭터 — 정체성 없음", True), ("128 : 5", "맵 수 : 전투 환경 — 반복 위험", True),
("에너지 5", "챕터 1회 입장 — 5~6판이면 소진", True), ("광고·가챠·IAP", "모바일 BM에 의존", True)])
D.image(sl, FR("capture_03_main_chapter.jpg"), X0, 2.82, 5.85, 3.0, fill=WHITE)
D.caption(sl, X0, 5.87, 5.85, "FirstRising 챕터 입장", "게임 시작 버튼에 에너지 ×5")
D.image(sl, FR("capture_07_shop_gacha.jpg"), 6.78, 2.82, 5.85, 3.0, fill=WHITE)
D.caption(sl, 6.78, 5.87, 5.85, "FirstRising 상점·가챠", "광고 무료 횟수 · 10/50회 천장")
D.banner(sl, "결론", "지금 구조를 다듬는 것으로는 '켜두는 게임'이 되지 않습니다 — 구조 전환이 필요합니다.", y=6.2)
# 04 왜 이 3요소인가 ===================================================================
sl = D.slide("왜 이 3요소인가 — TBH가 검증한 조합", "EVIDENCE | TBH 검증", "TBH는 세 요소를 한 게임 안에서 결합해 Steam에서 흥행했습니다 — 수치는 클라이언트 데이터·실행 화면 기준입니다.",
"근거 TBH 레퍼런스 분석 v2(클라이언트 데이터·실행 화면) · 공개 기사(Inven Global 기사 제목)")
D.panel(sl, X0, Y0, XW, 2.95)
cards = [(MK("thumb_strip.jpg"), "① 작업표시줄 상주 방치", "룬 골드의 94%가 '방치 효율'", "드롭률 48% · 자동화 25% · 보관 12% — 전투 스탯은 6%"),
(MK("thumb_hero.jpg"), "② 디아블로식 파밍", "장비 5,954종 · 10등급", "옵션 57종 × 10티어 · 고유 옵션 89 · 클래스 맞춤 드롭 75%"),
(MK("thumb_trade.jpg"), "③ Steam 장터 거래", "거래 가능 1,093종(전설 이상)", "교역선에 실어 장터 등록 · 후가공 아이템은 거래 금지")]
cw = (XW - 0.28 - 0.28) / 3
for i, (p, h1, h2, h3) in enumerate(cards):
cx = X0 + 0.14 + i * (cw + 0.14)
D.rect(sl, cx, 1.78, cw, 2.67, fill=WHITE, shadow=True)
D.image(sl, p, cx, 1.78, cw, 1.25, border=False)
D.text(sl, cx + 0.14, 3.1, cw - 0.28, 0.28, h1, size=12, bold=True, color=RED)
D.text(sl, cx + 0.14, 3.42, cw - 0.28, 0.24, h2, size=9.5, bold=True, color=INK)
D.text(sl, cx + 0.14, 3.7, cw - 0.28, 0.65, h3, size=8.5, color=SUB, spacing=1.25)
D.kpis(sl, X0, 4.75, XW, 1.2, [("Steam 동시접속 2위", "공개 기사 제목 기준", True), ("동시접속 14만", "공개 기사 제목 기준", True),
("스테이지 189", "3액트 × 10 × 4난이도 + 역병·오염", False), ("Steam 통계 68종", "온보딩 퍼널을 계측", False)], big=15)
D.banner(sl, "결론", "'켜두면 쌓이고, 쌓인 것이 돈이 되는' 조합이 통했습니다 — 우리는 이 구조를 3D로 가져옵니다.", y=6.2)
# 05 컨셉 ===============================================================================
sl = D.slide("리뉴얼 컨셉 — 작업표시줄 위의 3D 다크 판타지", "CONCEPT | 리뉴얼 컨셉", "일하는 동안 작업표시줄 위에서 파티가 싸우고, 돌아오면 전리품을 감정·합성·거래합니다.",
"근거 FirstRising 시스템 분석(SpawnSystem 동시 2,000 · DamageSystem 판정 4종 · eGimmickType 8종) · TBH 분석 v2")
D.image(sl, MK("mock_taskbar_concept.png"), X0, Y0, 7.9, 4.44, fill=WHITE)
D.pill(sl, X0 + 0.15, Y0 + 0.15, "컨셉 목업", fill=RED, w=0.95)
D.caption(sl, X0, 6.14, 7.9, "컨셉 목업", "FirstRising 실캡처를 가로 띠로 합성한 이미지 — 실제 구현 화면이 아닙니다")
D.panel(sl, 8.8, Y0, 3.83, 4.44, radius=0.04)
D.text(sl, 8.98, Y0 + 0.14, 3.5, 0.28, "TBH와 무엇이 다른가", size=11, bold=True, color=INK)
D.table(sl, 8.98, Y0 + 0.55, 3.47, 2.3, [["", "TBH", "FirstRising"], ["그래픽", "2D 픽셀", "3D 다크 판타지"], ["물량", "소수 몬스터", "동시 2,000"],
["타격감", "기본 연출", "판정 4종 연출"], ["필드", "평면 스트립", "기믹 8종·보스전"]],
colw=[0.9, 1.1, 1.47], size=9.5, col_colors=[None, SUB, RED], col_bold=[True, False, True])
D.text(sl, 8.98, 4.62, 3.47, 1.4, [[("가져오는 것 ", {"bold": True, "color": RED}), ("상주 방치 · 디아블로식 파밍 · Steam 장터", {})], "",
[("차별화하는 것 ", {"bold": True, "color": RED}), ("'보는 재미' — 3D·물량·타격감", {})]], size=10, color=BODY)
# 06 9개 영역 ===========================================================================
sl = D.slide("무엇을 바꾸나 — 9개 영역", "CHANGE MAP | As-Is → To-Be", "영역마다 '지금 → 바뀐 뒤 → 이유'를 한 줄로 정리했습니다. 상세 근거는 다음 쪽부터입니다.",
"근거 FirstRising 시스템 분석 · TBH 레퍼런스 분석 v2")
rows = [["영역", "지금 (As-Is)", "바뀐 뒤 (To-Be)", "이유"],
["① 플랫폼", "Android 모바일", "Steam PC · 작업표시줄 상주 모드", "상주 방치는 데스크톱에서 성립"],
["② 코어 루프", "수동 이동 서바이벌 · 판당 스킬 선택", "무개입 자동 전투 · 지속 빌드", "켜둔 동안 입력이 없어야 한다"],
["③ 파티", "2종(스탯 동일)", "3인 파티 · 역할 클래스", "정체성 + 클래스 맞춤 파밍"],
["④ 아이템", "5등급 · 6부위 · 고정 옵션", "등급 확장 · 랜덤 옵션 티어 · 고유 옵션", "예측 불가 드롭 = 파밍의 재미"],
["⑤ 순환 허브", "합성·룬합성·진화 분산", "큐브형 허브 하나로 통합", "쌓이는 드롭을 한곳에서 처리"],
["⑥ 메타 성장", "진화 306 · 특별훈련(준비 중)", "방치 효율 트리(자동화 해금)", "방치할수록 방치가 편해진다"],
["⑦ 경제", "게임 내 폐쇄 경제", "Steam 장터 거래(상위 등급만)", "드롭의 현실 가치 = 강한 동기"],
["⑧ BM", "에너지 · 광고 · 가챠 · IAP", "DLC(클래스·펫·치장) + 장터 수수료", "상주·거래 구조와의 충돌 제거"],
["⑨ 서버·보안", "로컬/서버 혼용 · TLS 검증 꺼짐", "서버 권한 인벤토리 · 보안 선결", "현금 가치 = 복제·해킹 피해"]]
D.table(sl, X0, Y0, XW, 4.95, rows, colw=[1.55, 3.3, 3.85, 3.24], size=10, hsize=10, col_colors=[None, SUB, RED, BODY], col_bold=[True, False, True, False])
# 07 ① 플랫폼 ===========================================================================
sl = D.slide("① 플랫폼 — Android → Steam PC 상주 모드", "CHANGE 01 | PLATFORM", "상주 방치는 '업무 중 켜 둔 PC'에서만 성립합니다. 모바일은 화면을 켜 둘 수 없습니다.",
"근거 TBH 설정 화면·PlayerPrefs(창 위치·확대) · FirstRising TestVisual 성능 실측(2026-09-19)")
D.image(sl, TB("01_settings_general.jpg"), X0, Y0, 4.4, 4.1, fill=PANEL)
D.caption(sl, X0, 5.8, 4.4, "TBH 설정", "항상 위 · 데스크톱 시작 시 자동 실행")
asis_tobe(sl, 5.35, Y0, 7.28, 2.2, ["Android 모바일", "짧게 끊어 하는 세션", "화면을 켜 둘 수 없음"],
["Steam PC · 작업표시줄 상주 모드", "투명 창 + UI 밖 클릭 통과", "창 위치·확대 저장 · 항상 위(선택)", "시작 시 자동 실행 · 저전력 모드"])
why(sl, 5.35, 4.0, 7.28, 1.95, ["상주 방치는 '업무 중 PC'에서만 성립합니다.",
"TestVisual 전투 부하 Draw 250~270 · SetPass 최대 79 → 상시 실행용 저전력 예산이 필요합니다(목표치는 P0에서 실측).",
"TBH도 창 제어·자동 실행·항상 위를 '선택'으로 제공합니다."])
D.banner(sl, "결론", "PC(Steam) 우선 — 모바일은 리뉴얼 검증 후 '확인용 동반 앱' 여부를 따로 판단합니다.")
# 08 ② 코어 루프 =========================================================================
sl = D.slide("② 코어 루프 — 수동 서바이벌 → 무개입 자동 전투", "CHANGE 02 | CORE LOOP", "우리 스킬은 이미 자동으로 발동합니다. 바꿀 것은 '이동'과 '판마다 고르는 성장'뿐입니다.",
"근거 FirstRising SkillTable CoolTime(자동 발동) · PlayerMoveInputSystem(수동 이동) · 스킬 선택 UI · TBH 스킬 125종 전부 규칙 발동")
D.image(sl, FR("capture_01_wave_combat.jpg"), X0, Y0, 5.2, 2.93, fill=WHITE)
D.caption(sl, X0, 4.62, 5.2, "FirstRising 웨이브 전투", "스킬 자동 발동 · 이동만 수동")
D.kpis(sl, X0, 4.95, 5.2, 1.0, [("작음", "스킬 발동 · 실패 처리", False), ("중간", "이동 AI · 지속 빌드 · 진행", True)], big=15)
D.table(sl, 6.1, Y0, 6.53, 2.45, [["요소", "지금", "바뀐 뒤", "변경량"], ["스킬 발동", "쿨타임 자동", "그대로 유지", "작음"],
["이동", "플레이어 조작", "AI 자동 이동(접근·회피)", ("중간", RED)], ["성장 선택", "판마다 레벨업 3택", "지속 빌드(장비·스킬 칸)", ("중간", RED)],
["진행", "챕터 입장·종료", "웨이브 연속 → 포털로 다음 스테이지", ("중간", RED)], ["실패", "부활 1회 후 종료", "자동 재도전 · 오프라인 보상", "작음"]],
colw=[1.2, 1.55, 2.95, 0.83], size=9.5, col_colors=[None, SUB, INK, INK], col_bold=[True, False, True, True])
why(sl, 6.1, 4.25, 6.53, 1.7, ["상주형은 켜둔 동안 입력이 0이어야 합니다.", "스킬은 이미 쿨타임 자동 발동 — 입력만 걷어내면 됩니다.",
"전투 연출·물량·기믹은 그대로 재사용합니다."])
D.banner(sl, "결론", "입력만 걷어내 '보는 전투'로 바꿉니다 — 연출·물량·기믹은 그대로 둡니다.")
# 09 ③ 파티 =============================================================================
sl = D.slide("③ 파티 — 2종(스탯 동일) → 3인 파티 · 역할 클래스", "CHANGE 03 | PARTY", "지금 캐릭터 선택은 외형 선택일 뿐입니다. 클래스가 있어야 '내 파티에 맞는 장비'라는 파밍 목표가 생깁니다.",
"근거 FirstRising PlayerTable(Ariel·Barbarian 전 필드 동일) · TBH 3인 파티(실행 화면) · 드롭 행 75% 클래스 조건 · DLC 영웅 3")
D.image(sl, TB("02_taskbar_strip.jpg"), X0, Y0, 5.6, 1.21, fill=WHITE)
D.caption(sl, X0, 2.9, 5.6, "TBH 작업표시줄 스트립", "3인 파티 자동 전투")
D.table(sl, X0, 3.25, 5.6, 2.7, [["역할(안)", "전투 방식", "파밍에서의 의미"], ["방패 탱커", "근접 · 막기 · 방어", "방어·저항 장비 수요"],
["근접 딜러", "광역 · 출혈", "주무기·치명 옵션 수요"], ["원거리 딜러", "투사체 · 관통", "활·석궁·투사체 옵션 수요"], ["서포터", "버프 · 회복 · 부활", "파티 전체 옵션 수요"]],
colw=[1.35, 1.75, 2.5], size=9.5)
asis_tobe(sl, 6.55, Y0, 6.08, 2.2, ["플레이어블 2종", "전 스탯 동일 — 선택은 외형뿐"], ["3인 파티 · 역할 클래스", "탱커 · 근접 · 원거리 · 서포터", "기본 3클래스 무료, 추가는 DLC"], split=0.4)
why(sl, 6.55, 4.0, 6.08, 1.95, ["클래스가 파밍 목표를 만듭니다 — TBH 드롭 행의 75%가 클래스 조건입니다.", "파티 조합이 방치 중 '다음에 볼 이유'가 됩니다.",
"추가 클래스는 DLC 판매 축이 됩니다(TBH: 6종 중 3종 DLC)."])
D.banner(sl, "결론", "기본 3클래스로 무료 파티를 완성하고, 추가 클래스는 DLC로 판매합니다.")
# 10 ④ 아이템 ============================================================================
sl = D.slide("④ 아이템 — 고정 옵션 → 디아블로식 랜덤 파밍", "CHANGE 04 | ITEMIZATION", "같은 아이템이 두 번 나오지 않는 '감정의 기대감'이 방치 중 접속 동기입니다. 고정 옵션은 한 번 맞추면 목표가 끝납니다.",
"근거 FirstRising ItemOptionTable(등급 1~5 · 고정/조건부 능력) · 부위 6 · TBH 10등급 · 옵션 57×10티어 · 고유 옵션 89 · 아이템 레벨 1~100")
D.image(sl, MK("mock_item_tooltip.png"), X0, Y0, 3.7, 4.1, fill=PANEL)
D.caption(sl, X0, 5.8, 3.7, "목업", "FirstRising 아이템 툴팁 예시(이름·수치는 예시)")
D.table(sl, 4.65, Y0, 7.98, 2.9, [["항목", "지금 (FirstRising)", "바뀐 뒤", "참고 (TBH)"], ["등급", "5단계", "8~10단계", "10단계"],
["부위", "6", "8~10 (무기·보조·방어구·장신구)", "10"], ["옵션", "등급별 고정·조건부", "랜덤 옵션 + 티어", "57종 × 10티어"],
["아이템 레벨", "없음", "스테이지 레벨과 연동", "1~100"], ["고유 옵션", "없음", "스킬 변형(원소·범위·투사체)", "89종"],
["드롭", "웨이브별 젬 가중치", "클래스 맞춤 + 등급 추첨", "행 75% 클래스 조건"]],
colw=[1.35, 2.0, 2.85, 1.78], size=9.5, col_colors=[None, SUB, RED, SUB], col_bold=[True, False, True, False])
why(sl, 4.65, 4.7, 7.98, 1.25, ["드롭 하나하나를 '열어볼 이유'로 — 랜덤 옵션 · 티어 · 스킬을 바꾸는 고유 옵션.",
"등급이 오를수록 거래 가능·고가 — Steam 장터(⑦)와 직결됩니다."], title="왜 바꾸나")
D.banner(sl, "결론", "고정 옵션을 걷어내고 '같은 게 두 번 없는' 드롭으로 — 파밍이 곧 방치의 목적이 됩니다.")
# 11 ⑤ 순환 허브 ==========================================================================
sl = D.slide("⑤ 순환 허브 — 흩어진 강화 시스템 → 큐브형 하나로", "CHANGE 05 | LOOP HUB", "방치로 쌓이는 드롭을 한곳에서 합성·분해해야 '보관 한도'가 접속 동기로 작동합니다.",
"근거 FirstRising ItemComposeTable · RuneComposeTable · EvolutionAbilityTable · SpecialTraining · TBH 큐브 9기능 · 합성 확률(GradeInfoData)")
D.image(sl, TB("06_cube_panel.jpg"), X0, Y0, 3.6, 4.1, fill=PANEL)
D.caption(sl, X0, 5.8, 3.6, "TBH 큐브", "연금술 레시피 · 큐브 Lv.93")
asis_tobe(sl, 4.55, Y0, 8.08, 2.75, ["장비 합성", "룬 합성", "진화(306항목)", "특별 훈련(준비 중)", "쓸모없는 드롭을 처리할 곳이 없음"],
["합성 — 같은 등급 N개 → 상위 1개", "분해 — 모든 아이템 → 골드", "재설정 — 옵션 다시 굴리기", "후가공·제거 — 슬롯 부여·해제", "제작 — 부위 지정 · 허브 레벨로 해금"], split=0.4)
why(sl, 4.55, 4.55, 8.08, 1.4, ["쌓인 드롭 → 허브에서 합성·분해 → 보관 공간이 다시 비는 순환이 방치 루프를 닫습니다.",
"TBH 큐브는 9기능을 한 화면에 — 합성은 전설까지 확정 승급, 불멸부터 확률로 바뀝니다."])
D.banner(sl, "결론", "강화처를 하나로 모아 '정리가 곧 성장'이 되게 합니다.")
# 12 ⑥ 메타 ===============================================================================
sl = D.slide("⑥ 메타 성장 — '강해지는 트리' → '방치가 편해지는 트리'", "CHANGE 06 | META", "방치 게임의 성장 목표는 '더 세게'보다 '더 편하게 더 많이'입니다. 자동화 자체를 보상으로 팝니다.",
"근거 TBH RuneLevelInfoData 833행(골드 499.5억) 분류 · 자동화 해금 순서 · FirstRising 진화 306 · 특별훈련 준비 중(실캡처)")
D.image(sl, MK("cover_rune.jpg"), X0, Y0, 5.6, 2.55, fill=WHITE)
D.caption(sl, X0, 4.24, 5.6, "TBH 룬 트리", "248노드 — 대부분이 방치 효율")
with open(os.path.join(BASE, "레퍼런스_TBH", "data", "derived_rune_categories.csv"), encoding="utf-8-sig") as fh:
rc = sorted(csv.DictReader(fh), key=lambda r: -int(r["gold"]))
tot = sum(int(r["gold"]) for r in rc)
D.card(sl, X0, 4.55, 5.6, 1.45)
D.rect(sl, X0, 4.55, 5.6, 1.45, fill=None, line=RULE, shape=MSO_SHAPE.ROUNDED_RECTANGLE, radius=0.03)
D.bar_chart(sl, X0 + 0.1, 4.6, 5.4, 1.35, [r["category"] for r in rc[:4]], [round(int(r["gold"]) / tot * 100, 1) for r in rc[:4]])
D.kpis(sl, 6.55, Y0, 6.08, 1.0, [("94%", "TBH 룬 골드 중 전투 외", True), ("6%", "전투 스탯", False)], big=18)
D.panel(sl, 6.55, 2.8, 6.08, 3.2, radius=0.04)
D.pill(sl, 6.75, 2.95, "FirstRising 방치 효율 트리(안)", fill=RED, w=2.4)
steps = ["오프라인 보상 해금", "상자 자동 개봉", "일괄 분해 필터", "파티 슬롯 +1", "스테이지 웨이브 단축", "보관 한도 확장"]
for i, s in enumerate(steps):
col = i % 2; row = i // 2
sx = 6.75 + col * 2.95; sy = 3.4 + row * 0.62
D.card(sl, sx, sy, 2.75, 0.5)
D.num(sl, sx + 0.12, sy + 0.13, i + 1, d=0.24, size=8.5)
D.text(sl, sx + 0.45, sy + 0.1, 2.25, 0.3, s, size=10.5, bold=True, color=INK)
D.text(sl, 6.75, 5.3, 5.7, 0.6, "진화·특별훈련 자리를 이 트리로 바꿉니다 — 방치로 번 재화를 '더 잘 방치하는 능력'에 재투자하는 구조.", size=9.5, color=SUB)
D.banner(sl, "결론", "자동화를 성장 보상으로 — 방치할수록 방치가 편해지는 사다리를 설계합니다.")
# 13 ⑦ 경제 ===============================================================================
sl = D.slide("⑦ 경제 — 폐쇄 경제 → Steam 장터 거래", "CHANGE 07 | ECONOMY", "드롭에 '현실 가치'가 붙는 순간 파밍 동기가 가장 강해집니다. 대신 통제 장치를 처음부터 설계합니다.",
"근거 TBH ItemInfoData 거래 가능 1,093종 · 교역선 10칸 · 후가공 거래 금지 · 장터 구매품 우편함 수령(로컬라이즈)")
D.image(sl, TB("09_trade_ship.jpg"), X0, Y0, 3.4, 4.1, fill=PANEL)
D.caption(sl, X0, 5.8, 3.4, "TBH 교역선", "싣기 → Steam 장터")
flow = [("드롭·감정", "등급·옵션 확정"), ("보관", "가방·창고"), ("교역선 적재", "거래 가능만"), ("Steam 장터", "유저 간 판매"), ("우편함 수령", "구매품 도착")]
fw = (8.18 - 4 * 0.22) / 5
for i, (t, s) in enumerate(flow):
fx = 4.45 + i * (fw + 0.22)
D.rect(sl, fx, Y0, fw, 0.9, fill=WHITE if i % 2 else PANEL, line=RULE, shape=MSO_SHAPE.ROUNDED_RECTANGLE, radius=0.08)
D.text(sl, fx, Y0 + 0.12, fw, 0.3, t, size=10.5, bold=True, color=RED if i == 3 else INK, align=PP_ALIGN.CENTER)
D.text(sl, fx, Y0 + 0.47, fw, 0.3, s, size=8.5, color=SUB, align=PP_ALIGN.CENTER)
if i < 4:
D.arrow(sl, fx + fw + 0.02, Y0 + 0.45, fx + fw + 0.2, Y0 + 0.45)
D.table(sl, 4.45, 2.75, 8.18, 3.2, [["규칙(안)", "이유"], ["상위 등급만 거래 가능", "저가 아이템 스팸 · 시세 붕괴 방지"],
["후가공(장식 등) 아이템은 거래 불가", "거래 대상 단순화 — 제거하면 거래 가능"], ["교역선 경유 · 칸 제한", "거래량 통제 · 봇 속도 제한"],
["서버가 인벤토리의 원본", "복제·조작 차단(현금 가치)"], ["거래마다 수수료", "외부 매출원 — Steam 수수료 구조는 확인 필요"]],
colw=[3.4, 4.78], size=10, col_colors=[None, BODY])
D.banner(sl, "결론", "Steam 장터로 드롭에 현실 가치를 — 상위 등급 한정·후가공 거래 금지·서버 권한으로 통제합니다.")
# 14 ⑧ BM =================================================================================
sl = D.slide("⑧ BM — 에너지·광고·가챠 → DLC·치장·장터 수수료", "CHANGE 08 | BUSINESS MODEL", "상주 방치와 장터 거래 구조에 맞지 않는 모바일 BM을 걷어내고, 콘텐츠·치장 판매와 거래 수수료로 옮깁니다.",
"근거 FirstRising ShopTable(광고 무료 횟수) · EventGachaTable · MapTable Energy · 코스튬 6세트 · TBH 상점 DLC 번들 · DLC 영웅 3 · 펫 3")
D.image(sl, FR("capture_07_shop_gacha.jpg"), X0, Y0, 4.9, 2.76, fill=WHITE)
D.caption(sl, X0, 4.45, 4.9, "FirstRising 지금", "가챠·광고 상점")
D.arrow(sl, 5.68, 3.0, 6.05, 3.0, width=2.5)
D.image(sl, TB("10_shop.jpg"), 6.15, Y0, 2.1, 2.76, fill=PANEL)
D.caption(sl, 6.15, 4.45, 2.1, "TBH", "DLC 번들 상점")
D.panel(sl, 8.55, Y0, 4.08, 1.3, radius=0.06)
D.pill(sl, 8.73, Y0 + 0.14, "없앤다", fill=DARK, w=0.72)
D.text(sl, 8.73, Y0 + 0.5, 3.75, 0.7, "에너지 · 광고 · 유료 랜덤 박스(가챠)", size=11, bold=True, color=SUB)
D.rect(sl, 8.55, 3.05, 4.08, 1.35, fill=WHITE, line=RULE, shape=MSO_SHAPE.ROUNDED_RECTANGLE, radius=0.06)
D.pill(sl, 8.73, 3.19, "판다", fill=RED, w=0.6)
D.text(sl, 8.73, 3.55, 3.75, 0.8, "DLC 클래스·펫 · 치장(코스튬 재활용) · 시즌 치장 · 장터 수수료", size=11, bold=True, color=INK)
D.panel(sl, X0, 4.8, XW, 1.15, radius=0.08)
reasons = [("에너지", "상주 방치와 정면 충돌 — 켜둔 게임이 멈추면 안 됩니다"), ("광고", "PC 상주형에는 광고 시청 동선이 없습니다"),
("가챠", "유료 랜덤 + 장터 현금 가치 = 사행성 규제 위험 — 법률 검토 전 배제")]
rw = XW / 3
for i, (t, s) in enumerate(reasons):
rx = X0 + i * rw
D.num(sl, rx + 0.2, 4.97, i + 1, d=0.24, size=8.5)
D.text(sl, rx + 0.52, 4.93, rw - 0.7, 0.3, t, size=10.5, bold=True, color=INK)
D.text(sl, rx + 0.52, 5.24, rw - 0.7, 0.65, s, size=9.5, color=BODY)
if i:
D.vline(sl, rx, 4.95, 0.85)
D.banner(sl, "결론", "매출 축을 '확률 판매'에서 '콘텐츠(DLC)·치장 판매 + 거래 수수료'로 옮깁니다.")
# 15 ⑨ 서버·보안 ==========================================================================
sl = D.slide("⑨ 서버·보안 — 현금 가치 경제의 선결 조건", "CHANGE 09 | SERVER & SECURITY", "아이템이 Steam 장터에서 돈이 되는 순간, 복제·조작은 곧 금전 피해입니다. 경제를 열기 전에 보안부터 닫습니다.",
"근거 FirstRising GameHub.cs:326-328(SelfSigned 무조건 통과) · GameHub local 분기 · TBH 안티치트·어뷰징 팝업·인벤토리 복구 UI · 공개 기사(출시 초 골드파밍)")
D.panel(sl, X0, Y0, 7.5, 4.36, radius=0.03)
checks = [("TLS 인증서 검증 복구", "지금은 조건 없이 통과(중간자 공격 노출) — 가장 먼저 수정", True), ("서버 권한 인벤토리·드롭 판정", "클라이언트는 표시만, 생성·삭제·거래는 서버가 결정", True),
("Steam 인증·인벤토리 연동", "Google 로그인 → Steam 계정, 장터용 아이템 정의 등록", False), ("안티치트·봇 탐지·속도 제한", "비정상 획득량·자동 입력 탐지, 위반 시 거래 제한", False),
("거래·획득 감사 로그", "복제·롤백 사고 추적과 복구", False)]
for i, (t, s, crit) in enumerate(checks):
cy = Y0 + 0.2 + i * 0.82
D.card(sl, X0 + 0.18, cy, 7.14, 0.68)
D.num(sl, X0 + 0.34, cy + 0.2, i + 1, d=0.28, size=9)
D.text(sl, X0 + 0.78, cy + 0.07, 5.2, 0.3, t, size=11, bold=True, color=INK)
D.text(sl, X0 + 0.78, cy + 0.36, 6.3, 0.28, s, size=9, color=SUB)
if crit:
D.pill(sl, X0 + 6.3, cy + 0.08, "최우선", fill=RED, w=0.75)
D.rect(sl, 8.4, Y0, 4.23, 4.36, fill=WHITE, line=RULE, shape=MSO_SHAPE.ROUNDED_RECTANGLE, radius=0.04)
D.pill(sl, 8.6, Y0 + 0.18, "왜 먼저인가", fill=RED, w=1.1)
D.bullets(sl, 8.6, Y0 + 0.6, 3.85, 3.6, ["아이템이 장터에서 돈이 되면 복제·조작은 곧 금전 피해입니다.", "TBH도 안티치트 감지·장터 이용 제한·인벤토리 복구 장치를 따로 운영합니다.",
"출시 초 골드파밍·치터로 서버 장애가 보도됐습니다(공개 기사).", "FirstRising은 TLS 검증이 꺼져 있어 지금 상태로는 경제를 열 수 없습니다."],
size=10.5, color=BODY, gap=9)
D.banner(sl, "결론", "경제를 여는 날보다 보안을 닫는 날이 먼저 — P2 착수 조건으로 둡니다.")
# 16 유지 자산 =============================================================================
sl = D.slide("그대로 쓰는 것 — 바꾸지 않는 우리 자산", "KEEP & REUSE | 유지·재활용", "보는 재미를 만드는 전투·연출·데이터는 그대로 씁니다. 새로 만드는 것은 '상주·파밍·거래' 층입니다.",
"근거 FirstRising 시스템 분석(SpawnSystem 2,000 · DamageSystem 판정 4종 · eGimmickType 8종 · ChapterWave · 코스튬) · TestVisual 비주얼 개선")
imgs = [(FR("capture_01_wave_combat.jpg"), "ECS 대규모 물량", "동시 2,000마리"), (FR("capture_02_battlefence.jpg"), "필드 기믹·배틀펜스", "기믹 8종"),
(FR("capture_05_item_costume.jpg"), "코스튬 6세트", "치장 BM으로 재활용")]
iw = (XW - 0.3) / 3
for i, (p, t, s) in enumerate(imgs):
ix = X0 + i * (iw + 0.15)
D.image(sl, p, ix, Y0, iw, 2.2, fill=WHITE)
D.caption(sl, ix, 3.9, iw, t, s)
D.panel(sl, X0, 4.25, XW, 1.7, radius=0.05)
keep = [("전투 연출", "타격 판정 4종 · 데미지 숫자"), ("웨이브 데이터", "ChapterWave 가중치 구조"), ("비주얼", "TestVisual 다크 판타지 렌더"),
("데이터 파이프라인", "JSON 테이블 89종"), ("오프라인 구동", "local 모드(서버 없이 테스트)")]
kw = (XW - 0.34 - 0.48) / 5
for i, (t, s) in enumerate(keep):
kx = X0 + 0.17 + i * (kw + 0.12)
D.card(sl, kx, 4.42, kw, 1.36)
D.pill(sl, kx + 0.14, 4.56, "유지", fill=DARK, w=0.55)
D.text(sl, kx + 0.14, 4.92, kw - 0.28, 0.3, t, size=11, bold=True, color=INK)
D.text(sl, kx + 0.14, 5.24, kw - 0.28, 0.5, s, size=9, color=SUB)
D.banner(sl, "결론", "새로 만드는 게임이 아니라 '바꾸는 게임' — 재활용 자산이 개발 비용과 위험을 줄입니다.")
# 17 로드맵 ================================================================================
sl = D.slide("로드맵 — 단계마다 게이트로 판단", "ROADMAP | 단계와 게이트", "기간·인력은 개발팀 산정 후 확정합니다. 여기서는 순서와 판단 기준(게이트)만 제안합니다.",
"기간·인력 미포함 — 개발팀 산정 후 확정")
ph = [("P0", "상주 프로토타입", ["TestVisual 기반", "투명 창 · 스트립 카메라", "자동 전투 · 저전력 모드"], "30분 상주 시 부하·가독성·'보는 재미' 사내 테스트"),
("P1", "파밍 코어", ["아이템화(등급·랜덤 옵션)", "클래스 3종 · 드롭", "순환 허브 · 방치 트리 · 오프라인"], "사내 방치 플레이 3일 — 접속 동기·보관 압력 확인"),
("P2", "경제 · 보안", ["TLS · 서버 권한 인벤토리", "Steam 인증 · 아이템 정의", "교역 · 안티치트 · 감사 로그"], "법률 검토 완료 + 보안 점검 통과"),
("P3", "Steam 출시", ["체험판 / 앞서 해보기", "DLC 1차(클래스·펫)", "Steam 통계로 퍼널 계측"], "체험판 지표로 정식 출시 판단")]
pw = (XW - 0.45) / 4
for i, (p, t, items, gate) in enumerate(ph):
px = X0 + i * (pw + 0.15)
D.panel(sl, px, Y0, pw, 4.36, radius=0.04)
D.rect(sl, px + 0.18, Y0 + 0.2, 0.62, 0.62, fill=RED if i == 0 else WHITE, line=RED, lw=1.25, shape=MSO_SHAPE.OVAL)
D.text(sl, px + 0.18, Y0 + 0.2, 0.62, 0.62, p, size=13, bold=True, color=WHITE if i == 0 else RED, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
D.text(sl, px + 0.92, Y0 + 0.33, pw - 1.0, 0.35, t, size=13, bold=True, color=INK)
D.card(sl, px + 0.15, Y0 + 1.0, pw - 0.3, 1.75)
D.bullets(sl, px + 0.3, Y0 + 1.13, pw - 0.55, 1.55, items, size=10, color=BODY, gap=6)
D.pill(sl, px + 0.15, Y0 + 2.95, "게이트", fill=DARK, w=0.72)
D.text(sl, px + 0.15, Y0 + 3.3, pw - 0.3, 0.95, gate, size=9.5, color=INK)
if i < 3:
D.arrow(sl, px + pw - 0.02, Y0 + 0.51, px + pw + 0.13, Y0 + 0.51)
D.banner(sl, "요청", "P0만 먼저 승인 요청 — 상주 모드에서 3D 전투가 '보이고 가볍다'가 확인돼야 다음 단계가 의미 있습니다.")
# 18 리스크 ================================================================================
sl = D.slide("리스크와 대응", "RISKS | 리스크", "리스크마다 대응과 확인 시점(게이트)을 정했습니다. 법률 항목은 전문가 검토 전 가정이며 결론이 아닙니다.",
"법률 관련 항목은 전문가 검토 전 가정 — 결론 아님")
D.table(sl, X0, Y0, XW, 4.1, [["리스크", "내용", "대응", "확인 시점"],
["법·규제", "아이템 현금 거래 · 유료 확률형과의 결합(국내 법 해석 필요)", "법률 검토 선행 · 유료 랜덤 박스 배제", "P2 전"],
["경제 어뷰징", "봇 파밍 · 복제로 시세 붕괴", "서버 권한 · 거래 등급 제한 · 봇 탐지", "P2"],
["3D 가독성", "좁은 스트립에서 3D가 뭉개질 위험", "카메라 · 실루엣 · 연출 검증", "P0"],
["상주 성능", "상시 실행 부하(현재 Draw 250~270)", "저전력 모드 · 스트립 영역만 렌더", "P0"],
["매출 공백", "모바일 BM 폐기 후 PC 매출 전까지", "게이트로 투자 단계화", "P0·P1"],
["차별성", "TBH 아류로 보일 위험", "3D 다크 판타지 · 물량 · 타격감을 전면에", "P1"]],
colw=[1.5, 4.6, 4.24, 1.6], size=10, col_colors=[None, BODY, RED, INK], col_bold=[True, False, True, True], hl_rows=(1,))
D.banner(sl, "주의", "법·규제는 결론이 아니라 확인할 항목입니다 — P2 착수 전 전문가 검토를 받습니다.", y=6.0)
# 19 승인 요청 =============================================================================
sl = D.slide("승인 요청", "DECISION | 승인 요청", "아래 4건을 승인해 주시면, 개발팀 P0 산정과 기획팀 P1 상세 설계를 바로 시작합니다.", "")
asks = [("플랫폼 전환", "Steam PC(작업표시줄 상주) 우선 개발"), ("모바일 BM 폐기", "에너지·광고·가챠 제거, DLC·치장·수수료로 전환"),
("P0 프로토타입 착수", "TestVisual 기반 상주 모드 — 게이트 통과 시 P1 진행"), ("법률 검토 착수", "Steam 장터 거래 · 아이템 현금 가치 관련")]
for i, (t, s) in enumerate(asks):
ay = Y0 + 0.1 + i * 0.95
D.rect(sl, X0 + 0.1, ay, 0.68, 0.68, fill=RED, shape=MSO_SHAPE.OVAL)
D.text(sl, X0 + 0.1, ay, 0.68, 0.68, str(i + 1), size=22, bold=True, color=WHITE, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
D.text(sl, X0 + 1.05, ay + 0.02, 10.5, 0.4, t, size=17, bold=True, color=INK)
D.text(sl, X0 + 1.05, ay + 0.4, 10.5, 0.3, s, size=11, color=SUB)
D.hline(sl, X0, ay + 0.84, XW)
D.rect(sl, 0, 6.2, 13.333, 0.5, fill=RED)
D.text(sl, X0, 6.2, XW, 0.5, "승인 시 다음 단계 개발팀 P0 범위·기간 산정 → 기획팀 P1 상세 설계(아이템화 · 순환 허브 · 방치 트리) 병행",
size=11, bold=True, color=WHITE, anchor=MSO_ANCHOR.MIDDLE)
n = D.save(OUT)
print("saved", OUT, "slides:", n)