Files
core2026/app/game-frontend/src/views/InheritView.vue
T

987 lines
33 KiB
Vue

<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue';
import { trpc } from '../utils/trpc';
type InheritStatus = Awaited<ReturnType<typeof trpc.inherit.getStatus.query>>;
type InheritLog = Awaited<ReturnType<typeof trpc.inherit.getLogs.query>>[number];
type JoinConfig = Awaited<ReturnType<typeof trpc.join.getConfig.query>>;
type BuffKey =
| 'warAvoidRatio'
| 'warCriticalRatio'
| 'warMagicTrialProb'
| 'domesticSuccessProb'
| 'domesticFailProb'
| 'warAvoidRatioOppose'
| 'warCriticalRatioOppose'
| 'warMagicTrialProbOppose';
const buffKeys: BuffKey[] = [
'warAvoidRatio',
'warCriticalRatio',
'warMagicTrialProb',
'domesticSuccessProb',
'domesticFailProb',
'warAvoidRatioOppose',
'warCriticalRatioOppose',
'warMagicTrialProbOppose',
];
const buffLabels: Record<BuffKey, string> = {
warAvoidRatio: '회피 확률 증가',
warCriticalRatio: '필살 확률 증가',
warMagicTrialProb: '전투계략 시도 확률 증가',
domesticSuccessProb: '내정 성공 확률 증가',
domesticFailProb: '내정 실패 확률 감소',
warAvoidRatioOppose: '상대 회피 확률 감소',
warCriticalRatioOppose: '상대 필살 확률 감소',
warMagicTrialProbOppose: '상대 전투계략 시도 확률 감소',
};
const pointLabels: Record<string, string> = {
previous: '보유',
lived_month: '생존 턴',
max_domestic_critical: '최대 연속 내정 성공',
active_action: '능동 행동 수',
combat: '전투 횟수',
sabotage: '계략 성공 횟수',
dex: '숙련도',
unifier: '천통 기여',
tournament: '토너먼트',
betting: '베팅 당첨',
max_belong: '최대 임관년 수',
};
const pointOrder = [
'previous',
'lived_month',
'max_belong',
'max_domestic_critical',
'active_action',
'combat',
'sabotage',
'dex',
'unifier',
'tournament',
'betting',
] as const;
const pointHelp: Record<string, string> = {
previous: '이전에 물려받은 포인트입니다.',
lived_month: '살아남은 기간입니다. (1개월 단위)',
max_belong: '가장 오래 임관했던 국가의 연도입니다.',
max_domestic_critical: '성공한 내정 중 최대 연속값입니다.',
active_action: '장수 동향에 본인의 이름이 직접 나타난 수입니다. 일부 사령턴은 제외됩니다.',
combat: '전투 횟수입니다.',
sabotage: '계략 성공 횟수입니다.',
unifier: '천통에 기여한 포인트입니다. 각 국의 군주, 천통 수뇌, 천통 군주가 받습니다.',
dex: '총 숙련도합입니다. 최대 숙련 이후에는 상승량이 1/3로 감소합니다.',
tournament: '토너먼트 입상 포인트입니다.',
betting: '성공적인 베팅을 했습니다. 수익율과 베팅 성공 횟수를 따릅니다.',
};
const buffHelp: Record<BuffKey, string> = {
warAvoidRatio: '전투 시 회피 확률이 1%p ~ 5%p 증가합니다.',
warCriticalRatio: '전투 시 필살 확률이 1%p ~ 5%p 증가합니다.',
warMagicTrialProb: '전투 시 계략을 시도할 확률이 1%p ~ 5%p 증가합니다. 무장도 계략을 시도합니다.',
domesticSuccessProb: '민심, 인구, 농업, 상업, 치안, 수비, 성벽, 기술 내정의 성공 확률이 증가합니다.',
domesticFailProb: '민심, 인구, 농업, 상업, 치안, 수비, 성벽, 기술 내정의 실패 확률이 감소합니다.',
warAvoidRatioOppose: '전투 시 상대의 회피 확률이 1%p ~ 5%p 감소합니다.',
warCriticalRatioOppose: '전투 시 상대의 필살 확률이 1%p ~ 5%p 감소합니다.',
warMagicTrialProbOppose: '전투 시 상대의 계략 시도 확률이 1%p ~ 5%p 감소합니다.',
};
const loading = ref(true);
const error = ref<string | null>(null);
const status = ref<InheritStatus | null>(null);
const logs = ref<InheritLog[]>([]);
const logLoading = ref(false);
const logEnd = ref(false);
const actionError = ref<string | null>(null);
const actionMessage = ref<string | null>(null);
const actionBusy = ref(false);
const joinConfig = ref<JoinConfig | null>(null);
const buffTargets = reactive<Record<BuffKey, number>>({
warAvoidRatio: 1,
warCriticalRatio: 1,
warMagicTrialProb: 1,
domesticSuccessProb: 1,
domesticFailProb: 1,
warAvoidRatioOppose: 1,
warCriticalRatioOppose: 1,
warMagicTrialProbOppose: 1,
});
const nextSpecialKey = ref('');
const ownerTargetId = ref('');
const ownerResult = ref<{ targetName: string; ownerName: string } | null>(null);
const turnTimeResult = ref<string | null>(null);
const uniqueForm = reactive({
itemId: '',
amount: 0,
});
const resetStatForm = reactive({
leadership: 0,
strength: 0,
intel: 0,
bonus: [0, 0, 0] as [number, number, number],
});
const statRules = computed(() => joinConfig.value?.rules.stat ?? null);
const resetStatTotal = computed(() => resetStatForm.leadership + resetStatForm.strength + resetStatForm.intel);
const resetBonusSum = computed(() => resetStatForm.bonus.reduce((acc, value) => acc + value, 0));
const resetStatCost = computed(() =>
resetBonusSum.value > 0 ? (status.value?.inheritConst.inheritBornStatPoint ?? 0) : 0
);
const resetStatErrors = computed(() => {
const errors: string[] = [];
const rules = statRules.value;
if (rules) {
if (resetStatTotal.value !== rules.total) {
errors.push(`능력치 총합이 ${rules.total}이 아닙니다.`);
}
if (
resetStatForm.leadership < rules.min ||
resetStatForm.strength < rules.min ||
resetStatForm.intel < rules.min ||
resetStatForm.leadership > rules.max ||
resetStatForm.strength > rules.max ||
resetStatForm.intel > rules.max
) {
errors.push(`능력치는 ${rules.min} ~ ${rules.max} 범위여야 합니다.`);
}
}
if (resetBonusSum.value !== 0 && (resetBonusSum.value < 3 || resetBonusSum.value > 5)) {
errors.push('보너스 능력치 합이 잘못되었습니다.');
}
return errors;
});
const turnTimeLabel = computed(() => {
if (!turnTimeResult.value) {
return null;
}
const parsed = new Date(turnTimeResult.value);
if (Number.isNaN(parsed.getTime())) {
return turnTimeResult.value;
}
return parsed.toLocaleString();
});
const isUnited = computed(() => status.value?.isUnited ?? false);
const pointEntries = computed(() => {
if (!status.value) {
return [] as Array<{ key: string; label: string; value: number }>;
}
return pointOrder.map((key) => ({
key,
label: pointLabels[key] ?? key,
value: status.value?.items[key] ?? 0,
}));
});
const previousPoint = computed(() => status.value?.items.previous ?? 0);
const newPoint = computed(() => (status.value?.totalPoint ?? 0) - previousPoint.value);
const specialNameMap = computed(() => {
const map = new Map<string, string>();
for (const entry of status.value?.availableSpecialWar ?? []) {
map.set(entry.key, entry.name);
}
return map;
});
const buffCost = (key: BuffKey, target: number): number => {
const points = status.value?.inheritConst.inheritBuffPoints ?? [0, 0, 0, 0, 0, 0];
const current = status.value?.buffLevels[key] ?? 0;
return Math.max(0, (points[target] ?? 0) - (points[current] ?? 0));
};
const resolveErrorMessage = (value: unknown): string => {
if (value instanceof Error) {
return value.message;
}
if (typeof value === 'string') {
return value;
}
return 'unknown_error';
};
const syncSelections = () => {
if (!status.value) {
return;
}
for (const key of buffKeys) {
const current = status.value.buffLevels[key] ?? 0;
buffTargets[key] = Math.min(5, Math.max(1, current + 1));
}
if (!nextSpecialKey.value) {
nextSpecialKey.value = status.value.availableSpecialWar[0]?.key ?? '';
}
if (!ownerTargetId.value) {
ownerTargetId.value = String(status.value.availableTargetGenerals[0]?.id ?? '');
}
if (!uniqueForm.amount) {
uniqueForm.amount = status.value.inheritConst.inheritItemUniqueMinPoint;
}
if (!uniqueForm.itemId) {
uniqueForm.itemId = status.value.availableUnique[0]?.key ?? '';
}
if (resetStatForm.leadership === 0 && resetStatForm.strength === 0 && resetStatForm.intel === 0) {
resetStatForm.leadership = status.value.currentStat.leadership;
resetStatForm.strength = status.value.currentStat.strength;
resetStatForm.intel = status.value.currentStat.intel;
}
};
const loadStatus = async () => {
loading.value = true;
error.value = null;
actionError.value = null;
try {
status.value = await trpc.inherit.getStatus.query();
syncSelections();
} catch (err) {
error.value = resolveErrorMessage(err);
} finally {
loading.value = false;
}
};
const loadJoinConfig = async () => {
try {
joinConfig.value = await trpc.join.getConfig.query();
} catch {
joinConfig.value = null;
}
};
const loadLogs = async (reset = false) => {
if (logLoading.value) {
return;
}
logLoading.value = true;
try {
const lastId = reset ? undefined : logs.value[logs.value.length - 1]?.id;
const result = await trpc.inherit.getLogs.query({ lastId });
logs.value = reset ? result : [...logs.value, ...result];
logEnd.value = result.length < 30;
} catch (err) {
actionError.value = resolveErrorMessage(err);
} finally {
logLoading.value = false;
}
};
const runAction = async (action: () => Promise<void>, message?: string) => {
if (actionBusy.value) {
return;
}
actionBusy.value = true;
actionError.value = null;
actionMessage.value = null;
try {
await action();
if (message) {
actionMessage.value = message;
}
await loadStatus();
await loadLogs(true);
} catch (err) {
actionError.value = resolveErrorMessage(err);
} finally {
actionBusy.value = false;
}
};
const buyHiddenBuff = async (key: BuffKey) => {
if (!status.value) {
return;
}
const target = buffTargets[key];
const cost = buffCost(key, target);
if (!window.confirm(`${buffLabels[key]} ${target}단계를 ${cost} 포인트로 구입하시겠습니까?`)) {
return;
}
await runAction(async () => {
await trpc.inherit.buyHiddenBuff.mutate({ type: key, level: target });
});
};
const reserveSpecialWar = async () => {
if (!nextSpecialKey.value) {
actionError.value = '다음 전투 특기를 선택해주세요.';
return;
}
const name = specialNameMap.value.get(nextSpecialKey.value) ?? nextSpecialKey.value;
const cost = status.value?.inheritConst.inheritSpecificSpecialPoint ?? 0;
if (!window.confirm(`${name} 특기를 ${cost} 포인트로 예약하시겠습니까?`)) {
return;
}
await runAction(async () => {
await trpc.inherit.setNextSpecialWar.mutate({ specialKey: nextSpecialKey.value });
});
};
const resetSpecialWar = async () => {
const cost = status.value?.resetCosts.resetSpecialWar ?? 0;
if (!window.confirm(`전투 특기를 ${cost} 포인트로 초기화하시겠습니까?`)) {
return;
}
await runAction(async () => {
await trpc.inherit.resetSpecialWar.mutate();
});
};
const resetTurnTime = async () => {
const cost = status.value?.resetCosts.resetTurnTime ?? 0;
if (!window.confirm(`턴 시간을 ${cost} 포인트로 초기화하시겠습니까?`)) {
return;
}
await runAction(async () => {
const result = await trpc.inherit.resetTurnTime.mutate();
turnTimeResult.value = result.nextTurnTime;
});
};
const resetStats = async () => {
if (resetStatErrors.value.length > 0) {
actionError.value = resetStatErrors.value[0] ?? '입력값을 확인해주세요.';
return;
}
if (!window.confirm('능력치를 초기화하시겠습니까?')) {
return;
}
await runAction(async () => {
await trpc.inherit.resetStat.mutate({
leadership: resetStatForm.leadership,
strength: resetStatForm.strength,
intel: resetStatForm.intel,
inheritBonusStat: resetStatForm.bonus,
});
});
};
const buyRandomUnique = async () => {
const cost = status.value?.inheritConst.inheritItemRandomPoint ?? 0;
if (!window.confirm(`랜덤 유니크를 ${cost} 포인트로 구매하시겠습니까?`)) {
return;
}
await runAction(async () => {
await trpc.inherit.buyRandomUnique.mutate();
});
};
const openUniqueAuction = async () => {
if (!uniqueForm.itemId.trim()) {
actionError.value = '유니크를 선택해주세요.';
return;
}
const amount = Math.max(0, Math.floor(uniqueForm.amount));
if (amount <= 0) {
actionError.value = '입찰 포인트를 입력해주세요.';
return;
}
if (!window.confirm(`유니크 경매를 ${amount} 포인트로 신청하시겠습니까?`)) {
return;
}
await runAction(async () => {
await trpc.inherit.openUniqueAuction.mutate({
itemId: uniqueForm.itemId.trim(),
amount,
});
});
};
const checkOwner = async () => {
const targetId = Number(ownerTargetId.value);
if (!targetId) {
actionError.value = '확인할 장수를 선택해주세요.';
return;
}
const cost = status.value?.inheritConst.inheritCheckOwnerPoint ?? 0;
if (!window.confirm(`장수 소유자 확인에 ${cost} 포인트를 사용하시겠습니까?`)) {
return;
}
await runAction(async () => {
const result = await trpc.inherit.checkOwner.mutate({ targetGeneralId: targetId });
ownerResult.value = { targetName: result.targetName, ownerName: result.ownerName };
});
};
onMounted(() => {
void loadStatus();
void loadJoinConfig();
void loadLogs(true);
});
</script>
<template>
<header class="top-back-bar legacy-bg0">
<RouterLink class="top-button legacy-button" to="/">돌아가기</RouterLink>
<strong>유산 관리</strong>
<button class="top-button legacy-button" type="button" :disabled="loading" @click="loadStatus">갱신</button>
</header>
<main id="container" class="inherit-page legacy-bg0">
<div v-if="error || actionError" class="notice error" role="alert">{{ error ?? actionError }}</div>
<div v-if="actionMessage" class="notice success">{{ actionMessage }}</div>
<div v-if="loading" class="loading-state">불러오는 중...</div>
<template v-else-if="status">
<section id="inheritance_list" class="point-grid">
<article id="inherit_sum" class="inherit-item">
<label for="inherit_sum_value"> 포인트</label>
<input id="inherit_sum_value" :value="Math.floor(status.totalPoint).toLocaleString()" readonly />
<small>다음 플레이에서 사용할 있는 포인트입니다.</small>
</article>
<article id="inherit_previous" class="inherit-item">
<label for="inherit_previous_value">기존 포인트</label>
<input id="inherit_previous_value" :value="Math.floor(previousPoint).toLocaleString()" readonly />
<small>이전에 물려받은 포인트입니다.</small>
</article>
<article id="inherit_new" class="inherit-item">
<label for="inherit_new_value">신규 포인트</label>
<input id="inherit_new_value" :value="Math.floor(newPoint).toLocaleString()" readonly />
<small>이번 플레이에서 얻은 포인트입니다.</small>
</article>
<div class="divider"></div>
<article
v-for="entry in pointEntries.filter((item) => item.key !== 'previous')"
:id="`inherit_${entry.key}`"
:key="entry.key"
class="inherit-item"
>
<label :for="`inherit_${entry.key}_value`">{{ entry.label }}</label>
<input
:id="`inherit_${entry.key}_value`"
:value="Math.floor(entry.value).toLocaleString()"
readonly
/>
<small>{{ pointHelp[entry.key] }}</small>
</article>
</section>
<section id="inheritance_store">
<h2 class="section-title legacy-bg1">유산 포인트 상점</h2>
<div class="action-grid leading-actions">
<article class="shop-item">
<div class="control-row">
<label for="next-special">다음 전투 특기 선택</label>
<select id="next-special" v-model="nextSpecialKey">
<option v-for="entry in status.availableSpecialWar" :key="entry.key" :value="entry.key">
{{ entry.name }}
</option>
</select>
</div>
<small
>{{ specialNameMap.get(nextSpecialKey) }} 특기를 다음에 얻도록 지정합니다.<br /><b
>필요 포인트: {{ status.inheritConst.inheritSpecificSpecialPoint }}</b
></small
>
<button
class="legacy-button buy-button"
:disabled="isUnited || actionBusy"
@click="reserveSpecialWar"
>
구입
</button>
</article>
<article class="shop-item">
<div class="control-row">
<label for="specific-unique">유니크 경매</label>
<select id="specific-unique" v-model="uniqueForm.itemId">
<option disabled value="">유니크 선택</option>
<option v-for="item in status.availableUnique" :key="item.key" :value="item.key">
{{ item.name }}
</option>
</select>
</div>
<div class="control-row">
<label for="specific-unique-amount">입찰 포인트</label>
<input
id="specific-unique-amount"
v-model.number="uniqueForm.amount"
type="number"
:min="status.inheritConst.inheritItemUniqueMinPoint"
:max="previousPoint"
/>
</div>
<small
>얻고자 하는 유니크 아이템으로 경매를 시작합니다. 24 동안 진행됩니다.<br />{{
status.availableUnique.find((item) => item.key === uniqueForm.itemId)?.info
}}</small
>
<button
class="legacy-button buy-button"
:disabled="isUnited || actionBusy"
@click="openUniqueAuction"
>
경매 시작
</button>
</article>
</div>
<div class="divider"></div>
<div class="action-grid">
<article class="shop-item simple-item">
<div class="control-row">
<span>랜덤 초기화</span
><button class="legacy-button" :disabled="isUnited || actionBusy" @click="resetTurnTime">
구입
</button>
</div>
<small
>다다음턴부터 시간이 랜덤하게 바뀝니다. (필요 포인트가 피보나치식으로 증가합니다)<br /><b
>필요 포인트: {{ status.resetCosts.resetTurnTime }}</b
><template v-if="turnTimeLabel"><br />적용 시간: {{ turnTimeLabel }}</template></small
>
</article>
<article class="shop-item simple-item">
<div class="control-row">
<span>랜덤 유니크 획득</span
><button class="legacy-button" :disabled="isUnited || actionBusy" @click="buyRandomUnique">
구입
</button>
</div>
<small
>다음 턴에 랜덤 유니크를 얻습니다.<br /><b
>필요 포인트: {{ status.inheritConst.inheritItemRandomPoint }}</b
></small
>
</article>
<article class="shop-item simple-item">
<div class="control-row">
<span>즉시 전투 특기 초기화</span
><button class="legacy-button" :disabled="isUnited || actionBusy" @click="resetSpecialWar">
구입
</button>
</div>
<small
>즉시 전투 특기를 초기화합니다. (필요 포인트가 피보나치식으로 증가합니다)<br /><b
>필요 포인트: {{ status.resetCosts.resetSpecialWar }}</b
></small
>
</article>
</div>
<div class="divider"></div>
<div class="buff-grid">
<article v-for="key in buffKeys" :key="key" class="shop-item buff-item">
<div class="control-row">
<label :for="`buff-${key}`">{{ buffLabels[key] }}</label>
<input
:id="`buff-${key}`"
v-model.number="buffTargets[key]"
type="number"
:min="status.buffLevels[key] ?? 0"
max="5"
/>
</div>
<small
>{{ buffHelp[key] }}<br /><b>필요 포인트: {{ buffCost(key, buffTargets[key]) }}</b></small
>
<div class="dual-buttons">
<button
class="legacy-button secondary"
:disabled="actionBusy"
@click="buffTargets[key] = status.buffLevels[key] ?? 0"
>
리셋
</button>
<button
class="legacy-button"
:disabled="isUnited || actionBusy"
@click="buyHiddenBuff(key)"
>
구입
</button>
</div>
</article>
</div>
<div class="divider"></div>
<div class="action-grid bottom-actions">
<article class="shop-item">
<div class="control-row">
<label for="owner-target">장수 소유자 확인</label>
<select id="owner-target" v-model="ownerTargetId">
<option disabled value="">장수 선택</option>
<option
v-for="general in status.availableTargetGenerals"
:key="general.id"
:value="String(general.id)"
>
{{ general.name }}
</option>
</select>
</div>
<small
>장수의 소유자를 찾습니다. 대상에게도 알림이 전송됩니다.<br /><b
>필요 포인트: {{ status.inheritConst.inheritCheckOwnerPoint }}</b
></small
>
<button class="legacy-button buy-button" :disabled="isUnited || actionBusy" @click="checkOwner">
소유자 찾기
</button>
<p v-if="ownerResult" class="owner-result">
{{ ownerResult.targetName }} 소유자: {{ ownerResult.ownerName }}
</p>
</article>
<article class="shop-item stat-reset">
<div class="stat-layout">
<span>능력치 초기화</span>
<div>
<strong>기본 능력치</strong>
<label
>
<input
v-model.number="resetStatForm.leadership"
type="number"
:min="statRules?.min"
:max="statRules?.max"
/></label>
<label
>
<input
v-model.number="resetStatForm.strength"
type="number"
:min="statRules?.min"
:max="statRules?.max"
/></label>
<label
>
<input
v-model.number="resetStatForm.intel"
type="number"
:min="statRules?.min"
:max="statRules?.max"
/></label>
<strong>추가 능력치</strong>
<label
> <input v-model.number="resetStatForm.bonus[0]" type="number" min="0" max="5"
/></label>
<label
> <input v-model.number="resetStatForm.bonus[1]" type="number" min="0" max="5"
/></label>
<label
> <input v-model.number="resetStatForm.bonus[2]" type="number" min="0" max="5"
/></label>
</div>
</div>
<small
>시즌 1회에 능력치를 초기화합니다.<br /><b>필요 포인트: {{ resetStatCost }}</b
><br /><span v-if="resetStatErrors.length">{{ resetStatErrors[0] }}</span></small
>
<button
class="legacy-button buy-button"
:disabled="isUnited || actionBusy || resetStatErrors.length > 0"
@click="resetStats"
>
능력치 초기화
</button>
</article>
</div>
</section>
<section class="inherit-logs">
<h2 class="section-title legacy-bg1">유산 포인트 변경 내역</h2>
<div v-if="logLoading && logs.length === 0" class="log-empty">불러오는 중...</div>
<div v-else-if="logs.length === 0" class="log-empty">기록이 없습니다.</div>
<div v-for="entry in logs" v-else :key="entry.id" class="log-row">
<small>[{{ new Date(entry.createdAt).toLocaleString('ko-KR') }}]</small>
<span>{{ entry.text }}</span>
</div>
<button class="legacy-button more-button" :disabled="logLoading || logEnd" @click="loadLogs()">
가져오기
</button>
</section>
</template>
</main>
</template>
<style scoped>
.top-back-bar {
width: min(100%, 1000px);
min-height: 38px;
margin: 0 auto;
border: 1px solid #888;
display: grid;
grid-template-columns: 100px 1fr 100px;
align-items: center;
text-align: center;
padding: 3px 6px;
box-sizing: border-box;
}
.top-button {
padding: 4px 8px;
text-decoration: none;
}
.inherit-page {
width: min(100%, 1000px);
margin: 0 auto;
border: 1px solid #888;
border-top: 0;
overflow: hidden;
box-sizing: border-box;
padding: 0 8px 10px;
color: #fff;
font:
14px/1.3 Pretendard,
'Apple SD Gothic Neo',
'Noto Sans KR',
'Malgun Gothic',
sans-serif;
}
.notice,
.loading-state,
.log-empty {
padding: 10px;
text-align: center;
}
.notice.error {
color: #ffb0b0;
}
.notice.success {
color: #b6efb6;
}
.point-grid,
.action-grid,
.buff-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.inherit-item,
.shop-item {
padding: 8px 16px;
box-sizing: border-box;
min-width: 0;
}
.inherit-item {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(100px, 1fr);
align-items: start;
gap: 6px;
}
.inherit-item label {
text-align: right;
padding: 7px 8px 0 0;
}
.inherit-item input,
.shop-item input,
.shop-item select {
width: 100%;
min-width: 0;
border: 1px solid #6c757d;
border-radius: 4px;
background: #212529;
color: #fff;
padding: 6px 8px;
box-sizing: border-box;
}
.inherit-item small,
.shop-item small {
grid-column: 1 / -1;
min-height: 34px;
text-align: right;
color: #aeb2b6;
}
.inherit-item small {
min-height: 36px;
}
.divider {
grid-column: 1 / -1;
border-top: 1px solid rgba(255, 255, 255, 0.22);
margin: 4px 2px;
}
.section-title {
font-size: 14px;
font-weight: 400;
text-align: center;
margin: 0 -8px;
padding: 2px;
}
.leading-actions .shop-item:first-child {
grid-column: 2;
}
.control-row {
display: grid;
grid-template-columns: 1fr 1fr;
align-items: center;
gap: 8px;
min-height: 36px;
}
.control-row > label,
.control-row > span {
text-align: right;
}
.shop-item {
display: flex;
flex-direction: column;
gap: 6px;
}
.shop-item .buy-button {
width: 50%;
margin-left: auto;
}
.simple-item small {
min-height: 55px;
}
.buff-item small {
min-height: 72px;
}
.dual-buttons {
display: grid;
grid-template-columns: 1fr 1fr;
}
.legacy-button.secondary {
border-color: #51585e;
background: #5c636a;
}
.bottom-actions .shop-item:first-child {
grid-column: 1;
}
.stat-layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
text-align: right;
}
.stat-layout > div {
display: grid;
gap: 3px;
}
.stat-layout label {
display: grid;
grid-template-columns: 22px 1fr;
align-items: center;
}
.stat-layout strong {
text-align: left;
font-size: 12px;
}
.owner-result {
margin: 0;
color: #fff;
text-align: right;
}
.inherit-logs {
margin: 8px 0 0;
}
.log-row {
display: grid;
grid-template-columns: minmax(150px, 20ch) 1fr;
gap: 8px;
padding: 3px 8px;
}
.log-row small {
color: #aeb2b6;
text-align: right;
font-variant-numeric: tabular-nums;
}
.more-button {
width: 100%;
margin-top: 6px;
}
button:focus-visible,
input:focus-visible,
select:focus-visible,
a:focus-visible {
outline: 2px solid #f39c12;
outline-offset: 1px;
}
@media (max-width: 767px) {
.point-grid,
.action-grid,
.buff-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.leading-actions .shop-item:first-child {
grid-column: 1;
}
}
@media (max-width: 575px) {
.top-back-bar,
.inherit-page {
width: 500px;
max-width: 100%;
}
.point-grid,
.action-grid,
.buff-grid {
grid-template-columns: 1fr;
}
.divider {
grid-column: 1;
}
.inherit-item,
.shop-item {
padding-left: 16px;
padding-right: 16px;
}
.log-row {
grid-template-columns: 1fr;
}
.log-row small {
text-align: left;
}
}
</style>