generated from nhcarrigan/template
feat: display unlock conditions on all locked items
All locked items now show players exactly what they need to do next: - Adventurers: "π Complete: [Quest Name]" - Upgrades: "βοΈ Defeat: [Boss]" or "π Complete: [Quest]" - Equipment (boss drops): "βοΈ Drop: [Boss Name]" instead of generic label - Bosses: "βοΈ Defeat: [Previous Boss] first" or zone gate boss - Quests: "π Complete: [Prerequisite Quest]" Also wires adventurer unlocks through quests (militia through dragon_rider had no unlock path), retroactively applies rewards on existing saves, syncs boss reward arrays from defaults on load, and removes invalid rune_stone reference from Forest Giant.
This commit is contained in:
@@ -49,7 +49,7 @@ export const DEFAULT_BOSSES: Boss[] = [
|
|||||||
essenceReward: 400,
|
essenceReward: 400,
|
||||||
crystalReward: 20,
|
crystalReward: 20,
|
||||||
upgradeRewards: ["archmage_1"],
|
upgradeRewards: ["archmage_1"],
|
||||||
equipmentRewards: ["hide_armour", "rune_stone"],
|
equipmentRewards: ["hide_armour"],
|
||||||
prestigeRequirement: 0,
|
prestigeRequirement: 0,
|
||||||
zoneId: "verdant_vale",
|
zoneId: "verdant_vale",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ export const DEFAULT_QUESTS: Quest[] = [
|
|||||||
description: "Every legend begins somewhere. Send your first adventurer into the field.",
|
description: "Every legend begins somewhere. Send your first adventurer into the field.",
|
||||||
status: "available",
|
status: "available",
|
||||||
durationSeconds: 60,
|
durationSeconds: 60,
|
||||||
rewards: [{ type: "gold", amount: 500 }],
|
rewards: [
|
||||||
|
{ type: "gold", amount: 500 },
|
||||||
|
{ type: "adventurer", targetId: "militia" },
|
||||||
|
],
|
||||||
prerequisiteIds: [],
|
prerequisiteIds: [],
|
||||||
zoneId: "verdant_vale",
|
zoneId: "verdant_vale",
|
||||||
},
|
},
|
||||||
@@ -21,6 +24,7 @@ export const DEFAULT_QUESTS: Quest[] = [
|
|||||||
rewards: [
|
rewards: [
|
||||||
{ type: "gold", amount: 2_000 },
|
{ type: "gold", amount: 2_000 },
|
||||||
{ type: "essence", amount: 5 },
|
{ type: "essence", amount: 5 },
|
||||||
|
{ type: "adventurer", targetId: "apprentice" },
|
||||||
],
|
],
|
||||||
prerequisiteIds: ["first_steps"],
|
prerequisiteIds: ["first_steps"],
|
||||||
zoneId: "verdant_vale",
|
zoneId: "verdant_vale",
|
||||||
@@ -34,6 +38,7 @@ export const DEFAULT_QUESTS: Quest[] = [
|
|||||||
rewards: [
|
rewards: [
|
||||||
{ type: "crystals", amount: 10 },
|
{ type: "crystals", amount: 10 },
|
||||||
{ type: "upgrade", targetId: "global_1" },
|
{ type: "upgrade", targetId: "global_1" },
|
||||||
|
{ type: "adventurer", targetId: "scout" },
|
||||||
],
|
],
|
||||||
prerequisiteIds: ["goblin_camp"],
|
prerequisiteIds: ["goblin_camp"],
|
||||||
zoneId: "verdant_vale",
|
zoneId: "verdant_vale",
|
||||||
@@ -47,6 +52,7 @@ export const DEFAULT_QUESTS: Quest[] = [
|
|||||||
rewards: [
|
rewards: [
|
||||||
{ type: "essence", amount: 50 },
|
{ type: "essence", amount: 50 },
|
||||||
{ type: "upgrade", targetId: "click_2" },
|
{ type: "upgrade", targetId: "click_2" },
|
||||||
|
{ type: "adventurer", targetId: "acolyte" },
|
||||||
],
|
],
|
||||||
prerequisiteIds: ["haunted_mine"],
|
prerequisiteIds: ["haunted_mine"],
|
||||||
zoneId: "verdant_vale",
|
zoneId: "verdant_vale",
|
||||||
@@ -63,6 +69,7 @@ export const DEFAULT_QUESTS: Quest[] = [
|
|||||||
{ type: "gold", amount: 15_000 },
|
{ type: "gold", amount: 15_000 },
|
||||||
{ type: "essence", amount: 20 },
|
{ type: "essence", amount: 20 },
|
||||||
{ type: "upgrade", targetId: "cleric_1" },
|
{ type: "upgrade", targetId: "cleric_1" },
|
||||||
|
{ type: "adventurer", targetId: "ranger" },
|
||||||
],
|
],
|
||||||
prerequisiteIds: [],
|
prerequisiteIds: [],
|
||||||
zoneId: "shattered_ruins",
|
zoneId: "shattered_ruins",
|
||||||
@@ -78,6 +85,7 @@ export const DEFAULT_QUESTS: Quest[] = [
|
|||||||
{ type: "gold", amount: 80_000 },
|
{ type: "gold", amount: 80_000 },
|
||||||
{ type: "essence", amount: 120 },
|
{ type: "essence", amount: 120 },
|
||||||
{ type: "upgrade", targetId: "scout_1" },
|
{ type: "upgrade", targetId: "scout_1" },
|
||||||
|
{ type: "adventurer", targetId: "knight" },
|
||||||
],
|
],
|
||||||
prerequisiteIds: ["necromancer_tower"],
|
prerequisiteIds: ["necromancer_tower"],
|
||||||
zoneId: "shattered_ruins",
|
zoneId: "shattered_ruins",
|
||||||
@@ -93,6 +101,7 @@ export const DEFAULT_QUESTS: Quest[] = [
|
|||||||
{ type: "essence", amount: 300 },
|
{ type: "essence", amount: 300 },
|
||||||
{ type: "crystals", amount: 30 },
|
{ type: "crystals", amount: 30 },
|
||||||
{ type: "upgrade", targetId: "mage_1" },
|
{ type: "upgrade", targetId: "mage_1" },
|
||||||
|
{ type: "adventurer", targetId: "archmage" },
|
||||||
],
|
],
|
||||||
prerequisiteIds: ["crumbling_fortress"],
|
prerequisiteIds: ["crumbling_fortress"],
|
||||||
zoneId: "shattered_ruins",
|
zoneId: "shattered_ruins",
|
||||||
@@ -107,6 +116,7 @@ export const DEFAULT_QUESTS: Quest[] = [
|
|||||||
rewards: [
|
rewards: [
|
||||||
{ type: "gold", amount: 500_000 },
|
{ type: "gold", amount: 500_000 },
|
||||||
{ type: "crystals", amount: 50 },
|
{ type: "crystals", amount: 50 },
|
||||||
|
{ type: "adventurer", targetId: "paladin" },
|
||||||
{ type: "adventurer", targetId: "dragon_rider" },
|
{ type: "adventurer", targetId: "dragon_rider" },
|
||||||
],
|
],
|
||||||
prerequisiteIds: ["cursed_library"],
|
prerequisiteIds: ["cursed_library"],
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ gameRouter.get("/load", async (context) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backfill equipmentRewards on bosses that predate the field
|
// Backfill equipmentRewards on bosses that predate the field (will be synced below after defaults load)
|
||||||
for (const boss of state.bosses) {
|
for (const boss of state.bosses) {
|
||||||
if (!Array.isArray(boss.equipmentRewards)) {
|
if (!Array.isArray(boss.equipmentRewards)) {
|
||||||
boss.equipmentRewards = [];
|
boss.equipmentRewards = [];
|
||||||
@@ -82,7 +82,7 @@ gameRouter.get("/load", async (context) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync zoneId on quests to match current defaults
|
// Sync zoneId and rewards on quests to match current defaults
|
||||||
for (const quest of state.quests) {
|
for (const quest of state.quests) {
|
||||||
const defaults = DEFAULT_QUESTS.find((d) => d.id === quest.id);
|
const defaults = DEFAULT_QUESTS.find((d) => d.id === quest.id);
|
||||||
if (defaults && quest.zoneId !== defaults.zoneId) {
|
if (defaults && quest.zoneId !== defaults.zoneId) {
|
||||||
@@ -93,6 +93,23 @@ gameRouter.get("/load", async (context) => {
|
|||||||
quest.zoneId = defaults?.zoneId ?? "verdant_vale";
|
quest.zoneId = defaults?.zoneId ?? "verdant_vale";
|
||||||
needsBackfill = true;
|
needsBackfill = true;
|
||||||
}
|
}
|
||||||
|
// Sync rewards to match defaults so newly-added rewards take effect
|
||||||
|
if (defaults && JSON.stringify(quest.rewards) !== JSON.stringify(defaults.rewards)) {
|
||||||
|
quest.rewards = structuredClone(defaults.rewards);
|
||||||
|
needsBackfill = true;
|
||||||
|
}
|
||||||
|
// Retroactively apply adventurer unlocks from already-completed quests
|
||||||
|
if (quest.status === "completed") {
|
||||||
|
for (const reward of quest.rewards) {
|
||||||
|
if (reward.type === "adventurer" && reward.targetId) {
|
||||||
|
const adventurer = state.adventurers.find((a) => a.id === reward.targetId);
|
||||||
|
if (adventurer && !adventurer.unlocked) {
|
||||||
|
adventurer.unlocked = true;
|
||||||
|
needsBackfill = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const defaultUpgrade of DEFAULT_UPGRADES) {
|
for (const defaultUpgrade of DEFAULT_UPGRADES) {
|
||||||
@@ -149,13 +166,24 @@ gameRouter.get("/load", async (context) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backfill zoneId on bosses that predate the field
|
// Backfill zoneId and sync rewards on bosses to match current defaults
|
||||||
for (const boss of state.bosses) {
|
for (const boss of state.bosses) {
|
||||||
|
const defaults = DEFAULT_BOSSES.find((d) => d.id === boss.id);
|
||||||
if (!boss.zoneId) {
|
if (!boss.zoneId) {
|
||||||
const defaults = DEFAULT_BOSSES.find((d) => d.id === boss.id);
|
|
||||||
boss.zoneId = defaults?.zoneId ?? "verdant_vale";
|
boss.zoneId = defaults?.zoneId ?? "verdant_vale";
|
||||||
needsBackfill = true;
|
needsBackfill = true;
|
||||||
}
|
}
|
||||||
|
// Sync equipmentRewards and upgradeRewards to match defaults
|
||||||
|
if (defaults) {
|
||||||
|
if (JSON.stringify(boss.equipmentRewards) !== JSON.stringify(defaults.equipmentRewards)) {
|
||||||
|
boss.equipmentRewards = structuredClone(defaults.equipmentRewards);
|
||||||
|
needsBackfill = true;
|
||||||
|
}
|
||||||
|
if (JSON.stringify(boss.upgradeRewards) !== JSON.stringify(defaults.upgradeRewards)) {
|
||||||
|
boss.upgradeRewards = structuredClone(defaults.upgradeRewards);
|
||||||
|
needsBackfill = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge new bosses from defaults (new zones' bosses)
|
// Merge new bosses from defaults (new zones' bosses)
|
||||||
|
|||||||
@@ -18,9 +18,10 @@ const adventurerCost = (adventurer: Adventurer): number =>
|
|||||||
interface AdventurerCardProps {
|
interface AdventurerCardProps {
|
||||||
adventurer: Adventurer;
|
adventurer: Adventurer;
|
||||||
currentGold: number;
|
currentGold: number;
|
||||||
|
unlockHint?: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AdventurerCard = ({ adventurer, currentGold }: AdventurerCardProps): React.JSX.Element => {
|
const AdventurerCard = ({ adventurer, currentGold, unlockHint }: AdventurerCardProps): React.JSX.Element => {
|
||||||
const { buyAdventurer } = useGame();
|
const { buyAdventurer } = useGame();
|
||||||
const cost = adventurerCost(adventurer);
|
const cost = adventurerCost(adventurer);
|
||||||
const canAfford = currentGold >= cost;
|
const canAfford = currentGold >= cost;
|
||||||
@@ -44,6 +45,9 @@ const AdventurerCard = ({ adventurer, currentGold }: AdventurerCardProps): React
|
|||||||
>
|
>
|
||||||
{adventurer.unlocked ? `πͺ ${cost}` : "π Locked"}
|
{adventurer.unlocked ? `πͺ ${cost}` : "π Locked"}
|
||||||
</button>
|
</button>
|
||||||
|
{!adventurer.unlocked && unlockHint && (
|
||||||
|
<p className="unlock-hint">π Complete: {unlockHint}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -57,6 +61,15 @@ export const AdventurerPanel = (): React.JSX.Element => {
|
|||||||
const locked = state.adventurers.filter((a) => !a.unlocked);
|
const locked = state.adventurers.filter((a) => !a.unlocked);
|
||||||
const visible = showLocked ? state.adventurers : state.adventurers.filter((a) => a.unlocked);
|
const visible = showLocked ? state.adventurers : state.adventurers.filter((a) => a.unlocked);
|
||||||
|
|
||||||
|
const adventurerUnlockHints = new Map<string, string>();
|
||||||
|
for (const quest of state.quests) {
|
||||||
|
for (const reward of quest.rewards) {
|
||||||
|
if (reward.type === "adventurer" && reward.targetId) {
|
||||||
|
adventurerUnlockHints.set(reward.targetId, quest.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="panel adventurer-panel">
|
<section className="panel adventurer-panel">
|
||||||
<div className="panel-header">
|
<div className="panel-header">
|
||||||
@@ -73,6 +86,7 @@ export const AdventurerPanel = (): React.JSX.Element => {
|
|||||||
key={adventurer.id}
|
key={adventurer.id}
|
||||||
adventurer={adventurer}
|
adventurer={adventurer}
|
||||||
currentGold={state.resources.gold}
|
currentGold={state.resources.gold}
|
||||||
|
unlockHint={adventurerUnlockHints.get(adventurer.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ interface BossCardProps {
|
|||||||
prestigeCount: number;
|
prestigeCount: number;
|
||||||
onChallenge: (bossId: string) => void;
|
onChallenge: (bossId: string) => void;
|
||||||
isChallenging: boolean;
|
isChallenging: boolean;
|
||||||
|
unlockHint?: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BossCard = ({
|
const BossCard = ({
|
||||||
@@ -17,6 +18,7 @@ const BossCard = ({
|
|||||||
prestigeCount,
|
prestigeCount,
|
||||||
onChallenge,
|
onChallenge,
|
||||||
isChallenging,
|
isChallenging,
|
||||||
|
unlockHint,
|
||||||
}: BossCardProps): React.JSX.Element => {
|
}: BossCardProps): React.JSX.Element => {
|
||||||
const hpPercent = (boss.currentHp / boss.maxHp) * 100;
|
const hpPercent = (boss.currentHp / boss.maxHp) * 100;
|
||||||
const isPrestigeLocked = boss.prestigeRequirement > prestigeCount;
|
const isPrestigeLocked = boss.prestigeRequirement > prestigeCount;
|
||||||
@@ -33,6 +35,9 @@ const BossCard = ({
|
|||||||
π Requires Prestige {boss.prestigeRequirement}
|
π Requires Prestige {boss.prestigeRequirement}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{!isPrestigeLocked && boss.status === "locked" && unlockHint && (
|
||||||
|
<p className="unlock-hint">{unlockHint}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{boss.status !== "locked" && boss.status !== "defeated" && (
|
{boss.status !== "locked" && boss.status !== "defeated" && (
|
||||||
@@ -146,6 +151,26 @@ export const BossPanel = (): React.JSX.Element => {
|
|||||||
? zoneBosses
|
? zoneBosses
|
||||||
: zoneBosses.filter((b) => b.status !== "locked");
|
: zoneBosses.filter((b) => b.status !== "locked");
|
||||||
|
|
||||||
|
const bossUnlockHints = new Map<string, string>();
|
||||||
|
for (const zone of zones) {
|
||||||
|
const allZoneBosses = state.bosses.filter((b) => b.zoneId === zone.id);
|
||||||
|
for (let i = 0; i < allZoneBosses.length; i++) {
|
||||||
|
const boss = allZoneBosses[i];
|
||||||
|
if (!boss || boss.status !== "locked") continue;
|
||||||
|
if (i === 0 && zone.unlockBossId) {
|
||||||
|
const gateBoss = state.bosses.find((b) => b.id === zone.unlockBossId);
|
||||||
|
if (gateBoss) {
|
||||||
|
bossUnlockHints.set(boss.id, `βοΈ Defeat: ${gateBoss.name}`);
|
||||||
|
}
|
||||||
|
} else if (i > 0) {
|
||||||
|
const prevBoss = allZoneBosses[i - 1];
|
||||||
|
if (prevBoss) {
|
||||||
|
bossUnlockHints.set(boss.id, `βοΈ Defeat: ${prevBoss.name} first`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="panel boss-panel">
|
<section className="panel boss-panel">
|
||||||
<div className="panel-header">
|
<div className="panel-header">
|
||||||
@@ -181,6 +206,7 @@ export const BossPanel = (): React.JSX.Element => {
|
|||||||
boss={boss}
|
boss={boss}
|
||||||
isChallenging={challengingBossId === boss.id}
|
isChallenging={challengingBossId === boss.id}
|
||||||
prestigeCount={state.prestige.count}
|
prestigeCount={state.prestige.count}
|
||||||
|
unlockHint={bossUnlockHints.get(boss.id)}
|
||||||
onChallenge={(id) => {
|
onChallenge={(id) => {
|
||||||
void handleChallenge(id);
|
void handleChallenge(id);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ interface EquipmentCardProps {
|
|||||||
gold: number;
|
gold: number;
|
||||||
essence: number;
|
essence: number;
|
||||||
crystals: number;
|
crystals: number;
|
||||||
|
dropBossName?: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const costLabel = (cost: { gold: number; essence: number; crystals: number }): string => {
|
const costLabel = (cost: { gold: number; essence: number; crystals: number }): string => {
|
||||||
@@ -45,7 +46,7 @@ const costLabel = (cost: { gold: number; essence: number; crystals: number }): s
|
|||||||
return parts.join(" ");
|
return parts.join(" ");
|
||||||
};
|
};
|
||||||
|
|
||||||
const EquipmentCard = ({ item, gold, essence, crystals }: EquipmentCardProps): React.JSX.Element => {
|
const EquipmentCard = ({ item, gold, essence, crystals, dropBossName }: EquipmentCardProps): React.JSX.Element => {
|
||||||
const { equipItem, buyEquipment } = useGame();
|
const { equipItem, buyEquipment } = useGame();
|
||||||
|
|
||||||
const canAfford = item.cost
|
const canAfford = item.cost
|
||||||
@@ -67,7 +68,11 @@ const EquipmentCard = ({ item, gold, essence, crystals }: EquipmentCardProps): R
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="equipment-action">
|
<div className="equipment-action">
|
||||||
{!item.owned && !item.cost && <span className="equipment-locked">π Boss drop</span>}
|
{!item.owned && !item.cost && (
|
||||||
|
<span className="equipment-locked">
|
||||||
|
{dropBossName ? `βοΈ Drop: ${dropBossName}` : "π Boss drop"}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{!item.owned && item.cost && (
|
{!item.owned && item.cost && (
|
||||||
<button
|
<button
|
||||||
className="equip-button"
|
className="equip-button"
|
||||||
@@ -109,6 +114,13 @@ export const EquipmentPanel = (): React.JSX.Element => {
|
|||||||
const equipment = state.equipment ?? [];
|
const equipment = state.equipment ?? [];
|
||||||
const unownedCount = equipment.filter((e) => !e.owned).length;
|
const unownedCount = equipment.filter((e) => !e.owned).length;
|
||||||
|
|
||||||
|
const equipmentDropSources = new Map<string, string>();
|
||||||
|
for (const boss of state.bosses) {
|
||||||
|
for (const equipmentId of (boss.equipmentRewards ?? [])) {
|
||||||
|
equipmentDropSources.set(equipmentId, boss.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="panel equipment-panel">
|
<section className="panel equipment-panel">
|
||||||
<div className="panel-header">
|
<div className="panel-header">
|
||||||
@@ -138,6 +150,7 @@ export const EquipmentPanel = (): React.JSX.Element => {
|
|||||||
gold={state.resources.gold}
|
gold={state.resources.gold}
|
||||||
essence={state.resources.essence}
|
essence={state.resources.essence}
|
||||||
crystals={state.resources.crystals}
|
crystals={state.resources.crystals}
|
||||||
|
dropBossName={equipmentDropSources.get(item.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{items.length === 0 && (
|
{items.length === 0 && (
|
||||||
|
|||||||
@@ -18,9 +18,10 @@ const questTimeRemaining = (quest: Quest): number => {
|
|||||||
|
|
||||||
interface QuestCardProps {
|
interface QuestCardProps {
|
||||||
quest: Quest;
|
quest: Quest;
|
||||||
|
unlockHint?: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QuestCard = ({ quest }: QuestCardProps): React.JSX.Element => {
|
const QuestCard = ({ quest, unlockHint }: QuestCardProps): React.JSX.Element => {
|
||||||
const { startQuest } = useGame();
|
const { startQuest } = useGame();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -42,7 +43,12 @@ const QuestCard = ({ quest }: QuestCardProps): React.JSX.Element => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="quest-action">
|
<div className="quest-action">
|
||||||
{quest.status === "locked" && <span className="quest-badge locked">π Locked</span>}
|
{quest.status === "locked" && (
|
||||||
|
<>
|
||||||
|
<span className="quest-badge locked">π Locked</span>
|
||||||
|
{unlockHint && <p className="unlock-hint">π Complete: {unlockHint}</p>}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{quest.status === "available" && (
|
{quest.status === "available" && (
|
||||||
<button
|
<button
|
||||||
className="start-quest-button"
|
className="start-quest-button"
|
||||||
@@ -77,6 +83,20 @@ export const QuestPanel = (): React.JSX.Element => {
|
|||||||
? zoneQuests
|
? zoneQuests
|
||||||
: zoneQuests.filter((q) => q.status !== "locked");
|
: zoneQuests.filter((q) => q.status !== "locked");
|
||||||
|
|
||||||
|
const questNameById = new Map(state.quests.map((q) => [q.id, q.name]));
|
||||||
|
const questUnlockHints = new Map<string, string>();
|
||||||
|
for (const quest of state.quests) {
|
||||||
|
if (quest.status === "locked" && quest.prerequisiteIds.length > 0) {
|
||||||
|
const prereqId = quest.prerequisiteIds[0];
|
||||||
|
if (prereqId) {
|
||||||
|
const prereqName = questNameById.get(prereqId);
|
||||||
|
if (prereqName) {
|
||||||
|
questUnlockHints.set(quest.id, prereqName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="panel quest-panel">
|
<section className="panel quest-panel">
|
||||||
<div className="panel-header">
|
<div className="panel-header">
|
||||||
@@ -96,7 +116,7 @@ export const QuestPanel = (): React.JSX.Element => {
|
|||||||
|
|
||||||
<div className="quest-list">
|
<div className="quest-list">
|
||||||
{visibleQuests.map((quest) => (
|
{visibleQuests.map((quest) => (
|
||||||
<QuestCard key={quest.id} quest={quest} />
|
<QuestCard key={quest.id} quest={quest} unlockHint={questUnlockHints.get(quest.id)} />
|
||||||
))}
|
))}
|
||||||
{visibleQuests.length === 0 && (
|
{visibleQuests.length === 0 && (
|
||||||
<p className="empty-zone">No quests to show in this zone.</p>
|
<p className="empty-zone">No quests to show in this zone.</p>
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ interface UpgradeCardProps {
|
|||||||
currentGold: number;
|
currentGold: number;
|
||||||
currentEssence: number;
|
currentEssence: number;
|
||||||
currentCrystals: number;
|
currentCrystals: number;
|
||||||
|
unlockHint?: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UpgradeCard = ({ upgrade, currentGold, currentEssence, currentCrystals }: UpgradeCardProps): React.JSX.Element => {
|
const UpgradeCard = ({ upgrade, currentGold, currentEssence, currentCrystals, unlockHint }: UpgradeCardProps): React.JSX.Element => {
|
||||||
const { buyUpgrade } = useGame();
|
const { buyUpgrade } = useGame();
|
||||||
const canAfford =
|
const canAfford =
|
||||||
currentGold >= upgrade.costGold &&
|
currentGold >= upgrade.costGold &&
|
||||||
@@ -31,6 +32,7 @@ const UpgradeCard = ({ upgrade, currentGold, currentEssence, currentCrystals }:
|
|||||||
{(upgrade.costCrystals ?? 0) > 0 && <span>π {upgrade.costCrystals?.toLocaleString()}</span>}
|
{(upgrade.costCrystals ?? 0) > 0 && <span>π {upgrade.costCrystals?.toLocaleString()}</span>}
|
||||||
</div>
|
</div>
|
||||||
<span className="upgrade-locked-label">Locked</span>
|
<span className="upgrade-locked-label">Locked</span>
|
||||||
|
{unlockHint && <p className="unlock-hint">{unlockHint}</p>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -78,6 +80,20 @@ export const UpgradePanel = (): React.JSX.Element => {
|
|||||||
const available = state.upgrades.filter((u) => u.unlocked && !u.purchased);
|
const available = state.upgrades.filter((u) => u.unlocked && !u.purchased);
|
||||||
const locked = state.upgrades.filter((u) => !u.unlocked);
|
const locked = state.upgrades.filter((u) => !u.unlocked);
|
||||||
|
|
||||||
|
const upgradeUnlockHints = new Map<string, string>();
|
||||||
|
for (const boss of state.bosses) {
|
||||||
|
for (const upgradeId of (boss.upgradeRewards ?? [])) {
|
||||||
|
upgradeUnlockHints.set(upgradeId, `βοΈ Defeat: ${boss.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const quest of state.quests) {
|
||||||
|
for (const reward of quest.rewards) {
|
||||||
|
if (reward.type === "upgrade" && reward.targetId && !upgradeUnlockHints.has(reward.targetId)) {
|
||||||
|
upgradeUnlockHints.set(reward.targetId, `π Complete: ${quest.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="panel upgrade-panel">
|
<section className="panel upgrade-panel">
|
||||||
<div className="panel-header">
|
<div className="panel-header">
|
||||||
@@ -118,6 +134,7 @@ export const UpgradePanel = (): React.JSX.Element => {
|
|||||||
currentGold={state.resources.gold}
|
currentGold={state.resources.gold}
|
||||||
currentEssence={state.resources.essence}
|
currentEssence={state.resources.essence}
|
||||||
currentCrystals={state.resources.crystals}
|
currentCrystals={state.resources.crystals}
|
||||||
|
unlockHint={upgradeUnlockHints.get(upgrade.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -943,6 +943,13 @@ body {
|
|||||||
margin-top: 0.2rem;
|
margin-top: 0.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.unlock-hint {
|
||||||
|
color: var(--colour-text-muted);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-style: italic;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.equipment-equipped-badge {
|
.equipment-equipped-badge {
|
||||||
color: var(--colour-success);
|
color: var(--colour-success);
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user