-
Notifications
You must be signed in to change notification settings - Fork 0
feat: gameplay changes — cost scaling, hacks prestige gate, endgame surge #20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f99a394
c59effc
ed22003
d5378e3
ce03c99
6ac3209
1fefe62
417e181
7774161
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -5,7 +5,7 @@ import { pickRandomBuff } from "../../data/buffs"; | |||||
| import type { SpawnedItem } from "../../hooks/useSpawnSystem"; | ||||||
| import { useSpawnSystem } from "../../hooks/useSpawnSystem"; | ||||||
| import { useGameStore } from "../../store/gameStore"; | ||||||
| import { selectClickValue, selectLocPerSecond } from "../../store/selectors"; | ||||||
| import { selectClickValue, selectIsSurgeActive, selectLocPerSecond } from "../../store/selectors"; | ||||||
|
|
||||||
| const SPAWN_DURATION = GAME_CONFIG.buffs.spawnDurationMs; | ||||||
|
|
||||||
|
|
@@ -20,18 +20,26 @@ let msgKey = 0; | |||||
|
|
||||||
| export function BuffSpawnLayer() { | ||||||
| const [messages, setMessages] = useState<BuffMessageItem[]>([]); | ||||||
|
|
||||||
| const { items, removeItem } = useSpawnSystem<BuffDefinition>({ | ||||||
| getInterval: () => ({ | ||||||
| min: GAME_CONFIG.buffs.minSpawnIntervalMs, | ||||||
| max: GAME_CONFIG.buffs.maxSpawnIntervalMs, | ||||||
| }), | ||||||
| canSpawn: (current) => current.length === 0, | ||||||
| createItem: () => pickRandomBuff(), | ||||||
| getLifetime: () => SPAWN_DURATION, | ||||||
| paddingTop: 120, | ||||||
| paddingBottom: 80, | ||||||
| }); | ||||||
| const surgeStartedAt = useGameStore((s) => s.surgeStartedAt); | ||||||
|
|
||||||
| const { items, removeItem } = useSpawnSystem<BuffDefinition>( | ||||||
| { | ||||||
| getInterval: () => { | ||||||
| const surgeActive = selectIsSurgeActive(useGameStore.getState()); | ||||||
| const divisor = surgeActive ? 2 : 1; | ||||||
| return { | ||||||
| min: GAME_CONFIG.buffs.minSpawnIntervalMs / divisor, | ||||||
| max: GAME_CONFIG.buffs.maxSpawnIntervalMs / divisor, | ||||||
| }; | ||||||
| }, | ||||||
| canSpawn: (current) => current.length === 0, | ||||||
| createItem: () => pickRandomBuff(), | ||||||
| getLifetime: () => SPAWN_DURATION, | ||||||
| paddingTop: 120, | ||||||
| paddingBottom: 80, | ||||||
| }, | ||||||
| surgeStartedAt, | ||||||
| ); | ||||||
|
|
||||||
| const handleClick = useCallback( | ||||||
| (item: SpawnedItem<BuffDefinition>) => { | ||||||
|
|
@@ -50,6 +58,7 @@ export function BuffSpawnLayer() { | |||||
| if (result.productionMultiplier || result.clickMultiplier) { | ||||||
| let duration = result.duration ?? 30; | ||||||
| if (state.prestige.prestigeUpgrades.includes("buff_mastery")) duration *= 2; | ||||||
| if (selectIsSurgeActive(state)) duration *= 2; | ||||||
|
||||||
| if (selectIsSurgeActive(state)) duration *= 2; | |
| if (state.surgeStartedAt != null) duration *= 2; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keeping as-is: same as above — one-tick (50ms) delay between mastery threshold and surge start is imperceptible for buff duration.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,14 +28,14 @@ export function StatsPanel({ hideHacks = false }: StatsPanelProps) { | |
| const timePlayed = useGameStore((s) => s.stats.totalTimePlayed); | ||
| const timesShipped = useGameStore((s) => s.prestige.timesShipped); | ||
| const reputation = useGameStore((s) => s.prestige.totalReputationEarned); | ||
| const totalLoCEarned = useGameStore((s) => s.resources.totalLoCEarned); | ||
|
|
||
| const locPerSec = selectLocPerSecond(state); | ||
| const clickValue = selectClickValue(state); | ||
| const prestigeMult = selectPrestigeMultiplier(state); | ||
| const totalBuildings = selectTotalBuildings(state); | ||
|
|
||
| const showHacks = totalLoCEarned >= 1000; | ||
| const hasHackAccess = state.prestige.prestigeUpgrades.includes("hack_access"); | ||
| const showHacks = hasHackAccess && !hideHacks; | ||
|
Comment on lines
+37
to
+38
This comment was marked as resolved.
Sorry, something went wrong.
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in ed22003 — activateHack now checks for hack_access prestige upgrade before proceeding.
Comment on lines
+37
to
+38
This comment was marked as resolved.
Sorry, something went wrong.
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in ce03c99 — removed the redundant |
||
|
|
||
| // Force re-render on tick | ||
| useGameStore((s) => s.resources.linesOfCode); | ||
|
|
@@ -64,7 +64,7 @@ export function StatsPanel({ hideHacks = false }: StatsPanelProps) { | |
| </div> | ||
|
|
||
| {/* Hacks - below stats (hidden in mobile Stats drawer since Hacks has its own tab) */} | ||
| {showHacks && !hideHacks && ( | ||
| {showHacks && ( | ||
| <div className="shrink-0 border-t border-white/5 pt-3 mt-3"> | ||
| <h3 className="text-xs font-semibold text-text-muted uppercase tracking-wider mb-2">Hacks</h3> | ||
| <HackPanel /> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ import { | |
| selectLocPerSecond, | ||
| selectNetTechDebtPerSecond, | ||
| selectReputationOnPrestige, | ||
| selectSurgeMultiplier, | ||
| selectTechDebtMultiplier, | ||
| } from "../../store/selectors"; | ||
| import { formatNumber, formatRate } from "../../utils/formatNumber"; | ||
|
|
@@ -32,6 +33,7 @@ export function TopBar({ onPrestigeClick, onHelpClick }: Props) { | |
| const prestigeUpgrades = useGameStore((s) => s.prestige.prestigeUpgrades); | ||
| const netTdPerSec = selectNetTechDebtPerSecond(state); | ||
| const debtMult = selectTechDebtMultiplier(state); | ||
| const surgeMultiplier = selectSurgeMultiplier(state); | ||
|
|
||
| const hasTD = td > 0; | ||
| const penaltyPercent = Math.round((1 - debtMult) * 100); | ||
|
|
@@ -67,6 +69,15 @@ export function TopBar({ onPrestigeClick, onHelpClick }: Props) { | |
| {/* Mobile: inline LoC/s */} | ||
| <span className="font-mono text-xs text-accent-cyan font-semibold lg:hidden">{formatRate(locPerSec)}</span> | ||
|
|
||
| {/* Surge indicator */} | ||
| {surgeMultiplier > 1 && ( | ||
| <div className="flex items-center gap-1 pl-2 lg:pl-5 border-l border-white/10"> | ||
| <span className="text-accent-gold font-bold text-xs lg:text-sm animate-pulse"> | ||
| ⚡ SURGE {surgeMultiplier}x | ||
| </span> | ||
| </div> | ||
| )} | ||
|
|
||
| {/* Desktop: production stats */} | ||
| <div className="hidden lg:flex flex-col gap-0.5"> | ||
| <div className="flex items-center gap-1"> | ||
|
|
@@ -90,10 +101,22 @@ export function TopBar({ onPrestigeClick, onHelpClick }: Props) { | |
| {/* Tech Debt indicator */} | ||
| {(hasTD || netTdPerSec !== 0) && ( | ||
| <> | ||
| {/* Mobile: compact TD */} | ||
| {/* Mobile: compact TD + refactor */} | ||
| <div className="flex items-center gap-1.5 pl-2 border-l border-white/10 lg:hidden"> | ||
| <span className="font-mono text-xs text-accent-pink font-semibold">{formatNumber(td)}</span> | ||
| {penaltyPercent > 0 && <span className="text-[10px] text-accent-pink">-{penaltyPercent}%</span>} | ||
| {isRefactoring ? ( | ||
| <span className="text-[10px] text-accent-gold font-semibold animate-pulse">{refactorRemaining}s</span> | ||
| ) : ( | ||
| <button | ||
| type="button" | ||
| onClick={() => refactorDebt()} | ||
| disabled={td <= 0} | ||
| className="px-1.5 py-0.5 rounded text-[9px] font-semibold bg-accent-green/10 text-accent-green border border-accent-green/20 cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed" | ||
| > | ||
| Refactor | ||
| </button> | ||
| )} | ||
|
Comment on lines
+104
to
+119
This comment was marked as resolved.
Sorry, something went wrong.
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Acknowledged — the mobile Refactor button was added in a follow-up commit to this PR. PR description updated. |
||
| </div> | ||
|
|
||
| {/* Desktop: full TD section */} | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -19,6 +19,7 @@ export interface ProductionResult { | |||||
| techDebtPerSec: number; | ||||||
| clickValue: number; | ||||||
| tdMultiplier: number; | ||||||
| surgeMultiplier: number; | ||||||
| isRefactoring: boolean; | ||||||
| buildingProductions: Map<string, number>; | ||||||
| } | ||||||
|
|
@@ -90,6 +91,17 @@ export function computeAllProduction(state: GameState): ProductionResult { | |||||
| } | ||||||
| } | ||||||
|
|
||||||
| // Apply surge multiplier if active | ||||||
| let surgeMultiplier = 1; | ||||||
| if (state.surgeStartedAt) { | ||||||
|
||||||
| if (state.surgeStartedAt) { | |
| if (state.surgeStartedAt != null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keeping as-is: same reasoning — surgeStartedAt is set via Date.now() or null. The value 0 is not a valid state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getIntervalusesselectIsSurgeActive(...)(mastery-threshold check) to speed up buff spawns. BecauseselectIsSurgeActivecan be true beforesurgeStartedAtis set (surge production hasn’t started yet), buff spawns can speed up slightly earlier than the actual surge. Consider keying this offuseGameStore.getState().surgeStartedAt != null(or a dedicated “surge active” selector) so all surge effects start at the same moment.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keeping as-is: the one-tick gap between
selectIsSurgeActivebecoming true andsurgeStartedAtbeing set is 50ms — imperceptible for buff spawn timing.