Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function makeDefaultState(players) {
};
}

function applyAutoResets(raw, players) {
function applyAutoResets(raw, players, weekStartDay = 1) {
const state = { ...makeDefaultState(players), ...raw };

// migrate old points field to gold
Expand Down Expand Up @@ -137,9 +137,9 @@ function applyAutoResets(raw, players) {
changed = true;
}

if (state.weekKey !== weekKey()) {
if (state.weekKey !== weekKey(weekStartDay)) {
state.weeklyDone = {};
state.weekKey = weekKey();
state.weekKey = weekKey(weekStartDay);
state.weeklyGold = Object.fromEntries(players.map(p => [p.id, 0]));
changed = true;
}
Expand Down Expand Up @@ -248,7 +248,7 @@ export default function App() {

const stateRes = await fetch(`${API}/state`);
const fetched = await stateRes.json();
const { state: after, changed, penaltyMsgs } = applyAutoResets(fetched, cfg.players);
const { state: after, changed, penaltyMsgs } = applyAutoResets(fetched, cfg.players, cfg.weekStartDay ?? 1);

if (changed) {
await fetch(`${API}/state`, {
Expand All @@ -273,7 +273,7 @@ export default function App() {
try {
const res = await fetch(`${API}/state`);
const fetched = await res.json();
const { state: after, changed } = applyAutoResets(fetched, players);
const { state: after, changed } = applyAutoResets(fetched, players, config?.weekStartDay ?? 1);
if (changed) await saveState(after);
setServerState(after);
} catch (e) {
Expand Down Expand Up @@ -677,7 +677,7 @@ export default function App() {

const handleSetupComplete = useCallback(async (wizardConfig) => {
const freshState = makeDefaultState(wizardConfig.players);
const { state: after } = applyAutoResets(freshState, wizardConfig.players);
const { state: after } = applyAutoResets(freshState, wizardConfig.players, wizardConfig.weekStartDay ?? 1);
await Promise.all([
fetch(`${API}/config`, {
method: 'POST',
Expand Down
44 changes: 38 additions & 6 deletions frontend/src/components/SetupWizard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ function StepWelcome({ onNext }) {
<div style={{ fontSize: 48, marginBottom: 8 }}>⚔</div>
<div style={{ color: '#f5c870', fontSize: 22, fontWeight: 'bold', marginBottom: 8 }}>QUESTBOARD</div>
<p style={{ ...S.p, maxWidth: 360, margin: '0 auto 24px' }}>
Turn household chores into a pixel art RPG adventure. Each family member gets a hero and fights a monster every day complete chores to deal damage and earn gold.
Turn household chores into a pixel art RPG adventure. Each family member gets a hero and fights a monster every day - complete chores to deal damage and earn gold.
</p>
<button style={S.btnPrimary} onClick={onNext}>Start Setup →</button>
</div>
Expand Down Expand Up @@ -222,7 +222,7 @@ function StepPlayerSetup({ player, playerIdx, total, onChange, onNext, onBack, o
<div>
<div style={S.h2}>
Hero {playerIdx + 1} of {total}
{player.name && <span style={{ color: '#f5c870' }}> {player.name}</span>}
{player.name && <span style={{ color: '#f5c870' }}> - {player.name}</span>}
</div>
<PlayerForm player={player} onChange={onChange} />
{!canAdvance && (
Expand Down Expand Up @@ -342,7 +342,7 @@ function ChoreSection({ players, enabledChores, onToggle, choreOverrides, onOver
<button
onClick={e => { e.stopPropagation(); onOverride(chore.id, { ...ov, mode: mode === 'party' ? 'solo' : 'party' }); }}
style={{ background: 'none', border: '1px solid #3a3a5e', color: mode === 'solo' ? '#f5a0c0' : '#8dc447', fontSize: 10, padding: '2px 6px', cursor: 'pointer', minWidth: 34 }}
title={mode === 'solo' ? '1 player only tap to make shared' : 'All players share tap to make solo'}
title={mode === 'solo' ? '1 player only - tap to make shared' : 'All players share - tap to make solo'}
>{mode === 'solo' ? '1P' : 'ALL'}</button>
<CycleBtn value={who} onClick={e => { e.stopPropagation(); const next = WHO_CYCLE[(WHO_CYCLE.indexOf(who) + 1) % WHO_CYCLE.length]; onOverride(chore.id, { ...ov, who: next }); }} />
<button
Expand Down Expand Up @@ -530,7 +530,7 @@ function RewardSection({ players, enabledRewards, onToggle, rewardOverrides, onO
}

// ── Step 4: Reward selection (wizard) ─────────────────────────────────────────
function StepRewardSelect({ players, enabledRewards, onToggle, rewardOverrides, onOverride, customRewards, onAddCustom, onRemoveCustom, onBack, onLaunch, crtEnabled, onToggleCrt, uiScale, onChangeUiScale, animatedBg, onToggleAnimatedBg }) {
function StepRewardSelect({ players, enabledRewards, onToggle, rewardOverrides, onOverride, customRewards, onAddCustom, onRemoveCustom, onBack, onLaunch, crtEnabled, onToggleCrt, uiScale, onChangeUiScale, animatedBg, onToggleAnimatedBg, weekStartDay, onChangeWeekStartDay }) {
return (
<div>
<div style={S.h2}>Choose your rewards</div>
Expand All @@ -543,12 +543,24 @@ function StepRewardSelect({ players, enabledRewards, onToggle, rewardOverrides,
/>
<div style={{ marginTop: 20, borderTop: '1px solid #2a2a4a', paddingTop: 16 }}>
<div style={{ ...S.label, marginBottom: 10 }}>DISPLAY</div>
<div style={{ ...S.label, marginBottom: 6, fontSize: 10, color: '#8a8aaa' }}>WEEK STARTS ON</div>
<div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
{[{ id: 1, label: 'Monday' }, { id: 0, label: 'Sunday' }].map(opt => (
<button
key={opt.id}
style={{ ...(weekStartDay === opt.id ? S.btnPrimary : S.btn), flex: 1, padding: '6px 4px', fontSize: 11 }}
onClick={() => onChangeWeekStartDay(opt.id)}
>
{opt.label}
</button>
))}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 12 }}>
<button
style={{ ...(crtEnabled ? S.btnPrimary : S.btn), padding: '6px 14px', fontSize: 11 }}
onClick={onToggleCrt}
>
{crtEnabled ? 'CRT Scanlines ON' : 'CRT Scanlines OFF'}
{crtEnabled ? 'CRT Scanlines ON' : 'CRT Scanlines OFF'}
</button>
<span style={{ color: '#5a5a7a', fontSize: 10 }}>Retro CRT overlay effect</span>
</div>
Expand Down Expand Up @@ -713,9 +725,24 @@ function TabPowerUps({ powerUpSettings, onChange }) {
}

// ── Edit tab: Display ─────────────────────────────────────────────────────────
function TabDisplay({ crtEnabled, onToggleCrt, uiScale, onChangeUiScale, animatedBg, onToggleAnimatedBg }) {
function TabDisplay({ crtEnabled, onToggleCrt, uiScale, onChangeUiScale, animatedBg, onToggleAnimatedBg, weekStartDay, onChangeWeekStartDay }) {
return (
<div>
<div style={{ marginBottom: 24 }}>
<div style={{ ...S.label, marginBottom: 10 }}>WEEK STARTS ON</div>
<div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
{[{ id: 1, label: 'Monday' }, { id: 0, label: 'Sunday' }].map(opt => (
<button
key={opt.id}
style={{ ...(weekStartDay === opt.id ? S.btnPrimary : S.btn), flex: 1, padding: '8px 4px', fontSize: 11 }}
onClick={() => onChangeWeekStartDay(opt.id)}
>
{opt.label}
</button>
))}
</div>
<div style={{ color: '#5a5a7a', fontSize: 10, marginBottom: 16 }}>Controls when weekly chores and gold reset</div>
</div>
<div style={{ marginBottom: 24 }}>
<div style={{ ...S.label, marginBottom: 10 }}>CRT EFFECT</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
Expand Down Expand Up @@ -781,6 +808,7 @@ export default function SetupWizard({ onComplete, onCancel, initialConfig }) {
const [crtEnabled, setCrtEnabled] = useState(initialConfig?.crtEnabled ?? true);
const [uiScale, setUiScale] = useState(initialConfig?.uiScale ?? 'mini');
const [animatedBg, setAnimatedBg] = useState(initialConfig?.animatedBg ?? true);
const [weekStartDay, setWeekStartDay] = useState(initialConfig?.weekStartDay ?? 1);
const [powerUpSettings, setPowerUpSettings] = useState(
initialConfig?.powerUpSettings ?? { ...DEFAULT_POWER_UP_SETTINGS }
);
Expand Down Expand Up @@ -852,6 +880,7 @@ export default function SetupWizard({ onComplete, onCancel, initialConfig }) {
crtEnabled,
uiScale,
animatedBg,
weekStartDay,
powerUpSettings,
});
}
Expand Down Expand Up @@ -924,6 +953,7 @@ export default function SetupWizard({ onComplete, onCancel, initialConfig }) {
crtEnabled={crtEnabled} onToggleCrt={() => setCrtEnabled(v => !v)}
uiScale={uiScale} onChangeUiScale={setUiScale}
animatedBg={animatedBg} onToggleAnimatedBg={() => setAnimatedBg(v => !v)}
weekStartDay={weekStartDay} onChangeWeekStartDay={setWeekStartDay}
/>
)}
</div>
Expand Down Expand Up @@ -999,6 +1029,8 @@ export default function SetupWizard({ onComplete, onCancel, initialConfig }) {
onChangeUiScale={setUiScale}
animatedBg={animatedBg}
onToggleAnimatedBg={() => setAnimatedBg(v => !v)}
weekStartDay={weekStartDay}
onChangeWeekStartDay={setWeekStartDay}
/>
)}
</div>
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/logic.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,10 +202,12 @@ export function todayKey() {
return `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
}

export function weekKey() {
export function weekKey(weekStartDay = 1) {
const d = new Date();
const s = new Date(d);
s.setDate(d.getDate() - d.getDay());
const dow = d.getDay();
const diff = (dow - weekStartDay + 7) % 7;
s.setDate(d.getDate() - diff);
return `${s.getFullYear()}-${s.getMonth()}-${s.getDate()}`;
}

Expand Down
14 changes: 7 additions & 7 deletions questboard/DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,19 @@ A pixel art RPG-themed chore tracker for the whole family. Complete household ch

When you open Questboard for the first time, a setup wizard will guide you through:

1. **Add players** (1–6) enter names and choose difficulty
- **Easy mode** kid-appropriate chores, lower monster HP
- **Hard mode** full adult chore list, tougher monsters
2. **Pick an avatar and class** Knight, Sorceress, Ranger, and more
3. **Choose chores** toggle individual chores on/off, add custom ones
1. **Add players** (1–6) - enter names and choose difficulty
- **Easy mode** - kid-appropriate chores, lower monster HP
- **Hard mode** - full adult chore list, tougher monsters
2. **Pick an avatar and class** - Knight, Sorceress, Ranger, and more
3. **Choose chores** - toggle individual chores on/off, add custom ones
4. **Start your adventure!**

You can return to settings any time to add players, change chores, or adjust difficulty.

## How to Play

- Tap a **player card** to select your hero
- Tap any **chore** to complete it this deals damage to your monster
- Tap any **chore** to complete it - this deals damage to your monster
- Fill the monster's HP bar to zero before midnight to **earn gold**
- If you don't defeat it, the monster **attacks** and you lose gold
- Spend gold in the **Reward Shop** on treats your family has agreed on
Expand All @@ -35,7 +35,7 @@ Each chore has a chance to deal **double damage** (crit). Your base crit chance
| Weekly | Every Sunday |
| Monthly | 1st of each month |

XP and levels never reset they carry over forever.
XP and levels never reset - they carry over forever.

## Support

Expand Down
Loading