소프트락 감사: 재추첨 장터 해금 + 진행 불가 위험 3건 수정
- 입양 재추첨 버튼: 장터 완공 전까지 숨김 (완공 후 해금) - 부엌 있는데 요리 가능 고양이가 없으면 입양 후보에 요리 적성 보장 - 서버 접속 불가 시에도 닉네임 결정 가능 (오프라인 시작 → 이후 자동 재등록) - 저장 403(미등록 닉네임) 시 재등록 후 재시도 - 파업 중 굶주림 행복도 하락 제외 + 자연 회복 +2/분 (전원 파업 데드락 방지) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
이 커밋은 다음에 포함됨:
@@ -315,7 +315,10 @@ export default function Game() {
|
||||
🐾 고양이
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
onClick={() => {
|
||||
const needCook =
|
||||
state.buildings.some((b) => b.type === "kitchen") &&
|
||||
!state.cats.some((c) => c.aptitude.cooking > 0);
|
||||
setCandidates(
|
||||
engine.generateCandidates(
|
||||
3,
|
||||
@@ -323,10 +326,12 @@ export default function Game() {
|
||||
? "fishing"
|
||||
: state.tutorialStep === 4
|
||||
? "logging"
|
||||
: needCook
|
||||
? "cooking"
|
||||
: undefined,
|
||||
),
|
||||
)
|
||||
}
|
||||
);
|
||||
}}
|
||||
disabled={state.cats.length >= state.hall.capacity}
|
||||
className={"nature-btn flex items-center gap-1.5 px-4 py-2 text-[15px] disabled:cursor-not-allowed disabled:opacity-50" + tut("adopt")}
|
||||
>
|
||||
@@ -523,9 +528,15 @@ export default function Game() {
|
||||
highlightPick={tutTarget === "adopt-pick"}
|
||||
cost={adoptCost(state)}
|
||||
coins={Math.floor(state.coins ?? 0)}
|
||||
rerollUnlocked={state.buildings.some(
|
||||
(b) => b.type === "market" && b.constructionUntil === undefined,
|
||||
)}
|
||||
onReroll={() => {
|
||||
const err = run(() => engine.payReroll());
|
||||
if (!err)
|
||||
if (!err) {
|
||||
const needCook =
|
||||
state.buildings.some((b) => b.type === "kitchen") &&
|
||||
!state.cats.some((c) => c.aptitude.cooking > 0);
|
||||
setCandidates(
|
||||
engine.generateCandidates(
|
||||
3,
|
||||
@@ -533,9 +544,12 @@ export default function Game() {
|
||||
? "fishing"
|
||||
: state.tutorialStep === 4
|
||||
? "logging"
|
||||
: needCook
|
||||
? "cooking"
|
||||
: undefined,
|
||||
),
|
||||
);
|
||||
}
|
||||
}}
|
||||
onPick={(cat) => {
|
||||
const err = run(() => engine.adoptCandidate(cat));
|
||||
@@ -1333,6 +1347,7 @@ function AdoptModal({
|
||||
cost,
|
||||
onReroll,
|
||||
coins,
|
||||
rerollUnlocked,
|
||||
}: {
|
||||
candidates: Cat[];
|
||||
onPick: (cat: Cat) => void;
|
||||
@@ -1341,6 +1356,7 @@ function AdoptModal({
|
||||
cost: number;
|
||||
onReroll: () => void;
|
||||
coins: number;
|
||||
rerollUnlocked?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
@@ -1431,6 +1447,7 @@ function AdoptModal({
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{rerollUnlocked && (
|
||||
<div className="mt-4 flex items-center justify-center gap-3">
|
||||
<button
|
||||
onClick={onReroll}
|
||||
@@ -1440,6 +1457,7 @@ function AdoptModal({
|
||||
🎲 다른 아이들 보기 (🪙{REROLL_COST})
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<p className="mt-3 text-center text-xs text-[#8a7551]">
|
||||
한 마리만 선택할 수 있어요 · 패시브에 마우스를 올리면 상세 정보가 보입니다
|
||||
</p>
|
||||
|
||||
@@ -56,10 +56,12 @@ export default function NicknameModal({ onDone }: { onDone: () => void }) {
|
||||
};
|
||||
}, [trimmed, mode]);
|
||||
|
||||
// 모드별 결정 가능 조건: 새 마을=사용 가능, 불러오기=이미 존재
|
||||
// 모드별 결정 가능 조건: 새 마을=사용 가능(서버 불가 시에도 허용), 불러오기=이미 존재
|
||||
const canConfirm =
|
||||
!submitting &&
|
||||
(mode === "new" ? status === "available" : status === "taken");
|
||||
(mode === "new"
|
||||
? status === "available" || status === "offline"
|
||||
: status === "taken");
|
||||
|
||||
const confirm = async () => {
|
||||
if (!canConfirm) return;
|
||||
@@ -96,7 +98,7 @@ export default function NicknameModal({ onDone }: { onDone: () => void }) {
|
||||
checking: { text: "다른 마을과 겹치는지 확인 중… 🐾", cls: "text-[#8a7551]" },
|
||||
available: { text: "좋아, 이 이름은 비어 있어! ✨", cls: "text-[#5c8a2a]" },
|
||||
taken: { text: "앗, 이미 다른 마을이 쓰고 있는 이름이야 🙀", cls: "text-[#b0472a]" },
|
||||
offline: { text: "서버에 닿지 않아… 잠시 후 다시 시도해줘", cls: "text-[#b0472a]" },
|
||||
offline: { text: "확인은 못 했지만 일단 시작할 수 있어! (나중에 자동 확인)", cls: "text-[#8a6316]" },
|
||||
},
|
||||
recover: {
|
||||
idle: { text: "예전에 쓰던 마을 이름을 알려줘!", cls: "text-[#8a7551]" },
|
||||
|
||||
+5
-1
@@ -598,7 +598,8 @@ export const engine = {
|
||||
if (state.happinessUnlocked) {
|
||||
if (st.fullness >= 80)
|
||||
st.happiness = Math.min(100, st.happiness + (1 / 60) * gainMul * dt);
|
||||
if (st.fullness <= 0)
|
||||
// 굶주림 하락 — 파업 중엔 제외 (회복이 하락을 못 이기면 영구 파업 데드락)
|
||||
if (st.fullness <= 0 && cat.state !== "striking")
|
||||
st.happiness = Math.max(0, st.happiness - (4 / 60) * decayMul * dt);
|
||||
// 고양이 집 부족: 하락하되 이 요인만으로는 파업까지 가지 않도록 바닥 25 유지
|
||||
if (
|
||||
@@ -631,6 +632,9 @@ export const engine = {
|
||||
cat.assignedTo = null;
|
||||
cat.state = "striking";
|
||||
}
|
||||
// 파업 중엔 놀면서 서서히 기분이 풀림 (전원 파업 데드락 방지)
|
||||
if (cat.state === "striking")
|
||||
st.happiness = Math.min(100, st.happiness + (2 / 60) * dt);
|
||||
if (cat.state === "striking" && st.happiness >= STRIKE_RESUME)
|
||||
tryReturn(cat);
|
||||
} // happinessUnlocked
|
||||
|
||||
+17
-6
@@ -38,16 +38,27 @@ export async function registerNickname(
|
||||
export async function pushSave(): Promise<void> {
|
||||
const s = engine.peek();
|
||||
if (!s?.nickname || !s.syncToken) return;
|
||||
try {
|
||||
await fetch("/api/village/save", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
const body = JSON.stringify({
|
||||
nickname: s.nickname,
|
||||
token: s.syncToken,
|
||||
data: s,
|
||||
}),
|
||||
});
|
||||
try {
|
||||
const res = await fetch("/api/village/save", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body,
|
||||
});
|
||||
if (res.status === 403) {
|
||||
// 오프라인 상태에서 시작해 서버에 등록되지 않은 닉네임 — 재등록 후 재시도
|
||||
const ok = await registerNickname(s.nickname, s.syncToken);
|
||||
if (ok)
|
||||
await fetch("/api/village/save", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// 오프라인이면 다음 주기에 재시도
|
||||
}
|
||||
|
||||
새 이슈에서 참조
사용자 차단