- 1차 — 코어 검증 (여기까지만 되어도 재미 판정 가능) 1. **기호 체계 + 책상 상태 상시 노출 UI** (§3) 2. **책상 적재 + 누진 패시브 페널티 A·B** (§4) 3. **표준 족보 판정 및 합본 실행** (§5) 4. **합본 기반 토큰 산출** (§8.2) 게임 플레이 후 피드백 해결 - 내용은 아래 문서 참고 - docs/decisions/2026-09-05_core-graybox-ux-feedback.md
115 lines
4.4 KiB
JavaScript
115 lines
4.4 KiB
JavaScript
// 책상(Desk) 엔진 — ProjectSS_코어시스템_기획서.md §4.
|
|
// 오염 카드는 덱에 들어가지 않는다. 드로우·셔플 없음, 슬롯 무제한. 여기서 다루는 desk는
|
|
// 순수 배열 [{ id, axis, grade }] 이며, React state는 이 배열을 그대로 들고 있다가
|
|
// 이 모듈의 함수로 파생값(축별 스택, 패널티)을 계산한다. React/훅 의존 없음 — 순수 함수만.
|
|
|
|
import { AXES } from './symbols.js';
|
|
|
|
let seq = 0;
|
|
// meta: pollutionPool.js에서 뽑은 { name, flavor, tag } — 책상 카드가 실제 이름/사연을 갖게 한다.
|
|
export const makeDeskCard = (axis, grade, meta = {}) => ({
|
|
id: `desk-${Date.now().toString(36)}-${(seq++).toString(36)}`,
|
|
axis,
|
|
grade,
|
|
name: meta.name || null,
|
|
flavor: meta.flavor || null,
|
|
tag: meta.tag || null,
|
|
});
|
|
|
|
/** 축별 적재 장수. { axis_threat: 3, axis_hr: 1, ... } 형태로 항상 4축 전부를 채워 반환한다. */
|
|
export function countByAxis(deskCards) {
|
|
const out = Object.fromEntries(AXES.map((a) => [a, 0]));
|
|
deskCards.forEach((c) => { out[c.axis] = (out[c.axis] || 0) + 1; });
|
|
return out;
|
|
}
|
|
|
|
/** 축별 등급 분포. { axis_threat: {1: 2, 3: 1}, ... } — DeskBar의 축×등급 매트릭스 표시용. */
|
|
export function gradeBreakdownByAxis(deskCards) {
|
|
const out = Object.fromEntries(AXES.map((a) => [a, {}]));
|
|
deskCards.forEach((c) => {
|
|
out[c.axis][c.grade] = (out[c.axis][c.grade] || 0) + 1;
|
|
});
|
|
return out;
|
|
}
|
|
|
|
export const totalCount = (deskCards) => deskCards.length;
|
|
|
|
/* ── A. 게이지 상시 상승 (§4.2-A, 플레이테스트 피드백 반영 재조정) ──
|
|
* 최초 구현은 "장수"만 셌는데, 1급 기밀이나 일반 문서나 똑같이 취급돼 위기감이 없었다.
|
|
* 그래서 장수 대신 등급 가중 "오염 압력"(P)을 쓴다: 등급1=1, 등급2=2, 등급3=3, 등급4=5.
|
|
* 구간도 초반 체감이 되도록 올렸다 — 정확한 수치는 가안, 플레이테스트로 계속 조정한다.
|
|
*/
|
|
const GRADE_WEIGHT = { 1: 1, 2: 2, 3: 3, 4: 5 };
|
|
|
|
/** 해당 축의 오염 압력(P) = Σ 카드별 등급 가중치. */
|
|
export function axisPressure(deskCards, axis) {
|
|
return deskCards
|
|
.filter((c) => c.axis === axis)
|
|
.reduce((sum, c) => sum + (GRADE_WEIGHT[c.grade] || 0), 0);
|
|
}
|
|
|
|
const GAUGE_TIERS = [
|
|
{ max: 0, rate: 0 },
|
|
{ max: 2, rate: 1.0 },
|
|
{ max: 6, rate: 2.0 },
|
|
{ max: 12, rate: 3.5 },
|
|
{ max: 20, rate: 6.0 },
|
|
{ max: 30, rate: 9.0 },
|
|
{ max: Infinity, rate: 13.0 },
|
|
];
|
|
|
|
/** 오염 압력(P)에 따른 결재 1건당 게이지 상승률(%). */
|
|
export function gaugeRiseRate(pressure) {
|
|
const tier = GAUGE_TIERS.find((t) => pressure <= t.max);
|
|
return tier ? tier.rate : 13.0;
|
|
}
|
|
|
|
/** 축별 현재 상승률(%) — DeskBar의 "▲+X%/결재" 표시에 그대로 쓴다. */
|
|
export function gaugeRiseRateByAxis(deskCards) {
|
|
const out = {};
|
|
AXES.forEach((axis) => { out[axis] = gaugeRiseRate(axisPressure(deskCards, axis)); });
|
|
return out;
|
|
}
|
|
|
|
/** 매 결재마다 4개 게이지 전부에 축별 상승분을 더한다. gauges = { entropy, resistance, panic, risk } (0~100). */
|
|
export function applyPassiveGaugeRise(gauges, deskCards) {
|
|
const next = { ...gauges };
|
|
AXES.forEach((axis) => {
|
|
const gaugeKey = axisGaugeKey(axis);
|
|
const rate = gaugeRiseRate(axisPressure(deskCards, axis));
|
|
next[gaugeKey] = Math.min(100, (next[gaugeKey] || 0) + rate);
|
|
});
|
|
return next;
|
|
}
|
|
|
|
// symbols.js의 AXIS_META를 다시 import하면 순환 우려가 없으므로 바로 가져와 쓴다.
|
|
import { AXIS_META } from './symbols.js';
|
|
function axisGaugeKey(axis) {
|
|
return AXIS_META[axis].gauge;
|
|
}
|
|
|
|
/* ── B. 토큰 지출 가산 (§4.2-B) — 총 적재 장수에 따라 모든 토큰 지출에 가산 ── */
|
|
const SURCHARGE_TIERS = [
|
|
{ max: 7, surcharge: 0 },
|
|
{ max: 14, surcharge: 1 },
|
|
{ max: 21, surcharge: 2 },
|
|
{ max: Infinity, surcharge: 4 },
|
|
];
|
|
|
|
/** 총 적재 장수 기준 모든 토큰 지출에 붙는 가산액. */
|
|
export function tokenSurcharge(deskCards) {
|
|
const total = totalCount(deskCards);
|
|
const tier = SURCHARGE_TIERS.find((t) => total <= t.max);
|
|
return tier ? tier.surcharge : 4;
|
|
}
|
|
|
|
/** 책상에서 특정 카드들을 제거한 새 배열을 반환한다 (합본 실행 시 사용). */
|
|
export function removeFromDesk(deskCards, idsToRemove) {
|
|
const idSet = new Set(idsToRemove);
|
|
return deskCards.filter((c) => !idSet.has(c.id));
|
|
}
|
|
|
|
export function addToDesk(deskCards, newCards) {
|
|
return [...deskCards, ...newCards];
|
|
}
|