generated from nhcarrigan/template
feat: add zone system to bosses and quests
This commit is contained in:
@@ -16,6 +16,7 @@ export const DEFAULT_BOSSES: Boss[] = [
|
|||||||
upgradeRewards: ["click_2"],
|
upgradeRewards: ["click_2"],
|
||||||
equipmentRewards: ["iron_sword", "chainmail", "mages_focus"],
|
equipmentRewards: ["iron_sword", "chainmail", "mages_focus"],
|
||||||
prestigeRequirement: 0,
|
prestigeRequirement: 0,
|
||||||
|
zoneId: "verdant_vale",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "lich_queen",
|
id: "lich_queen",
|
||||||
@@ -32,6 +33,7 @@ export const DEFAULT_BOSSES: Boss[] = [
|
|||||||
upgradeRewards: ["global_2"],
|
upgradeRewards: ["global_2"],
|
||||||
equipmentRewards: ["enchanted_blade", "plate_armour", "arcane_orb"],
|
equipmentRewards: ["enchanted_blade", "plate_armour", "arcane_orb"],
|
||||||
prestigeRequirement: 0,
|
prestigeRequirement: 0,
|
||||||
|
zoneId: "verdant_vale",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "elder_dragon",
|
id: "elder_dragon",
|
||||||
@@ -48,6 +50,7 @@ export const DEFAULT_BOSSES: Boss[] = [
|
|||||||
upgradeRewards: ["click_3"],
|
upgradeRewards: ["click_3"],
|
||||||
equipmentRewards: ["vorpal_sword", "dragon_scale"],
|
equipmentRewards: ["vorpal_sword", "dragon_scale"],
|
||||||
prestigeRequirement: 1,
|
prestigeRequirement: 1,
|
||||||
|
zoneId: "shattered_ruins",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "void_titan",
|
id: "void_titan",
|
||||||
@@ -64,5 +67,6 @@ export const DEFAULT_BOSSES: Boss[] = [
|
|||||||
upgradeRewards: [],
|
upgradeRewards: [],
|
||||||
equipmentRewards: ["philosophers_stone"],
|
equipmentRewards: ["philosophers_stone"],
|
||||||
prestigeRequirement: 3,
|
prestigeRequirement: 3,
|
||||||
|
zoneId: "frozen_peaks",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { DEFAULT_BOSSES } from "./bosses.js";
|
|||||||
import { DEFAULT_EQUIPMENT } from "./equipment.js";
|
import { DEFAULT_EQUIPMENT } from "./equipment.js";
|
||||||
import { DEFAULT_QUESTS } from "./quests.js";
|
import { DEFAULT_QUESTS } from "./quests.js";
|
||||||
import { DEFAULT_UPGRADES } from "./upgrades.js";
|
import { DEFAULT_UPGRADES } from "./upgrades.js";
|
||||||
|
import { DEFAULT_ZONES } from "./zones.js";
|
||||||
|
|
||||||
export const INITIAL_PRESTIGE: PrestigeData = {
|
export const INITIAL_PRESTIGE: PrestigeData = {
|
||||||
count: 0,
|
count: 0,
|
||||||
@@ -33,6 +34,7 @@ export const INITIAL_GAME_STATE = (player: Player, characterName: string): GameS
|
|||||||
equipment: structuredClone(DEFAULT_EQUIPMENT),
|
equipment: structuredClone(DEFAULT_EQUIPMENT),
|
||||||
achievements: structuredClone(DEFAULT_ACHIEVEMENTS),
|
achievements: structuredClone(DEFAULT_ACHIEVEMENTS),
|
||||||
prestige: INITIAL_PRESTIGE,
|
prestige: INITIAL_PRESTIGE,
|
||||||
|
zones: structuredClone(DEFAULT_ZONES),
|
||||||
baseClickPower: 1,
|
baseClickPower: 1,
|
||||||
lastTickAt: Date.now(),
|
lastTickAt: Date.now(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export const DEFAULT_QUESTS: Quest[] = [
|
|||||||
durationSeconds: 60,
|
durationSeconds: 60,
|
||||||
rewards: [{ type: "gold", amount: 500 }],
|
rewards: [{ type: "gold", amount: 500 }],
|
||||||
prerequisiteIds: [],
|
prerequisiteIds: [],
|
||||||
|
zoneId: "verdant_vale",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "goblin_camp",
|
id: "goblin_camp",
|
||||||
@@ -21,6 +22,7 @@ export const DEFAULT_QUESTS: Quest[] = [
|
|||||||
{ type: "essence", amount: 5 },
|
{ type: "essence", amount: 5 },
|
||||||
],
|
],
|
||||||
prerequisiteIds: ["first_steps"],
|
prerequisiteIds: ["first_steps"],
|
||||||
|
zoneId: "verdant_vale",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "haunted_mine",
|
id: "haunted_mine",
|
||||||
@@ -33,6 +35,7 @@ export const DEFAULT_QUESTS: Quest[] = [
|
|||||||
{ type: "upgrade", targetId: "global_1" },
|
{ type: "upgrade", targetId: "global_1" },
|
||||||
],
|
],
|
||||||
prerequisiteIds: ["goblin_camp"],
|
prerequisiteIds: ["goblin_camp"],
|
||||||
|
zoneId: "verdant_vale",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "necromancer_tower",
|
id: "necromancer_tower",
|
||||||
@@ -47,6 +50,7 @@ export const DEFAULT_QUESTS: Quest[] = [
|
|||||||
{ type: "upgrade", targetId: "cleric_1" },
|
{ type: "upgrade", targetId: "cleric_1" },
|
||||||
],
|
],
|
||||||
prerequisiteIds: ["haunted_mine"],
|
prerequisiteIds: ["haunted_mine"],
|
||||||
|
zoneId: "verdant_vale",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "ancient_ruins",
|
id: "ancient_ruins",
|
||||||
@@ -59,6 +63,7 @@ export const DEFAULT_QUESTS: Quest[] = [
|
|||||||
{ type: "upgrade", targetId: "click_2" },
|
{ type: "upgrade", targetId: "click_2" },
|
||||||
],
|
],
|
||||||
prerequisiteIds: ["haunted_mine"],
|
prerequisiteIds: ["haunted_mine"],
|
||||||
|
zoneId: "verdant_vale",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "shadow_mere",
|
id: "shadow_mere",
|
||||||
@@ -72,6 +77,7 @@ export const DEFAULT_QUESTS: Quest[] = [
|
|||||||
{ type: "upgrade", targetId: "scout_1" },
|
{ type: "upgrade", targetId: "scout_1" },
|
||||||
],
|
],
|
||||||
prerequisiteIds: ["ancient_ruins"],
|
prerequisiteIds: ["ancient_ruins"],
|
||||||
|
zoneId: "shattered_ruins",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "dragon_lair",
|
id: "dragon_lair",
|
||||||
@@ -86,6 +92,7 @@ export const DEFAULT_QUESTS: Quest[] = [
|
|||||||
{ type: "adventurer", targetId: "dragon_rider" },
|
{ type: "adventurer", targetId: "dragon_rider" },
|
||||||
],
|
],
|
||||||
prerequisiteIds: ["ancient_ruins"],
|
prerequisiteIds: ["ancient_ruins"],
|
||||||
|
zoneId: "shattered_ruins",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "frozen_wastes",
|
id: "frozen_wastes",
|
||||||
@@ -100,6 +107,7 @@ export const DEFAULT_QUESTS: Quest[] = [
|
|||||||
{ type: "upgrade", targetId: "global_3" },
|
{ type: "upgrade", targetId: "global_3" },
|
||||||
],
|
],
|
||||||
prerequisiteIds: ["dragon_lair"],
|
prerequisiteIds: ["dragon_lair"],
|
||||||
|
zoneId: "frozen_peaks",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "void_rift",
|
id: "void_rift",
|
||||||
@@ -114,5 +122,6 @@ export const DEFAULT_QUESTS: Quest[] = [
|
|||||||
{ type: "upgrade", targetId: "knight_1" },
|
{ type: "upgrade", targetId: "knight_1" },
|
||||||
],
|
],
|
||||||
prerequisiteIds: ["frozen_wastes"],
|
prerequisiteIds: ["frozen_wastes"],
|
||||||
|
zoneId: "frozen_peaks",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import type { Zone } from "@elysium/types";
|
||||||
|
|
||||||
|
export const DEFAULT_ZONES: Zone[] = [
|
||||||
|
{
|
||||||
|
id: "verdant_vale",
|
||||||
|
name: "The Verdant Vale",
|
||||||
|
description:
|
||||||
|
"Rolling green hills and ancient forests stretch to the horizon. This is where your guild takes its first steps — trade roads in need of clearing, goblin camps to rout, and an undead queen stirring in the north.",
|
||||||
|
emoji: "🌿",
|
||||||
|
status: "unlocked",
|
||||||
|
unlockBossId: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "shattered_ruins",
|
||||||
|
name: "The Shattered Ruins",
|
||||||
|
description:
|
||||||
|
"The remnants of a civilisation long lost to war and dragonfire. Crumbling towers and cursed lakes hide treasures — and an elder dragon who claims these lands as his own.",
|
||||||
|
emoji: "🏛️",
|
||||||
|
status: "locked",
|
||||||
|
unlockBossId: "lich_queen",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "frozen_peaks",
|
||||||
|
name: "The Frozen Peaks",
|
||||||
|
description:
|
||||||
|
"At the edge of the world, where the sun barely rises and the cold is a living thing, a tear in reality has drawn something ancient and terrible. Only the mightiest guilds dare tread here.",
|
||||||
|
emoji: "❄️",
|
||||||
|
status: "locked",
|
||||||
|
unlockBossId: "elder_dragon",
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -152,6 +152,13 @@ bossRouter.post("/challenge", async (context) => {
|
|||||||
nextBoss.status = "available";
|
nextBoss.status = "available";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unlock any zone whose unlock condition is this boss
|
||||||
|
for (const zone of (state.zones ?? [])) {
|
||||||
|
if (zone.unlockBossId === body.bossId) {
|
||||||
|
zone.status = "unlocked";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
rewards = {
|
rewards = {
|
||||||
gold: boss.goldReward,
|
gold: boss.goldReward,
|
||||||
essence: boss.essenceReward,
|
essence: boss.essenceReward,
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ gameRouter.get("/load", async (context) => {
|
|||||||
// Backfill new quests and upgrades from defaults (add missing ones)
|
// Backfill new quests and upgrades from defaults (add missing ones)
|
||||||
const { DEFAULT_QUESTS } = await import("../data/quests.js");
|
const { DEFAULT_QUESTS } = await import("../data/quests.js");
|
||||||
const { DEFAULT_UPGRADES } = await import("../data/upgrades.js");
|
const { DEFAULT_UPGRADES } = await import("../data/upgrades.js");
|
||||||
|
const { DEFAULT_ZONES } = await import("../data/zones.js");
|
||||||
|
const { DEFAULT_BOSSES } = await import("../data/bosses.js");
|
||||||
|
|
||||||
for (const defaultQuest of DEFAULT_QUESTS) {
|
for (const defaultQuest of DEFAULT_QUESTS) {
|
||||||
if (!state.quests.some((q) => q.id === defaultQuest.id)) {
|
if (!state.quests.some((q) => q.id === defaultQuest.id)) {
|
||||||
@@ -80,6 +82,15 @@ gameRouter.get("/load", async (context) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Backfill zoneId on quests that predate the field
|
||||||
|
for (const quest of state.quests) {
|
||||||
|
if (!quest.zoneId) {
|
||||||
|
const defaults = DEFAULT_QUESTS.find((d) => d.id === quest.id);
|
||||||
|
quest.zoneId = defaults?.zoneId ?? "verdant_vale";
|
||||||
|
needsBackfill = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const defaultUpgrade of DEFAULT_UPGRADES) {
|
for (const defaultUpgrade of DEFAULT_UPGRADES) {
|
||||||
if (!state.upgrades.some((u) => u.id === defaultUpgrade.id)) {
|
if (!state.upgrades.some((u) => u.id === defaultUpgrade.id)) {
|
||||||
state.upgrades.push(structuredClone(defaultUpgrade));
|
state.upgrades.push(structuredClone(defaultUpgrade));
|
||||||
@@ -87,6 +98,30 @@ gameRouter.get("/load", async (context) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Backfill zones on saves that predate the feature
|
||||||
|
if (!Array.isArray(state.zones) || state.zones.length === 0) {
|
||||||
|
state.zones = structuredClone(DEFAULT_ZONES);
|
||||||
|
// Infer unlock state from defeated bosses
|
||||||
|
for (const zone of state.zones) {
|
||||||
|
if (zone.unlockBossId != null) {
|
||||||
|
const unlockBoss = state.bosses.find((b) => b.id === zone.unlockBossId);
|
||||||
|
if (unlockBoss?.status === "defeated") {
|
||||||
|
zone.status = "unlocked";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
needsBackfill = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backfill zoneId on bosses that predate the field
|
||||||
|
for (const boss of state.bosses) {
|
||||||
|
if (!boss.zoneId) {
|
||||||
|
const defaults = DEFAULT_BOSSES.find((d) => d.id === boss.id);
|
||||||
|
boss.zoneId = defaults?.zoneId ?? "verdant_vale";
|
||||||
|
needsBackfill = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
const { offlineGold, offlineSeconds } = calculateOfflineGold(state, now);
|
const { offlineGold, offlineSeconds } = calculateOfflineGold(state, now);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { Boss } from "@elysium/types";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useGame } from "../../context/GameContext.js";
|
import { useGame } from "../../context/GameContext.js";
|
||||||
import { formatNumber } from "../../utils/format.js";
|
import { formatNumber } from "../../utils/format.js";
|
||||||
|
import { ZoneSelector } from "./ZoneSelector.js";
|
||||||
|
|
||||||
interface BossCardProps {
|
interface BossCardProps {
|
||||||
boss: Boss;
|
boss: Boss;
|
||||||
@@ -17,7 +18,7 @@ const BossCard = ({
|
|||||||
isChallenging,
|
isChallenging,
|
||||||
}: BossCardProps): React.JSX.Element => {
|
}: BossCardProps): React.JSX.Element => {
|
||||||
const hpPercent = (boss.currentHp / boss.maxHp) * 100;
|
const hpPercent = (boss.currentHp / boss.maxHp) * 100;
|
||||||
const isLocked = boss.prestigeRequirement > prestigeCount;
|
const isPrestigeLocked = boss.prestigeRequirement > prestigeCount;
|
||||||
const canChallenge =
|
const canChallenge =
|
||||||
(boss.status === "available" || boss.status === "in_progress") && !isChallenging;
|
(boss.status === "available" || boss.status === "in_progress") && !isChallenging;
|
||||||
|
|
||||||
@@ -26,7 +27,7 @@ const BossCard = ({
|
|||||||
<div className="boss-info">
|
<div className="boss-info">
|
||||||
<h3>{boss.name}</h3>
|
<h3>{boss.name}</h3>
|
||||||
<p>{boss.description}</p>
|
<p>{boss.description}</p>
|
||||||
{isLocked && boss.status === "locked" && (
|
{isPrestigeLocked && boss.status === "locked" && (
|
||||||
<p className="prestige-lock">
|
<p className="prestige-lock">
|
||||||
🔒 Requires Prestige {boss.prestigeRequirement}
|
🔒 Requires Prestige {boss.prestigeRequirement}
|
||||||
</p>
|
</p>
|
||||||
@@ -87,6 +88,7 @@ const BossCard = ({
|
|||||||
export const BossPanel = (): React.JSX.Element => {
|
export const BossPanel = (): React.JSX.Element => {
|
||||||
const { state, challengeBoss } = useGame();
|
const { state, challengeBoss } = useGame();
|
||||||
const [challengingBossId, setChallengingBossId] = useState<string | null>(null);
|
const [challengingBossId, setChallengingBossId] = useState<string | null>(null);
|
||||||
|
const [activeZoneId, setActiveZoneId] = useState("verdant_vale");
|
||||||
|
|
||||||
if (!state) return <section className="panel"><p>Loading...</p></section>;
|
if (!state) return <section className="panel"><p>Loading...</p></section>;
|
||||||
|
|
||||||
@@ -135,10 +137,19 @@ export const BossPanel = (): React.JSX.Element => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const zones = state.zones ?? [];
|
||||||
|
const zoneBosses = state.bosses.filter((b) => b.zoneId === activeZoneId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="panel boss-panel">
|
<section className="panel boss-panel">
|
||||||
<h2>Boss Encounters</h2>
|
<h2>Boss Encounters</h2>
|
||||||
|
|
||||||
|
<ZoneSelector
|
||||||
|
activeZoneId={activeZoneId}
|
||||||
|
zones={zones}
|
||||||
|
onSelectZone={setActiveZoneId}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="party-combat-stats">
|
<div className="party-combat-stats">
|
||||||
<div className="combat-stat">
|
<div className="combat-stat">
|
||||||
<span className="stat-label">⚔️ Party DPS</span>
|
<span className="stat-label">⚔️ Party DPS</span>
|
||||||
@@ -151,7 +162,7 @@ export const BossPanel = (): React.JSX.Element => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="boss-list">
|
<div className="boss-list">
|
||||||
{state.bosses.map((boss) => (
|
{zoneBosses.map((boss) => (
|
||||||
<BossCard
|
<BossCard
|
||||||
key={boss.id}
|
key={boss.id}
|
||||||
boss={boss}
|
boss={boss}
|
||||||
@@ -162,6 +173,9 @@ export const BossPanel = (): React.JSX.Element => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
{zoneBosses.length === 0 && (
|
||||||
|
<p className="empty-zone">No bosses in this zone yet.</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { Quest } from "@elysium/types";
|
import type { Quest } from "@elysium/types";
|
||||||
|
import { useState } from "react";
|
||||||
import { useGame } from "../../context/GameContext.js";
|
import { useGame } from "../../context/GameContext.js";
|
||||||
|
import { ZoneSelector } from "./ZoneSelector.js";
|
||||||
|
|
||||||
const formatDuration = (seconds: number): string => {
|
const formatDuration = (seconds: number): string => {
|
||||||
if (seconds >= 3600) return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
|
if (seconds >= 3600) return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
|
||||||
@@ -62,16 +64,30 @@ const QuestCard = ({ quest }: QuestCardProps): React.JSX.Element => {
|
|||||||
|
|
||||||
export const QuestPanel = (): React.JSX.Element => {
|
export const QuestPanel = (): React.JSX.Element => {
|
||||||
const { state } = useGame();
|
const { state } = useGame();
|
||||||
|
const [activeZoneId, setActiveZoneId] = useState("verdant_vale");
|
||||||
|
|
||||||
if (!state) return <section className="panel"><p>Loading...</p></section>;
|
if (!state) return <section className="panel"><p>Loading...</p></section>;
|
||||||
|
|
||||||
|
const zones = state.zones ?? [];
|
||||||
|
const zoneQuests = state.quests.filter((q) => q.zoneId === activeZoneId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="panel quest-panel">
|
<section className="panel quest-panel">
|
||||||
<h2>Quests</h2>
|
<h2>Quests</h2>
|
||||||
|
|
||||||
|
<ZoneSelector
|
||||||
|
activeZoneId={activeZoneId}
|
||||||
|
zones={zones}
|
||||||
|
onSelectZone={setActiveZoneId}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="quest-list">
|
<div className="quest-list">
|
||||||
{state.quests.map((quest) => (
|
{zoneQuests.map((quest) => (
|
||||||
<QuestCard key={quest.id} quest={quest} />
|
<QuestCard key={quest.id} quest={quest} />
|
||||||
))}
|
))}
|
||||||
|
{zoneQuests.length === 0 && (
|
||||||
|
<p className="empty-zone">No quests in this zone yet.</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type { Zone } from "@elysium/types";
|
||||||
|
|
||||||
|
interface ZoneSelectorProps {
|
||||||
|
zones: Zone[];
|
||||||
|
activeZoneId: string;
|
||||||
|
onSelectZone: (zoneId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ZoneSelector = ({
|
||||||
|
zones,
|
||||||
|
activeZoneId,
|
||||||
|
onSelectZone,
|
||||||
|
}: ZoneSelectorProps): React.JSX.Element => (
|
||||||
|
<div className="zone-selector">
|
||||||
|
{zones.map((zone) => (
|
||||||
|
<button
|
||||||
|
key={zone.id}
|
||||||
|
className={`zone-tab ${zone.id === activeZoneId ? "zone-tab-active" : ""} ${zone.status === "locked" ? "zone-tab-locked" : ""}`}
|
||||||
|
disabled={zone.status === "locked"}
|
||||||
|
onClick={() => {
|
||||||
|
onSelectZone(zone.id);
|
||||||
|
}}
|
||||||
|
title={zone.status === "locked" ? `Unlock by defeating ${zone.unlockBossId?.replace(/_/g, " ") ?? "the previous boss"}` : zone.description}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span className="zone-emoji">{zone.emoji}</span>
|
||||||
|
<span className="zone-name">{zone.name}</span>
|
||||||
|
{zone.status === "locked" && <span className="zone-lock">🔒</span>}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
@@ -1107,6 +1107,73 @@ body {
|
|||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Zone Selector ─────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.zone-selector {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zone-tab {
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(147, 51, 234, 0.3);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
color: var(--colour-text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
gap: 0.2rem;
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zone-tab:hover:not(:disabled) {
|
||||||
|
background: rgba(147, 51, 234, 0.15);
|
||||||
|
border-color: rgba(147, 51, 234, 0.6);
|
||||||
|
color: var(--colour-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zone-tab-active {
|
||||||
|
background: rgba(147, 51, 234, 0.25);
|
||||||
|
border-color: var(--colour-primary);
|
||||||
|
color: var(--colour-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zone-tab-locked {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zone-emoji {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zone-name {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zone-lock {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-zone {
|
||||||
|
color: var(--colour-text-muted);
|
||||||
|
font-style: italic;
|
||||||
|
padding: 1rem 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Loading / Error screens ───────────────────────────────────────────── */
|
||||||
|
|
||||||
.loading-screen,
|
.loading-screen,
|
||||||
.error-screen {
|
.error-screen {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -38,3 +38,4 @@ export type {
|
|||||||
Upgrade,
|
Upgrade,
|
||||||
UpgradeTarget,
|
UpgradeTarget,
|
||||||
} from "./interfaces/Upgrade.js";
|
} from "./interfaces/Upgrade.js";
|
||||||
|
export type { Zone, ZoneStatus } from "./interfaces/Zone.js";
|
||||||
|
|||||||
@@ -21,4 +21,6 @@ export interface Boss {
|
|||||||
equipmentRewards: string[];
|
equipmentRewards: string[];
|
||||||
/** Minimum prestige level required to access this boss */
|
/** Minimum prestige level required to access this boss */
|
||||||
prestigeRequirement: number;
|
prestigeRequirement: number;
|
||||||
|
/** Zone this boss belongs to */
|
||||||
|
zoneId: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type { PrestigeData } from "./Prestige.js";
|
|||||||
import type { Quest } from "./Quest.js";
|
import type { Quest } from "./Quest.js";
|
||||||
import type { Resource } from "./Resource.js";
|
import type { Resource } from "./Resource.js";
|
||||||
import type { Upgrade } from "./Upgrade.js";
|
import type { Upgrade } from "./Upgrade.js";
|
||||||
|
import type { Zone } from "./Zone.js";
|
||||||
|
|
||||||
export interface GameState {
|
export interface GameState {
|
||||||
player: Player;
|
player: Player;
|
||||||
@@ -18,6 +19,7 @@ export interface GameState {
|
|||||||
equipment: Equipment[];
|
equipment: Equipment[];
|
||||||
achievements: Achievement[];
|
achievements: Achievement[];
|
||||||
prestige: PrestigeData;
|
prestige: PrestigeData;
|
||||||
|
zones: Zone[];
|
||||||
/** Click power (gold per click, before upgrades) */
|
/** Click power (gold per click, before upgrades) */
|
||||||
baseClickPower: number;
|
baseClickPower: number;
|
||||||
/** Unix timestamp of the last client-side tick */
|
/** Unix timestamp of the last client-side tick */
|
||||||
|
|||||||
@@ -21,4 +21,6 @@ export interface Quest {
|
|||||||
rewards: QuestReward[];
|
rewards: QuestReward[];
|
||||||
/** IDs of quests that must be completed before this one unlocks */
|
/** IDs of quests that must be completed before this one unlocks */
|
||||||
prerequisiteIds: string[];
|
prerequisiteIds: string[];
|
||||||
|
/** Zone this quest belongs to */
|
||||||
|
zoneId: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export type ZoneStatus = "locked" | "unlocked";
|
||||||
|
|
||||||
|
export interface Zone {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
emoji: string;
|
||||||
|
status: ZoneStatus;
|
||||||
|
/** Boss ID whose defeat unlocks this zone (null for the starter zone) */
|
||||||
|
unlockBossId: string | null;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user