diff --git a/.project/designs/app/src/pages/PromotionPicker.tsx b/.project/designs/app/src/pages/PromotionPicker.tsx new file mode 100644 index 00000000..2dc4745c --- /dev/null +++ b/.project/designs/app/src/pages/PromotionPicker.tsx @@ -0,0 +1,406 @@ +import { useState } from "react"; +import styled from "styled-components"; +import { t } from "../theme"; + +type PromoType = "combat" | "support" | "special"; + +interface Promo { + id: string; + icon: string; + name: string; + rank: number; // 1..3 stars filled + type: PromoType; + typeLabel: string; + desc: string; + effect: string; + effectColor: string; + locked?: boolean; + lockReason?: string; +} + +const PROMOS: Promo[] = [ + { id: "combat-ii", icon: "โš”", name: "Combat II", rank: 2, type: "combat", typeLabel: "Combat", + desc: "Increases base combat strength when attacking.", + effect: "+10% attack strength", effectColor: t.sem.negative }, + { id: "shock", icon: "๐Ÿ—ก", name: "Shock", rank: 2, type: "combat", typeLabel: "Combat", + desc: "Bonus strength when attacking units in open terrain.", + effect: "+15% vs units on plains / desert", effectColor: t.sem.negative }, + { id: "raider", icon: "๐Ÿฐ", name: "City Raider", rank: 2, type: "combat", typeLabel: "Combat", + desc: "Specialised for assaulting fortified positions and cities.", + effect: "+20% vs fortified / city units", effectColor: t.sem.negative }, + { id: "stalwart", icon: "๐Ÿ›ก", name: "Stalwart", rank: 2, type: "support", typeLabel: "Defense", + desc: "Hardens the unit against incoming attacks.", + effect: "+10% defense strength", effectColor: t.accent.sage }, + { id: "medic", icon: "โค", name: "Medic I", rank: 2, type: "support", typeLabel: "Support", + desc: "Unit and adjacent friendly units heal faster each turn.", + effect: "+5 HP healed / turn (nearby units +2)", effectColor: t.accent.sage }, + { id: "march", icon: "๐Ÿ’€", name: "March", rank: 2, type: "special", typeLabel: "Special", + desc: "Unit heals even when it attacks during a turn.", + effect: "Heal 10 HP per turn even when moving", effectColor: t.accent.science, + locked: true, lockReason: "๐Ÿ”’ Requires: Medic I" }, + { id: "blitz", icon: "โšก", name: "Blitz", rank: 2, type: "special", typeLabel: "Special", + desc: "May attack more than once per turn if first strike kills.", + effect: "Extra attack on kill", effectColor: t.accent.science }, + { id: "mountaineer", icon: "โ›ฐ", name: "Mountaineer", rank: 2, type: "combat", typeLabel: "Terrain", + desc: "Excels in rough highland terrain.", + effect: "+25% in hills / mountains", effectColor: t.sem.negative, + locked: true, lockReason: "๐Ÿ”’ Requires: Combat II" }, + { id: "scout", icon: "๐Ÿ‘", name: "Scout", rank: 2, type: "support", typeLabel: "Recon", + desc: "Extends visibility range and reveals more fog of war.", + effect: "+1 vision range ยท reveals terrain faster", effectColor: t.accent.sage }, +]; + +const PageWrap = styled.div` + background: rgba(0,0,0,0.7); + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; + font-family: ${t.font.body}; + position: relative; + + &::before { + content: ''; + position: fixed; inset: 0; + background: + radial-gradient(ellipse at 30% 60%, #1a3319 0%, transparent 50%), + radial-gradient(ellipse at 70% 30%, #19231a 0%, transparent 40%), + #0d1208; + z-index: -1; + filter: blur(2px); + } +`; + +const Modal = styled.div` + width: 660px; + background: ${t.bg.panel}; + border: 1px solid ${t.border.panel}; + border-radius: ${t.radius.panel}; + box-shadow: 0 24px 80px rgba(0,0,0,0.9), 0 0 0 1px #73591f22; + overflow: hidden; +`; + +const ModalHeader = styled.div` + background: linear-gradient(180deg, #2a1a06 0%, #1a1208 100%); + border-bottom: 2px solid ${t.accent.gold}; + padding: 16px 20px; + display: flex; + align-items: center; + gap: 16px; +`; + +const UnitPortrait = styled.div` + width: 56px; height: 56px; + border-radius: 50%; + border: 2px solid ${t.accent.gold}; + background: radial-gradient(ellipse at 40% 40%, #2a1a06, ${t.bg.menu}); + display: flex; align-items: center; justify-content: center; + font-size: 26px; + box-shadow: 0 0 16px #d9a02033; + flex-shrink: 0; +`; + +const HeaderInfo = styled.div` + flex: 1; +`; + +const HeaderTitle = styled.div` + font-family: ${t.font.heading}; + font-size: 22px; + color: ${t.text.title}; + letter-spacing: 0.04em; + margin-bottom: 2px; +`; + +const HeaderSub = styled.div` + font-size: 12px; + color: ${t.text.secondary}; +`; + +const HeaderXp = styled.div` + text-align: right; + font-size: 12px; + color: ${t.text.muted}; +`; + +const HeaderXpVal = styled.span` + font-size: 20px; + font-weight: bold; + color: ${t.accent.gold}; + font-family: ${t.font.mono}; + display: block; + margin-bottom: 2px; +`; + +const EarnedStrip = styled.div` + background: rgba(10,8,16,0.6); + border-bottom: 1px solid ${t.border.divider}; + padding: 8px 20px; + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +`; + +const EarnedLabel = styled.span` + font-size: 11px; + color: ${t.text.muted}; + text-transform: uppercase; + letter-spacing: 0.08em; + flex-shrink: 0; +`; + +const PromoTag = styled.div` + display: flex; + align-items: center; + gap: 4px; + background: #1a1208; + border: 1px solid ${t.accent.gold}; + border-radius: 2px; + padding: 3px 8px; + font-size: 11px; + color: ${t.accent.gold}; +`; + +const SectionLabel = styled.div` + padding: 14px 20px 8px; + font-size: 11px; + color: ${t.text.muted}; + text-transform: uppercase; + letter-spacing: 0.1em; +`; + +const PromoGrid = styled.div` + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 10px; + padding: 0 20px 16px; +`; + +const typeAccent = (type: PromoType): string => { + switch (type) { + case "combat": return t.sem.negative; + case "support": return t.accent.sage; + case "special": return t.accent.science; + } +}; + +const PromoCard = styled.div<{ $type: PromoType; $selected: boolean; $locked: boolean }>` + background: ${p => p.$selected ? "#2a1a06" : t.bg.btnNormal}; + border: 1px solid ${p => p.$selected ? t.accent.gold : "#73591f88"}; + border-top: 2px solid ${p => p.$selected ? typeAccent(p.$type) : `${typeAccent(p.$type)}66`}; + border-radius: ${t.radius.btn}; + padding: 12px; + cursor: ${p => p.$locked ? "not-allowed" : "pointer"}; + transition: all 150ms ease; + position: relative; + overflow: hidden; + opacity: ${p => p.$locked ? 0.4 : 1}; + filter: ${p => p.$locked ? "grayscale(0.4)" : "none"}; + box-shadow: ${p => p.$selected ? "0 0 20px #d9a02022" : "none"}; + + &:hover { + ${p => !p.$locked && ` + border-color: ${t.accent.goldBright}; + background: #2a1a0a; + transform: translateY(-2px); + box-shadow: 0 4px 16px rgba(0,0,0,0.6); + `} + } +`; + +const PromoCardTop = styled.div` + display: flex; + align-items: flex-start; + gap: 8px; + margin-bottom: 8px; +`; + +const PromoIcon = styled.div` + font-size: 22px; + flex-shrink: 0; +`; + +const PromoName = styled.div` + font-size: 13px; + font-weight: bold; + color: ${t.text.primary}; + line-height: 1.2; +`; + +const PromoRank = styled.div` + display: flex; + gap: 2px; + margin-top: 2px; +`; + +const Star = styled.span<{ $empty?: boolean }>` + color: ${p => p.$empty ? "#73591f44" : t.accent.gold}; + font-size: 10px; +`; + +const PromoTypeBadge = styled.div<{ $type: PromoType }>` + margin-left: auto; + font-size: 9px; + text-transform: uppercase; + letter-spacing: 0.08em; + padding: 2px 5px; + border-radius: 2px; + flex-shrink: 0; + background: ${p => `${typeAccent(p.$type)}22`}; + color: ${p => `${typeAccent(p.$type)}99`}; + border: 1px solid ${p => `${typeAccent(p.$type)}33`}; +`; + +const PromoDesc = styled.div` + font-size: 11px; + color: ${t.text.secondary}; + line-height: 1.4; + margin-bottom: 6px; +`; + +const PromoEffect = styled.div<{ $color: string }>` + font-size: 11px; + font-family: ${t.font.mono}; + font-weight: bold; + color: ${p => p.$color}; +`; + +const SelectedCheck = styled.div` + position: absolute; + top: 8px; right: 8px; + width: 18px; height: 18px; + border-radius: 50%; + background: ${t.accent.gold}; + display: flex; align-items: center; justify-content: center; + font-size: 11px; + color: #000; +`; + +const LockReason = styled.div` + font-size: 10px; + color: ${t.sem.negative}; + margin-top: 4px; + display: flex; + align-items: center; + gap: 4px; +`; + +const ModalFooter = styled.div` + border-top: 1px solid ${t.border.panel}; + padding: 14px 20px; + display: flex; + align-items: center; + gap: 10px; + background: rgba(10,8,16,0.5); +`; + +const SelectedSummary = styled.div` + flex: 1; + font-size: 13px; + color: ${t.text.secondary}; +`; + +const SelectedName = styled.span` + color: ${t.text.title}; + font-weight: bold; +`; + +const BtnConfirm = styled.button` + font-family: ${t.font.heading}; + font-size: 18px; + color: ${t.text.title}; + background: #2a1a06; + border: 2px solid ${t.accent.gold}; + border-radius: ${t.radius.btn}; + padding: 10px 28px; + cursor: pointer; + letter-spacing: 0.05em; + transition: all 150ms ease; + &:hover { background: #3d2608; border-color: ${t.accent.goldBright}; } +`; + +const BtnCancel = styled.button` + font-family: ${t.font.body}; + font-size: 14px; + font-weight: 700; + color: ${t.text.muted}; + background: ${t.bg.btnNormal}; + border: 1px solid ${t.border.panel}; + border-radius: ${t.radius.btn}; + padding: 10px 18px; + cursor: pointer; +`; + +export function PromotionPickerPage(): React.ReactElement { + const [selectedId, setSelectedId] = useState("combat-ii"); + const selected = PROMOS.find(p => p.id === selectedId); + + return ( + + + + โš” + + Promotion Available + Iron Warrior ยท Melee Infantry ยท Stoneguard Clan + + + โ˜…โ˜… + Rank 2 ยท 48 XP + + + + + Earned: + โ˜… Strength I + โ˜… Flanking + + + Choose one promotion + + + {PROMOS.map(p => { + const isSelected = p.id === selectedId; + return ( + { if (!p.locked) setSelectedId(p.id); }} + > + {isSelected && โœ“} + + {p.icon} +
+ {p.name} + + {[0, 1, 2].map(i => ( + = p.rank}>โ˜… + ))} + +
+ {p.typeLabel} +
+ {p.desc} + {p.effect} + {p.lockReason && {p.lockReason}} +
+ ); + })} +
+ + + + Selected: {selected?.name ?? "โ€”"} + {selected && ยท {selected.effect}} + + Back + Promote โ˜…โ˜… + +
+
+ ); +} diff --git a/.project/designs/app/src/pages/TechTree.tsx b/.project/designs/app/src/pages/TechTree.tsx new file mode 100644 index 00000000..1c4578e0 --- /dev/null +++ b/.project/designs/app/src/pages/TechTree.tsx @@ -0,0 +1,514 @@ +import { useState } from "react"; +import styled from "styled-components"; +import { t } from "../theme"; + +type NodeStatus = "researched" | "in-progress" | "available" | "locked"; +type Pillar = "Military" | "Economy" | "Industry" | "Agriculture" | "Governance" | "Culture" | "Exploration" | "Engineering" | "Medicine"; + +interface TechNodeData { + id: string; + name: string; + icon: string; + status: NodeStatus; + cost: string; + unlocks: string; + x: number; + y: number; + progress?: number; +} + +interface ConnectionData { + x1: number; y1: number; x2: number; y2: number; + stroke: string; width: number; dash?: string; opacity?: number; + animated?: boolean; +} + +const PILLARS: Pillar[] = [ + "Military", "Economy", "Industry", "Agriculture", "Governance", + "Culture", "Exploration", "Engineering", "Medicine", +]; + +const NODES: TechNodeData[] = [ + // Stone + { id: "stone-tools", name: "Stone Tools", icon: "๐Ÿชจ", status: "researched", cost: "โœ“ Researched", unlocks: "Worker ยท Quarry", x: 65, y: 95 }, + { id: "fire", name: "Fire Making", icon: "๐Ÿ”ฅ", status: "researched", cost: "โœ“ Researched", unlocks: "Forge ยท Ale Hall", x: 65, y: 155 }, + { id: "agriculture", name: "Agriculture", icon: "๐ŸŒพ", status: "researched", cost: "โœ“ Researched", unlocks: "Farm ยท Granary", x: 65, y: 215 }, + // Bronze + { id: "bronze", name: "Bronze Casting", icon: "๐Ÿฅ‰", status: "researched", cost: "โœ“ Researched", unlocks: "Spearman ยท Shield", x: 300, y: 155 }, + { id: "mining", name: "Mining", icon: "โš’", status: "researched", cost: "โœ“ Researched", unlocks: "Mine ยท Pickaxe", x: 300, y: 260 }, + { id: "husbandry", name: "Animal Husbandry", icon: "๐Ÿ‚", status: "researched", cost: "โœ“ Researched", unlocks: "Pasture ยท War Boar", x: 300, y: 360 }, + // Iron + { id: "steel", name: "Steel Working", icon: "โš™", status: "in-progress", cost: "โš— 88 / 120 ยท 4t", unlocks: "Iron Warrior ยท Forge +1", x: 550, y: 255, progress: 73 }, + { id: "archery", name: "Archery", icon: "๐Ÿน", status: "available", cost: "โš— 120", unlocks: "Archer ยท Watchtower", x: 550, y: 355 }, + { id: "masonry", name: "Masonry", icon: "๐Ÿ›", status: "available", cost: "โš— 140", unlocks: "Walls ยท Monument", x: 550, y: 455 }, + // Steel + { id: "sword", name: "Swordsmanship", icon: "๐Ÿ—ก", status: "locked", cost: "โš— 200", unlocks: "Swordsman ยท Siege Eng.", x: 800, y: 325 }, + { id: "iron", name: "Iron Working", icon: "๐Ÿ”ฉ", status: "locked", cost: "โš— 180", unlocks: "Forge upgrade ยท Armorer", x: 800, y: 430 }, + { id: "hydra", name: "Hydraulics", icon: "๐ŸŒŠ", status: "locked", cost: "โš— 160", unlocks: "Aqueduct upgrade", x: 800, y: 540 }, + // Late + { id: "tactics", name: "Tactics", icon: "โš”", status: "locked", cost: "โš— 280", unlocks: "Formation bonuses", x: 1050, y: 400 }, + { id: "engineering", name: "Engineering", icon: "๐Ÿ—", status: "locked", cost: "โš— 260", unlocks: "Catapult ยท Roads", x: 1050, y: 515 }, + { id: "writing", name: "Writing", icon: "๐Ÿ“œ", status: "locked", cost: "โš— 220", unlocks: "Library ยท Diplomat", x: 1050, y: 630 }, + // End + { id: "mastery", name: "Dwarven Mastery", icon: "๐Ÿ†", status: "locked", cost: "โš— 400", unlocks: "Victory ยท All bonuses", x: 1300, y: 470 }, + { id: "alchemy", name: "Alchemy", icon: "โš—", status: "locked", cost: "โš— 350", unlocks: "Alchemist Guild", x: 1300, y: 630 }, +]; + +const CONNECTIONS: ConnectionData[] = [ + { x1: 195, y1: 130, x2: 300, y2: 185, stroke: "#d9a02055", width: 1.5 }, + { x1: 195, y1: 130, x2: 300, y2: 290, stroke: "#d9a02033", width: 1 }, + { x1: 195, y1: 130, x2: 300, y2: 390, stroke: "#d9a02022", width: 1 }, + { x1: 195, y1: 185, x2: 300, y2: 185, stroke: "#d9a02066", width: 1.5 }, + { x1: 195, y1: 185, x2: 300, y2: 290, stroke: "#d9a02033", width: 1 }, + { x1: 195, y1: 240, x2: 300, y2: 290, stroke: "#d9a02055", width: 1.5 }, + { x1: 195, y1: 240, x2: 300, y2: 390, stroke: "#d9a02033", width: 1 }, + { x1: 435, y1: 185, x2: 550, y2: 285, stroke: "#d9a02055", width: 1.5 }, + { x1: 435, y1: 185, x2: 550, y2: 385, stroke: "#66bfff66", width: 1.5, dash: "4,3" }, + { x1: 435, y1: 290, x2: 550, y2: 285, stroke: "#d9a02044", width: 1.5 }, + { x1: 435, y1: 290, x2: 550, y2: 485, stroke: "#d9a02033", width: 1 }, + { x1: 435, y1: 390, x2: 550, y2: 385, stroke: "#d9a02055", width: 1.5 }, + { x1: 435, y1: 390, x2: 550, y2: 485, stroke: "#d9a02033", width: 1 }, + { x1: 685, y1: 285, x2: 800, y2: 355, stroke: "#66bfff55", width: 1.5, dash: "4,3" }, + { x1: 685, y1: 285, x2: 800, y2: 460, stroke: "#d9a02033", width: 1 }, + { x1: 685, y1: 385, x2: 800, y2: 355, stroke: "#d9a02044", width: 1.5 }, + { x1: 685, y1: 385, x2: 800, y2: 460, stroke: "#d9a02055", width: 1.5 }, + { x1: 685, y1: 485, x2: 800, y2: 460, stroke: "#d9a02044", width: 1 }, + { x1: 685, y1: 485, x2: 800, y2: 570, stroke: "#d9a02033", width: 1 }, + { x1: 935, y1: 355, x2: 1050, y2: 430, stroke: "#d9a02044", width: 1.5 }, + { x1: 935, y1: 460, x2: 1050, y2: 430, stroke: "#d9a02044", width: 1.5 }, + { x1: 935, y1: 460, x2: 1050, y2: 545, stroke: "#d9a02033", width: 1 }, + { x1: 935, y1: 570, x2: 1050, y2: 545, stroke: "#d9a02033", width: 1 }, + { x1: 935, y1: 570, x2: 1050, y2: 660, stroke: "#d9a02022", width: 1 }, + { x1: 1185, y1: 430, x2: 1300, y2: 500, stroke: "#d9a02033", width: 1 }, + { x1: 1185, y1: 545, x2: 1300, y2: 500, stroke: "#d9a02033", width: 1 }, + { x1: 1185, y1: 660, x2: 1300, y2: 660, stroke: "#d9a02022", width: 1 }, + { x1: 195, y1: 130, x2: 300, y2: 185, stroke: "#d9a020", width: 2, opacity: 0.5 }, + { x1: 195, y1: 185, x2: 300, y2: 185, stroke: "#d9a020", width: 2, opacity: 0.5 }, + { x1: 195, y1: 240, x2: 300, y2: 290, stroke: "#d9a020", width: 2, opacity: 0.4 }, + { x1: 435, y1: 185, x2: 550, y2: 285, stroke: "#d9a020", width: 2, opacity: 0.35 }, + { x1: 550, y1: 285, x2: 685, y2: 285, stroke: "#66bfff", width: 2, dash: "6,4", opacity: 0.7, animated: true }, +]; + +const ERAS = [ + { y: 60, label: "Stone Age" }, + { y: 240, label: "Bronze Age" }, + { y: 430, label: "Iron Age" }, + { y: 620, label: "Steel Age" }, +]; + +const PageWrap = styled.div` + background: ${t.bg.menu}; + color: ${t.text.primary}; + font-family: ${t.font.body}; + height: 100vh; + display: flex; + flex-direction: column; + overflow: hidden; +`; + +const Header = styled.div` + background: ${t.bg.panel}; + border-bottom: 1px solid ${t.border.panel}; + padding: 12px 24px; + display: flex; + align-items: center; + gap: 24px; + flex-shrink: 0; +`; + +const HeaderTitle = styled.div` + font-family: ${t.font.heading}; + font-size: 22px; + color: ${t.text.title}; + letter-spacing: 0.04em; +`; + +const SciRate = styled.div` + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + color: ${t.accent.science}; + background: #0d1a2a; + border: 1px solid #66bfff44; + border-radius: 3px; + padding: 5px 12px; +`; + +const CurrentResearch = styled.div` + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + color: ${t.text.secondary}; + margin-left: auto; +`; + +const ResearchName = styled.span` + color: ${t.accent.science}; + font-weight: bold; +`; + +const ResearchTurns = styled.span` + color: ${t.accent.gold}; + font-size: 12px; +`; + +const CloseBtn = styled.div` + width: 32px; height: 32px; + background: ${t.bg.btnNormal}; + border: 1px solid ${t.border.panel}; + border-radius: 3px; + display: flex; align-items: center; justify-content: center; + color: ${t.text.muted}; + cursor: pointer; + font-size: 16px; +`; + +const PillarBar = styled.div` + background: rgba(23, 18, 30, 0.9); + border-bottom: 1px solid ${t.border.divider}; + display: flex; + flex-shrink: 0; + padding: 0 20px; +`; + +const PillarLabel = styled.div<{ $active?: boolean }>` + flex: 1; + text-align: center; + padding: 7px 4px; + font-size: 11px; + color: ${p => p.$active ? t.accent.gold : t.text.muted}; + text-transform: uppercase; + letter-spacing: 0.08em; + border-right: 1px solid #73591f22; + &:last-child { border-right: none; } +`; + +const FilterBar = styled.div` + position: fixed; + top: 108px; right: 16px; + display: flex; + flex-direction: column; + gap: 4px; + z-index: 50; +`; + +const FilterChip = styled.div<{ $on?: boolean }>` + padding: 5px 10px; + border-radius: 3px; + border: 1px solid ${p => p.$on ? t.accent.goldBright : t.border.panel}; + background: ${p => p.$on ? "#2a1a06" : t.bg.btnNormal}; + font-size: 11px; + color: ${p => p.$on ? t.accent.gold : t.text.muted}; + cursor: pointer; + text-align: center; +`; + +const TreeScroll = styled.div` + flex: 1; + overflow: auto; + position: relative; +`; + +const TreeCanvas = styled.div` + position: relative; + width: 1400px; + height: 820px; + padding: 24px 20px; +`; + +const ConnectionsSvg = styled.svg` + position: absolute; + top: 0; left: 0; + width: 100%; height: 100%; + pointer-events: none; + z-index: 0; +`; + +const EraBand = styled.div<{ $top: number }>` + position: absolute; + left: 0; right: 0; + top: ${p => p.$top}px; + border-top: 1px dashed ${t.border.divider}; + z-index: 0; +`; + +const EraLabel = styled.span` + position: absolute; + right: 12px; + font-size: 11px; + color: #73591f99; + text-transform: uppercase; + letter-spacing: 0.1em; + transform: translateY(-50%); + font-family: ${t.font.mono}; +`; + +const NodeBox = styled.div<{ $status: NodeStatus; $x: number; $y: number }>` + position: absolute; + left: ${p => p.$x}px; + top: ${p => p.$y}px; + width: 130px; + border-radius: 4px; + border: 1px solid ${p => { + switch (p.$status) { + case "researched": return t.accent.gold; + case "in-progress": return t.accent.science; + case "available": return "#73591f99"; + case "locked": return t.border.panel; + } + }}; + background: ${p => p.$status === "researched" ? "#1a1408" : t.bg.panel}; + cursor: pointer; + z-index: 1; + transition: all 150ms ease; + opacity: ${p => p.$status === "locked" ? 0.45 : 1}; + filter: ${p => p.$status === "locked" ? "grayscale(0.5)" : "none"}; + box-shadow: ${p => p.$status === "in-progress" ? "0 0 12px #66bfff22" : "none"}; + + &:hover { + border-color: ${t.accent.goldBright}; + transform: translateY(-2px); + box-shadow: 0 4px 16px #000a; + } +`; + +const NodeHeader = styled.div<{ $status: NodeStatus }>` + padding: 7px 9px 5px; + border-bottom: 1px solid ${t.border.divider}; + display: flex; + align-items: center; + gap: 7px; + background: ${p => { + switch (p.$status) { + case "researched": return "#2a1a06"; + case "in-progress": return "#0a1a2a"; + default: return "transparent"; + } + }}; +`; + +const NodeIcon = styled.span` + font-size: 18px; +`; + +const NodeName = styled.span<{ $status: NodeStatus }>` + font-size: 12px; + font-weight: bold; + line-height: 1.2; + color: ${p => { + switch (p.$status) { + case "researched": return t.accent.gold; + case "in-progress": return t.accent.science; + default: return t.text.primary; + } + }}; +`; + +const NodeBody = styled.div` + padding: 6px 9px 8px; +`; + +const NodeCost = styled.div<{ $researched: boolean }>` + font-size: 11px; + color: ${p => p.$researched ? t.accent.sage : t.accent.science}; + font-family: ${t.font.mono}; + margin-bottom: 4px; +`; + +const NodeUnlocks = styled.div` + font-size: 10px; + color: ${t.text.muted}; + line-height: 1.4; +`; + +const NodeProgressTrack = styled.div` + height: 3px; + background: #ffffff18; + border-radius: 1px; + overflow: hidden; + margin-top: 5px; +`; + +const NodeProgressFill = styled.div<{ $pct: number }>` + width: ${p => p.$pct}%; + height: 100%; + background: ${t.accent.science}; + border-radius: 1px; +`; + +/* Tooltip */ +const Tooltip = styled.div` + position: fixed; + bottom: 20px; right: 20px; + width: 280px; + background: ${t.bg.panel}; + border: 1px solid ${t.accent.gold}; + border-radius: 4px; + z-index: 100; + overflow: hidden; +`; + +const TtHeader = styled.div` + background: #2a1a06; + padding: 10px 14px; + display: flex; + align-items: center; + gap: 10px; +`; + +const TtIcon = styled.div` + font-size: 24px; +`; + +const TtName = styled.div` + font-family: ${t.font.heading}; + font-size: 18px; + color: ${t.text.title}; + letter-spacing: 0.04em; +`; + +const TtEra = styled.div` + font-size: 11px; + color: ${t.accent.gold}; +`; + +const TtBody = styled.div` + padding: 12px 14px; +`; + +const TtRow = styled.div` + display: flex; + justify-content: space-between; + font-size: 12px; + color: ${t.text.secondary}; + margin-bottom: 6px; +`; + +const TtVal = styled.span<{ $color?: string }>` + color: ${p => p.$color ?? t.accent.science}; + font-family: ${t.font.mono}; +`; + +const TtDivider = styled.hr` + border: none; + border-top: 1px solid ${t.border.divider}; + margin: 8px 0; +`; + +const TtUnlocksLabel = styled.div` + font-size: 11px; + color: ${t.text.muted}; + text-transform: uppercase; + letter-spacing: 0.06em; + margin-bottom: 6px; +`; + +const TtUnlockItem = styled.div` + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: ${t.text.primary}; + margin-bottom: 3px; +`; + +const FILTERS = ["All", "Military", "Economy", "Industry"] as const; + +export function TechTreePage(): React.ReactElement { + const [activeFilter, setActiveFilter] = useState("All"); + const [activePillar] = useState("Military"); + + return ( + +
+ Technology Web + โš— +22 / turn + + Researching: Steel Working + ยท 4 turns + + โœ• +
+ + + {PILLARS.map(p => ( + {p} + ))} + + + + {FILTERS.map(f => ( + setActiveFilter(f)}>{f} + ))} + + + + + {ERAS.map(e => ( + + {e.label} + + ))} + + + {CONNECTIONS.map((c, i) => ( + + {c.animated && ( + + )} + + ))} + + + {NODES.map(n => ( + + + {n.icon} + {n.name} + + + {n.cost} + {n.unlocks} + {n.progress !== undefined && ( + + + + )} + + + ))} + + + + + + โš™ +
+ Steel Working + Iron Age ยท Military + Industry +
+
+ + Cost120 โš— + Progress88 / 120 ยท 4 turns + RequiresBronze Casting, Mining + + Unlocks + โš” Iron Warrior (melee unit) + ๐Ÿ”ฅ Forge upgrade (+2 โš’) + ๐Ÿ›ก Iron Shield (unit equipment) + โš’ Enables: Swordsmanship + +
+
+ ); +}