Files
ProjectSS/client/src/core/engine/desk.js
T
kyoung5seo 83060f3e5b 구현 우선 순위 1차 진행 완료
- 1차 — 코어 검증 (여기까지만 되어도 재미 판정 가능)
  1. **기호 체계 + 책상 상태 상시 노출 UI** (§3)
  2. **책상 적재 + 누진 패시브 페널티 A·B** (§4)
  3. **표준 족보 판정 및 합본 실행** (§5)
  4. **합본 기반 토큰 산출** (§8.2)

 게임 플레이 후 피드백 해결
 - 내용은 아래 문서 참고
 - docs/decisions/2026-09-05_core-graybox-ux-feedback.md
2026-09-05 01:06:41 +09:00

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];
}