minor fixes

master
Denis Ranneft 1 month ago
parent 945458d345
commit ab9486cb83

@ -1173,6 +1173,9 @@ export function App() {
const dismissToast = useCallback(() => setToast(null), []); const dismissToast = useCallback(() => setToast(null), []);
const questClaimDisabled =
gameState.phase === GamePhase.Dead || (gameState.hero?.hp ?? 0) <= 0;
return ( return (
<I18nContext.Provider value={{ tr: translations, locale, setLocale: handleSetLocale }}> <I18nContext.Provider value={{ tr: translations, locale, setLocale: handleSetLocale }}>
<div style={appStyle}> <div style={appStyle}>
@ -1205,6 +1208,7 @@ export function App() {
quests={heroQuests} quests={heroQuests}
onQuestClaim={handleQuestClaim} onQuestClaim={handleQuestClaim}
onQuestAbandon={handleQuestAbandon} onQuestAbandon={handleQuestAbandon}
questClaimDisabled={questClaimDisabled}
/> />
)} )}
@ -1226,11 +1230,8 @@ export function App() {
<DeathScreen <DeathScreen
visible={gameState.phase === GamePhase.Dead} visible={gameState.phase === GamePhase.Dead}
onRevive={handleRevive} onRevive={handleRevive}
revivesRemaining={ subscriptionUnlimited={!!gameState.hero?.subscriptionActive}
gameState.hero?.subscriptionActive revivesRemaining={Math.max(0, 2 - (gameState.hero?.reviveCount ?? 0))}
? undefined
: Math.max(0, 2 - (gameState.hero?.reviveCount ?? 0))
}
/> />
{/* Toast Notification */} {/* Toast Notification */}
@ -1292,6 +1293,7 @@ export function App() {
onQuestsChanged={refreshHeroQuests} onQuestsChanged={refreshHeroQuests}
onHeroUpdated={handleNPCHeroUpdated} onHeroUpdated={handleNPCHeroUpdated}
onToast={(message, color) => setToast({ message, color })} onToast={(message, color) => setToast({ message, color })}
questClaimDisabled={questClaimDisabled}
/> />
)} )}

