feat: add equipment, achievements, and visual polish

- Equipment system: 12 items across weapon/armour/trinket slots with
  common/rare/epic/legendary rarities; starter commons auto-equipped,
  higher tiers drop from boss victories
- Achievement system: 15 milestones with typed conditions; checked
  each tick and crystal rewards applied automatically
- Achievement toast: slide-in notification, auto-dismisses after 4s
- Floating click text: +X gold floats on each manual click
- Expanded quests (9 total) and upgrades (12 total)
- Upgrade panel now shows locked upgrades so players can see their
  progression path
- formatNumber utility (K/M/B/T) used consistently across all panels
- Backfill logic for existing saves to add new content gracefully
- types package now emits .d.ts declarations
This commit is contained in:
2026-03-06 13:27:48 -08:00
committed by Naomi Carrigan
parent a3daed1683
commit e9e0df31fd
33 changed files with 2066 additions and 133 deletions
+485
View File
@@ -291,6 +291,22 @@ body {
opacity: 0.7;
}
.upgrade-card.locked {
opacity: 0.45;
}
.upgrade-locked-label {
color: var(--colour-text-muted);
font-size: 0.75rem;
white-space: nowrap;
}
.upgrade-progress {
color: var(--colour-text-muted);
font-size: 0.85rem;
margin-bottom: 0.75rem;
}
.upgrade-info {
flex: 1;
}
@@ -611,6 +627,475 @@ body {
background: var(--colour-accent-light);
}
/* ===================== BATTLE MODAL ===================== */
.battle-modal {
max-width: 520px;
}
.battle-stats {
align-items: center;
display: flex;
gap: 1rem;
justify-content: center;
margin-bottom: 1.5rem;
}
.battle-stat {
background: var(--colour-bg);
border: 1px solid var(--colour-border);
border-radius: var(--radius);
display: flex;
flex-direction: column;
gap: 0.25rem;
min-width: 120px;
padding: 0.5rem 0.75rem;
text-align: center;
}
.battle-stat .stat-label {
color: var(--colour-text-muted);
font-size: 0.75rem;
text-transform: uppercase;
}
.battle-stat .stat-value {
color: var(--colour-accent-light);
font-size: 1.1rem;
font-weight: 700;
}
.battle-stat-divider {
color: var(--colour-text-muted);
font-size: 0.85rem;
}
.battle-bars {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 1.25rem;
}
.battle-bar-row {
align-items: center;
display: flex;
gap: 0.5rem;
}
.battle-bar-row .bar-label {
font-size: 0.85rem;
min-width: 100px;
text-align: left;
}
.hp-bar-container {
background: var(--colour-bg);
border: 1px solid var(--colour-border);
border-radius: 4px;
flex: 1;
height: 14px;
overflow: hidden;
}
.hp-bar-fill {
border-radius: 4px;
height: 100%;
}
.battle-bar-row .bar-hp {
color: var(--colour-text-muted);
font-size: 0.75rem;
min-width: 80px;
text-align: right;
}
.vs-divider {
color: var(--colour-text-muted);
font-size: 0.85rem;
text-align: center;
}
.battle-in-progress {
color: var(--colour-text-muted);
font-style: italic;
}
.battle-outcome {
border-radius: var(--radius);
margin-top: 1rem;
padding: 1rem;
}
.battle-outcome.victory {
background: rgba(39, 174, 96, 0.1);
border: 1px solid #27ae60;
}
.battle-outcome.defeat {
background: rgba(231, 76, 60, 0.1);
border: 1px solid #e74c3c;
}
.battle-outcome h3 {
font-size: 1.2rem;
margin-bottom: 0.5rem;
}
.battle-rewards,
.battle-casualties {
display: flex;
flex-direction: column;
font-size: 0.9rem;
gap: 0.2rem;
margin: 0.5rem 0;
}
.dismiss-button {
background: var(--colour-accent);
border: none;
border-radius: var(--radius);
color: #fff;
cursor: pointer;
font-size: 1rem;
font-weight: 700;
margin-top: 0.75rem;
padding: 0.5rem 2rem;
transition: background 0.15s;
}
.dismiss-button:hover {
background: var(--colour-accent-light);
}
/* Party combat stat bar in BossPanel */
.party-combat-stats {
background: var(--colour-bg);
border: 1px solid var(--colour-border);
border-radius: var(--radius);
display: flex;
gap: 2rem;
justify-content: center;
margin-bottom: 1.25rem;
padding: 0.75rem 1rem;
}
.combat-stat {
display: flex;
flex-direction: column;
gap: 0.2rem;
text-align: center;
}
.combat-stat .stat-label {
color: var(--colour-text-muted);
font-size: 0.75rem;
text-transform: uppercase;
}
.combat-stat .stat-value {
color: var(--colour-accent-light);
font-size: 1rem;
font-weight: 700;
}
.boss-meta {
color: var(--colour-text-muted);
font-size: 0.8rem;
margin-bottom: 0.5rem;
}
/* ===================== CLICK FLOAT ===================== */
@keyframes float-up {
0% { opacity: 1; transform: translate(-50%, 0); }
100% { opacity: 0; transform: translate(-50%, -70px); }
}
.click-button-wrapper {
position: relative;
display: inline-block;
}
.click-float {
animation: float-up 0.9s ease-out forwards;
color: var(--colour-gold);
font-size: 1rem;
font-weight: 700;
pointer-events: none;
position: absolute;
text-shadow: 0 1px 4px rgba(0,0,0,0.5);
user-select: none;
}
/* ===================== EQUIPMENT ===================== */
.equipment-intro {
color: var(--colour-text-muted);
font-size: 0.85rem;
margin-bottom: 1.25rem;
}
.equipment-slot-section {
margin-bottom: 1.5rem;
}
.slot-heading {
color: var(--colour-text-muted);
font-size: 0.9rem;
font-weight: 600;
letter-spacing: 0.05em;
margin-bottom: 0.5rem;
text-transform: uppercase;
}
.equipment-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.equipment-card {
align-items: center;
background: var(--colour-surface);
border: 1px solid var(--colour-border);
border-left: 3px solid var(--colour-border);
border-radius: var(--radius);
display: flex;
gap: 0.75rem;
padding: 0.75rem;
transition: border-color 0.15s;
}
.equipment-card.equipped {
border-color: var(--colour-success);
border-left-color: var(--colour-success);
box-shadow: 0 0 8px rgba(16, 185, 129, 0.15);
}
.equipment-card.not-owned {
opacity: 0.45;
}
/* Rarity border-left colours */
.equipment-card.rarity-common { border-left-color: #9ca3af; }
.equipment-card.rarity-rare { border-left-color: #3b82f6; }
.equipment-card.rarity-epic { border-left-color: #a855f7; }
.equipment-card.rarity-legendary { border-left-color: #f59e0b; }
.equipment-icon {
font-size: 1.5rem;
min-width: 2rem;
text-align: center;
}
.equipment-info {
flex: 1;
min-width: 0;
}
.equipment-name-row {
align-items: center;
display: flex;
gap: 0.5rem;
margin-bottom: 0.15rem;
}
.equipment-name-row h3 {
font-size: 0.95rem;
margin: 0;
}
.rarity-badge {
border-radius: 999px;
font-size: 0.7rem;
font-weight: 600;
padding: 0.1rem 0.45rem;
}
.rarity-badge.rarity-common { background: rgba(156, 163, 175, 0.2); color: #9ca3af; }
.rarity-badge.rarity-rare { background: rgba(59, 130, 246, 0.2); color: #60a5fa; }
.rarity-badge.rarity-epic { background: rgba(168, 85, 247, 0.2); color: #c084fc; }
.rarity-badge.rarity-legendary { background: rgba(245, 158, 11, 0.2); color: #fbbf24; }
.equipment-description {
color: var(--colour-text-muted);
font-size: 0.8rem;
margin-bottom: 0.2rem;
}
.equipment-bonus {
color: var(--colour-gold);
font-size: 0.8rem;
font-weight: 600;
}
.equipment-action {
flex-shrink: 0;
text-align: right;
}
.equipment-locked {
color: var(--colour-text-muted);
font-size: 0.8rem;
}
.equipment-equipped-badge {
color: var(--colour-success);
font-size: 0.85rem;
font-weight: 600;
}
.equip-button {
background: var(--colour-accent);
border: none;
border-radius: var(--radius);
color: #fff;
cursor: pointer;
font-size: 0.8rem;
font-weight: 600;
padding: 0.3rem 0.8rem;
transition: background 0.15s;
}
.equip-button:hover {
background: var(--colour-accent-light);
}
/* ===================== ACHIEVEMENTS ===================== */
.achievement-progress {
color: var(--colour-text-muted);
font-size: 0.85rem;
margin-bottom: 1rem;
}
.achievement-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.achievement-card {
align-items: center;
background: var(--colour-surface);
border: 1px solid var(--colour-border);
border-radius: var(--radius);
display: flex;
gap: 0.75rem;
padding: 0.75rem;
transition: border-color 0.15s;
}
.achievement-card.unlocked {
border-color: var(--colour-gold);
box-shadow: 0 0 8px rgba(245, 158, 11, 0.15);
}
.achievement-card.locked {
opacity: 0.5;
}
.achievement-icon {
font-size: 1.5rem;
min-width: 2rem;
text-align: center;
}
.achievement-info {
flex: 1;
}
.achievement-info h3 {
font-size: 0.95rem;
margin-bottom: 0.1rem;
}
.achievement-info p {
color: var(--colour-text-muted);
font-size: 0.8rem;
}
.achievement-condition {
font-style: italic;
}
.achievement-reward {
color: var(--colour-crystal) !important;
font-weight: 600;
}
.achievement-status {
flex-shrink: 0;
}
.achievement-unlocked-badge {
color: var(--colour-gold);
font-size: 0.85rem;
font-weight: 700;
}
.achievement-locked-badge {
color: var(--colour-text-muted);
font-size: 1rem;
}
/* ===================== ACHIEVEMENT TOAST ===================== */
@keyframes slide-in-right {
from { opacity: 0; transform: translateX(120%); }
to { opacity: 1; transform: translateX(0); }
}
.achievement-toast-container {
bottom: 1.5rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
position: fixed;
right: 1.5rem;
z-index: 200;
}
.achievement-toast {
align-items: center;
animation: slide-in-right 0.35s ease-out;
background: var(--colour-surface);
border: 1px solid var(--colour-gold);
border-radius: var(--radius);
box-shadow: 0 4px 20px rgba(0,0,0,0.4);
cursor: pointer;
display: flex;
gap: 0.75rem;
max-width: 280px;
padding: 0.75rem 1rem;
}
.toast-icon {
font-size: 1.5rem;
flex-shrink: 0;
}
.toast-content {
display: flex;
flex-direction: column;
gap: 0.1rem;
}
.toast-label {
color: var(--colour-gold);
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.toast-name {
color: var(--colour-text);
font-size: 0.9rem;
font-weight: 600;
}
.toast-reward {
color: var(--colour-crystal);
font-size: 0.8rem;
}
/* ===================== UTILITY ===================== */
.error {
color: var(--colour-error);