@ -93,6 +93,7 @@ export const en = {
questLog: 'Quest Log', questLog: 'Quest Log',
noActiveQuests: 'No active quests. Visit an NPC to accept quests!', noActiveQuests: 'No active quests. Visit an NPC to accept quests!',
claimRewards: 'Claim Rewards', claimRewards: 'Claim Rewards',
claimRewardsDisabledDead: 'Revive to claim quest rewards',
questDestination: 'Destination', questDestination: 'Destination',
abandon: 'Abandon', abandon: 'Abandon',
acceptQuest: 'Accept', acceptQuest: 'Accept',
@ -127,6 +128,8 @@ export const en = {
youDied: 'YOU DIED', youDied: 'YOU DIED',
reviveNow: 'REVIVE NOW', reviveNow: 'REVIVE NOW',
freeRevivesLeft: 'Free revives left: {count}', freeRevivesLeft: 'Free revives left: {count}',
revivesUnlimitedSubscription: 'Unlimited revives (subscription)',
reviveNowWithCount: 'REVIVE NOW ({count})',
autoReviveIn: 'Auto-revive in {timer}s', autoReviveIn: 'Auto-revive in {timer}s',
noFreeRevives: 'No free revives left \u2014 subscription required', noFreeRevives: 'No free revives left \u2014 subscription required',

@ -95,6 +95,8 @@ export const ru: Translations = {
questLog: '\u0416\u0443\u0440\u043d\u0430\u043b \u0437\u0430\u0434\u0430\u043d\u0438\u0439', questLog: '\u0416\u0443\u0440\u043d\u0430\u043b \u0437\u0430\u0434\u0430\u043d\u0438\u0439',
noActiveQuests: '\u041d\u0435\u0442 \u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0445 \u0437\u0430\u0434\u0430\u043d\u0438\u0439. \u041f\u043e\u0433\u043e\u0432\u043e\u0440\u0438\u0442\u0435 \u0441 NPC!', noActiveQuests: '\u041d\u0435\u0442 \u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0445 \u0437\u0430\u0434\u0430\u043d\u0438\u0439. \u041f\u043e\u0433\u043e\u0432\u043e\u0440\u0438\u0442\u0435 \u0441 NPC!',
claimRewards: '\u041f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043d\u0430\u0433\u0440\u0430\u0434\u0443', claimRewards: '\u041f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043d\u0430\u0433\u0440\u0430\u0434\u0443',
claimRewardsDisabledDead:
'\u0412\u043e\u0441\u043a\u0440\u0435\u0441\u043d\u0438\u0442\u0435, \u0447\u0442\u043e\u0431\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043d\u0430\u0433\u0440\u0430\u0434\u044b \u0437\u0430 \u0437\u0430\u0434\u0430\u043d\u0438\u044f',
questDestination: '\u041f\u0443\u043d\u043a\u0442 \u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f', questDestination: '\u041f\u0443\u043d\u043a\u0442 \u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f',
abandon: '\u041e\u0442\u043a\u0430\u0437\u0430\u0442\u044c\u0441\u044f', abandon: '\u041e\u0442\u043a\u0430\u0437\u0430\u0442\u044c\u0441\u044f',
acceptQuest: '\u041f\u0440\u0438\u043d\u044f\u0442\u044c', acceptQuest: '\u041f\u0440\u0438\u043d\u044f\u0442\u044c',
@ -129,6 +131,8 @@ export const ru: Translations = {
youDied: '\u0412\u042b \u041f\u041e\u0413\u0418\u0411\u041b\u0418', youDied: '\u0412\u042b \u041f\u041e\u0413\u0418\u0411\u041b\u0418',
reviveNow: '\u0412\u041e\u0421\u041a\u0420\u0415\u0421\u0418\u0422\u042c', reviveNow: '\u0412\u041e\u0421\u041a\u0420\u0415\u0421\u0418\u0422\u042c',
freeRevivesLeft: '\u0411\u0435\u0441\u043f\u043b\u0430\u0442\u043d\u044b\u0445 \u0432\u043e\u0441\u043a\u0440\u0435\u0448\u0435\u043d\u0438\u0439: {count}', freeRevivesLeft: '\u0411\u0435\u0441\u043f\u043b\u0430\u0442\u043d\u044b\u0445 \u0432\u043e\u0441\u043a\u0440\u0435\u0448\u0435\u043d\u0438\u0439: {count}',
revivesUnlimitedSubscription: '\u0411\u0435\u0437\u043b\u0438\u043c\u0438\u0442\u043d\u044b\u0435 \u0432\u043e\u0441\u043a\u0440\u0435\u0448\u0435\u043d\u0438\u044f (\u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0430)',
reviveNowWithCount: '\u0412\u041e\u0421\u041a\u0420\u0415\u0421\u0418\u0422\u042c ({count})',
autoReviveIn: '\u0410\u0432\u0442\u043e-\u0432\u043e\u0441\u043a\u0440\u0435\u0448\u0435\u043d\u0438\u0435 \u0447\u0435\u0440\u0435\u0437 {timer}\u0441', autoReviveIn: '\u0410\u0432\u0442\u043e-\u0432\u043e\u0441\u043a\u0440\u0435\u0448\u0435\u043d\u0438\u0435 \u0447\u0435\u0440\u0435\u0437 {timer}\u0441',
noFreeRevives: '\u041d\u0435\u0442 \u0431\u0435\u0441\u043f\u043b\u0430\u0442\u043d\u044b\u0445 \u0432\u043e\u0441\u043a\u0440\u0435\u0448\u0435\u043d\u0438\u0439 \u2014 \u043d\u0443\u0436\u043d\u0430 \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0430', noFreeRevives: '\u041d\u0435\u0442 \u0431\u0435\u0441\u043f\u043b\u0430\u0442\u043d\u044b\u0445 \u0432\u043e\u0441\u043a\u0440\u0435\u0448\u0435\u043d\u0438\u0439 \u2014 \u043d\u0443\u0436\u043d\u0430 \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0430',

@ -5,8 +5,10 @@ import { useT, t } from '../i18n';
interface DeathScreenProps { interface DeathScreenProps {
visible: boolean; visible: boolean;
onRevive: () => void; onRevive: () => void;
/** Free revives left for non-subscribers; omit when subscribed / unlimited. */ /** Active subscription: manual revives are not limited by the free quota. */
revivesRemaining?: number; subscriptionUnlimited?: boolean;
/** Free manual revives left (02) when not subscriptionUnlimited. */
revivesRemaining: number;
} }
/** Full-screen dimming; `pointerEvents: 'none'` so HUD controls (e.g. hero sheet) stay clickable. */ /** Full-screen dimming; `pointerEvents: 'none'` so HUD controls (e.g. hero sheet) stay clickable. */
@ -62,10 +64,15 @@ const buttonStyle: CSSProperties = {
transition: 'background-color 0.2s', transition: 'background-color 0.2s',
}; };
export function DeathScreen({ visible, onRevive, revivesRemaining }: DeathScreenProps) { export function DeathScreen({
visible,
onRevive,
subscriptionUnlimited,
revivesRemaining,
}: DeathScreenProps) {
const tr = useT(); const tr = useT();
const [timer, setTimer] = useState(REVIVE_TIMER_SECONDS); const [timer, setTimer] = useState(REVIVE_TIMER_SECONDS);
const canRevive = revivesRemaining === undefined || revivesRemaining > 0; const canRevive = !!subscriptionUnlimited || revivesRemaining > 0;
// Countdown timer // Countdown timer
useEffect(() => { useEffect(() => {
@ -101,11 +108,11 @@ export function DeathScreen({ visible, onRevive, revivesRemaining }: DeathScreen
<div style={deathPanelStyle}> <div style={deathPanelStyle}>
<div style={titleStyle}>{tr.youDied}</div> <div style={titleStyle}>{tr.youDied}</div>
<div style={timerStyle}>{canRevive ? timer : '—'}</div> <div style={timerStyle}>{canRevive ? timer : '—'}</div>
{revivesRemaining !== undefined && ( <div style={{ color: '#aaa', fontSize: 14 }}>
<div style={{ color: '#aaa', fontSize: 14 }}> {subscriptionUnlimited
{t(tr.freeRevivesLeft, { count: Math.max(0, revivesRemaining) })} ? tr.revivesUnlimitedSubscription
</div> : t(tr.freeRevivesLeft, { count: Math.max(0, revivesRemaining) })}
)} </div>
<button <button
style={{ style={{
...buttonStyle, ...buttonStyle,
@ -122,7 +129,9 @@ export function DeathScreen({ visible, onRevive, revivesRemaining }: DeathScreen
e.currentTarget.style.backgroundColor = '#cc3333'; e.currentTarget.style.backgroundColor = '#cc3333';
}} }}
> >
{tr.reviveNow} {subscriptionUnlimited
? tr.reviveNow
: t(tr.reviveNowWithCount, { count: Math.max(0, revivesRemaining) })}
</button> </button>
<div style={{ color: '#888', fontSize: 13 }}> <div style={{ color: '#888', fontSize: 13 }}>
{canRevive ? t(tr.autoReviveIn, { timer }) : tr.noFreeRevives} {canRevive ? t(tr.autoReviveIn, { timer }) : tr.noFreeRevives}

@ -124,6 +124,8 @@ interface HeroSheetModalProps {
quests: HeroQuest[]; quests: HeroQuest[];
onQuestClaim: (heroQuestId: number) => void; onQuestClaim: (heroQuestId: number) => void;
onQuestAbandon: (heroQuestId: number) => void; onQuestAbandon: (heroQuestId: number) => void;
/** Disable claim while hero is dead (HP 0 / death phase). */
questClaimDisabled?: boolean;
} }
export function HeroSheetModal({ export function HeroSheetModal({
@ -137,6 +139,7 @@ export function HeroSheetModal({
quests, quests,
onQuestClaim, onQuestClaim,
onQuestAbandon, onQuestAbandon,
questClaimDisabled = false,
}: HeroSheetModalProps) { }: HeroSheetModalProps) {
const [tab, setTab] = useState<HeroSheetTab>(initialTab); const [tab, setTab] = useState<HeroSheetTab>(initialTab);
const tr = useT(); const tr = useT();
@ -207,6 +210,7 @@ export function HeroSheetModal({
quests={quests} quests={quests}
onClaim={onQuestClaim} onClaim={onQuestClaim}
onAbandon={onQuestAbandon} onAbandon={onQuestAbandon}
claimDisabled={questClaimDisabled}
/> />
)} )}
{tab === 'settings' && ( {tab === 'settings' && (

@ -18,6 +18,8 @@ interface NPCDialogProps {
onQuestsChanged: () => void; onQuestsChanged: () => void;
onHeroUpdated: (hero: HeroResponse) => void; onHeroUpdated: (hero: HeroResponse) => void;
onToast: (message: string, color: string) => void; onToast: (message: string, color: string) => void;
/** Block quest reward claim while hero is dead. */
questClaimDisabled?: boolean;
} }
// ---- Styles ---- // ---- Styles ----
@ -242,6 +244,7 @@ export function NPCDialog({
onQuestsChanged, onQuestsChanged,
onHeroUpdated, onHeroUpdated,
onToast, onToast,
questClaimDisabled = false,
}: NPCDialogProps) { }: NPCDialogProps) {
const tr = useT(); const tr = useT();
const [availableQuests, setAvailableQuests] = useState<Quest[]>([]); const [availableQuests, setAvailableQuests] = useState<Quest[]>([]);
@ -419,8 +422,14 @@ export function NPCDialog({
)} )}
</div> </div>
<button <button
style={claimBtnStyle} type="button"
onClick={() => handleClaimQuest(hq.id)} style={questClaimDisabled ? { ...claimBtnStyle, opacity: 0.45, cursor: 'not-allowed', animation: 'none' } : claimBtnStyle}
disabled={questClaimDisabled}
title={questClaimDisabled ? tr.claimRewardsDisabledDead : undefined}
onClick={() => {
if (questClaimDisabled) return;
handleClaimQuest(hq.id);
}}
> >
{tr.claimRewards} {tr.claimRewards}
</button> </button>

@ -9,6 +9,7 @@ interface QuestLogProps {
onClaim: (heroQuestId: number) => void; onClaim: (heroQuestId: number) => void;
onAbandon: (heroQuestId: number) => void; onAbandon: (heroQuestId: number) => void;
onClose: () => void; onClose: () => void;
claimDisabled?: boolean;
} }
// ---- Quest Type Icons ---- // ---- Quest Type Icons ----
@ -167,6 +168,15 @@ const claimBtnStyle: CSSProperties = {
animation: 'quest-claim-glow 1.5s ease-in-out infinite', animation: 'quest-claim-glow 1.5s ease-in-out infinite',
}; };
const claimBtnDisabledStyle: CSSProperties = {
...claimBtnStyle,
opacity: 0.45,
cursor: 'not-allowed',
animation: 'none',
boxShadow: 'none',
textShadow: 'none',
};
const abandonBtnStyle: CSSProperties = { const abandonBtnStyle: CSSProperties = {
padding: '8px 14px', padding: '8px 14px',
border: '1px solid rgba(255, 80, 80, 0.3)', border: '1px solid rgba(255, 80, 80, 0.3)',
@ -189,10 +199,12 @@ interface QuestLogListProps {
quests: HeroQuest[]; quests: HeroQuest[];
onClaim: (heroQuestId: number) => void; onClaim: (heroQuestId: number) => void;
onAbandon: (heroQuestId: number) => void; onAbandon: (heroQuestId: number) => void;
/** When true, completed-quest claim is blocked (e.g. hero dead). */
claimDisabled?: boolean;
} }
/** Quest list body (embedded in Hero sheet or standalone panel). */ /** Quest list body (embedded in Hero sheet or standalone panel). */
export function QuestLogList({ quests, onClaim, onAbandon }: QuestLogListProps) { export function QuestLogList({ quests, onClaim, onAbandon, claimDisabled = false }: QuestLogListProps) {
const tr = useT(); const tr = useT();
const [expandedId, setExpandedId] = useState<number | null>(null); const [expandedId, setExpandedId] = useState<number | null>(null);
@ -294,9 +306,13 @@ export function QuestLogList({ quests, onClaim, onAbandon }: QuestLogListProps)
<div style={actionRow}> <div style={actionRow}>
{isCompleted ? ( {isCompleted ? (
<button <button
style={claimBtnStyle} type="button"
style={claimDisabled ? claimBtnDisabledStyle : claimBtnStyle}
disabled={claimDisabled}
title={claimDisabled ? tr.claimRewardsDisabledDead : undefined}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
if (claimDisabled) return;
onClaim(q.id); onClaim(q.id);
}} }}
> >
@ -325,7 +341,7 @@ export function QuestLogList({ quests, onClaim, onAbandon }: QuestLogListProps)
); );
} }
export function QuestLog({ quests, onClaim, onAbandon, onClose }: QuestLogProps) { export function QuestLog({ quests, onClaim, onAbandon, onClose, claimDisabled }: QuestLogProps) {
const tr = useT(); const tr = useT();
useEffect(() => { useEffect(() => {
const handleKey = (e: KeyboardEvent) => { const handleKey = (e: KeyboardEvent) => {
@ -350,7 +366,7 @@ export function QuestLog({ quests, onClaim, onAbandon, onClose }: QuestLogProps)
<span style={titleStyle}>{'\uD83D\uDCDC'} {tr.questLog}</span> <span style={titleStyle}>{'\uD83D\uDCDC'} {tr.questLog}</span>
<button style={closeBtnStyle} onClick={onClose}>{'\u2715'}</button> <button style={closeBtnStyle} onClick={onClose}>{'\u2715'}</button>
</div> </div>
<QuestLogList quests={quests} onClaim={onClaim} onAbandon={onAbandon} /> <QuestLogList quests={quests} onClaim={onClaim} onAbandon={onAbandon} claimDisabled={claimDisabled} />
</div> </div>
</div> </div>
</> </>

Loading…
Cancel
Save