Compare commits

..

No commits in common. "9a9ec999ff940fa4123d8e1b1097d3ffa497fbc6" and "d75e9f3cbb1d42cda9642f9cb6e334fe912e4b97" have entirely different histories.

8 changed files with 160 additions and 170 deletions

View file

@ -1,4 +1,4 @@
import React from 'react'
import React, { useEffect } from 'react'
import {
Tabs,
TabsContent,
@ -10,7 +10,8 @@ import Field from './components/Field'
import Warehouse from './components/Warehouse'
import Market from './components/Market'
import { ActionCooldown } from './components/ActionCooldown'
import { useSaveSystem } from './store/useSaveSystem'
import { useGameStore } from './store/useGameStore'
import { hasSaveInSlot } from './utils/saveSystem'
const appContainerStyle: React.CSSProperties = {
maxWidth: '1200px',
@ -26,7 +27,34 @@ const tabsListStyles: React.CSSProperties = {
function App() {
useGameTick()
useSaveSystem()
const { saveToSlot, loadFromSlot } = useGameStore()
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const slot =
{
'1': 1,
'2': 2,
'3': 3,
'!': 1,
'@': 2,
'#': 3,
}[e.key] ?? null
if (slot !== null) {
if (e.shiftKey) {
saveToSlot(slot)
console.log(`Game saved to slot ${slot}`)
} else if (hasSaveInSlot(slot)) {
loadFromSlot(slot)
console.log(`Game loaded from slot ${slot}`)
}
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [saveToSlot, loadFromSlot])
return (
<div>

View file

@ -0,0 +1,59 @@
import React, { useEffect, useState } from 'react'
interface FloatingMessageProps {
message: string
startPosition: { x: number; y: number }
onComplete: () => void
}
const FloatingMessage: React.FC<FloatingMessageProps> = ({
message,
startPosition,
onComplete,
}) => {
const [position, setPosition] = useState({ y: startPosition.y })
const [opacity, setOpacity] = useState(1)
useEffect(() => {
const startTime = Date.now()
const duration = 1000 // 1 second animation
const animate = () => {
const elapsed = Date.now() - startTime
const progress = Math.min(elapsed / duration, 1)
setPosition({ y: startPosition.y - progress * 50 })
setOpacity(1 - progress)
if (progress < 1) {
requestAnimationFrame(animate)
} else {
onComplete()
}
}
const animationFrame = requestAnimationFrame(animate)
return () => {
cancelAnimationFrame(animationFrame)
}
}, [startPosition, onComplete])
return (
<div
style={{
position: 'absolute',
left: startPosition.x,
top: position.y,
opacity,
fontWeight: 'bold',
pointerEvents: 'none',
zIndex: 100,
}}
>
{message}
</div>
)
}
export default FloatingMessage

View file

@ -1,74 +0,0 @@
import React, { useEffect, useState, useImperativeHandle, forwardRef } from 'react'
interface Message {
id: string
text: string
position: { x: number; y: number }
}
export interface FloatingMessagesHandle {
addMessage: (text: string, position: { x: number; y: number }) => void
}
const FloatingMessages = forwardRef<FloatingMessagesHandle>((_, ref) => {
const [messages, setMessages] = useState<Message[]>([])
useImperativeHandle(ref, () => ({
addMessage: (text: string, position: { x: number; y: number }) => {
const newMessage: Message = {
id: Math.random().toString(36).substring(2),
text,
position,
}
setMessages(prev => [...prev, newMessage])
}
}))
const handleAnimationEnd = (messageId: string) => {
setMessages(prev => prev.filter(msg => msg.id !== messageId))
}
return (
<>
<style>
{`
@keyframes floatUp {
0% {
transform: translateY(0);
opacity: 1;
}
100% {
transform: translateY(-50px);
opacity: 0;
}
}
.floating-message {
position: absolute;
font-weight: bold;
pointer-events: none;
z-index: 100;
animation: floatUp 1s ease-out forwards;
}
`}
</style>
{messages.map(message => (
<div
key={message.id}
className="floating-message"
style={{
left: message.position.x,
top: message.position.y,
}}
onAnimationEnd={() => handleAnimationEnd(message.id)}
>
{message.text}
</div>
))}
</>
)
})
FloatingMessages.displayName = 'FloatingMessages'
export default FloatingMessages

View file

@ -1,7 +1,7 @@
import React, { useRef } from 'react'
import React, { useState } from 'react'
import { useGameStore } from '../store/useGameStore'
import { MARKET_ITEMS } from '../constants'
import FloatingMessage, { FloatingMessagesHandle } from './FloatingMessages'
import FloatingMessage from './FloatingMessage'
import { styled } from '@linaria/react'
const MarketContainer = styled.div`
@ -71,9 +71,17 @@ const ActionButton = styled.button<{ disabled?: boolean }>`
border: none;
`
interface FloatingMessageData {
id: string
message: string
position: { x: number; y: number }
}
const MarketComponent: React.FC = () => {
const { inventory, cash, buyItem, sellItem } = useGameStore()
const floatingMessagesRef = useRef<FloatingMessagesHandle>(null)
const [floatingMessages, setFloatingMessages] = useState<
FloatingMessageData[]
>([])
const handleBuy = (itemId: string, e: React.MouseEvent) => {
const item = MARKET_ITEMS[itemId]
@ -84,10 +92,14 @@ const MarketComponent: React.FC = () => {
const rect = (e.target as HTMLElement).getBoundingClientRect()
const position = { x: rect.left + rect.width / 2, y: rect.top }
floatingMessagesRef.current?.addMessage(
`+1 ${item.emoji} ${item.name}`,
position
)
setFloatingMessages([
...floatingMessages,
{
id: `buy-${itemId}-${Date.now()}`,
message: `+1 ${item.emoji} ${item.name}`,
position,
},
])
}
const handleSell = (itemId: string, e: React.MouseEvent) => {
@ -105,10 +117,18 @@ const MarketComponent: React.FC = () => {
const rect = (e.target as HTMLElement).getBoundingClientRect()
const position = { x: rect.left + rect.width / 2, y: rect.top }
floatingMessagesRef.current?.addMessage(
`-1 ${item.emoji} ${item.name}`,
position
)
setFloatingMessages([
...floatingMessages,
{
id: `sell-${itemId}-${Date.now()}`,
message: `-1 ${item.emoji} ${item.name}`,
position,
},
])
}
const removeFloatingMessage = (messageId: string) => {
setFloatingMessages(floatingMessages.filter((msg) => msg.id !== messageId))
}
const sellableItems = Object.entries(MARKET_ITEMS)
@ -196,7 +216,14 @@ const MarketComponent: React.FC = () => {
</tbody>
</MarketTable>
<FloatingMessage ref={floatingMessagesRef} />
{floatingMessages.map((msg) => (
<FloatingMessage
key={msg.id}
message={msg.message}
startPosition={msg.position}
onComplete={() => removeFloatingMessage(msg.id)}
/>
))}
</MarketContainer>
)
}

View file

@ -1,7 +1,7 @@
import { CropDefinitions } from '../types'
export const INITIAL_CASH = 50
export const INITIAL_FIELD_SIZE = 3
export const INITIAL_FIELD_SIZE = 4
export const COOLDOWN_DURATION = 2000 // 2 seconds in milliseconds
export const TICK_INTERVAL = 12000 // 12 seconds in milliseconds
export const GAME_SPEEDS = {

View file

@ -11,6 +11,7 @@ import {
MARKET_ITEMS,
} from '../constants'
import { GameState, PlotState } from '../types'
import { saveGame, loadGame } from '../utils/saveSystem'
const initializeField = (size: number): PlotState[][] => {
return Array(size)
@ -24,34 +25,6 @@ const initializeField = (size: number): PlotState[][] => {
)
}
const SAVE_SLOT_PREFIX = 'dionysian_idle_save_'
const saveGame = (slot: number, state: GameState) => {
try {
const saveData = JSON.stringify(state)
localStorage.setItem(`${SAVE_SLOT_PREFIX}${slot}`, saveData)
return true
} catch (error) {
console.error('Failed to save game:', error)
return false
}
}
const loadGame = (slot: number): GameState | null => {
try {
const saveData = localStorage.getItem(`${SAVE_SLOT_PREFIX}${slot}`)
if (!saveData) return null
return JSON.parse(saveData)
} catch (error) {
console.error('Failed to load game:', error)
return null
}
}
export const hasSaveInSlot = (slot: number): boolean => {
return !!localStorage.getItem(`${SAVE_SLOT_PREFIX}${slot}`)
}
export const useGameStore = create<
GameState & {
plant: () => void

View file

@ -1,52 +0,0 @@
import { useEffect } from "react"
import { hasSaveInSlot, useGameStore } from "./useGameStore"
export const useSaveSystem = () => {
const { saveToSlot, loadFromSlot } = useGameStore()
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'r' && e.ctrlKey) {
localStorage.clear()
}
const slot =
{
'1': 1,
'2': 2,
'3': 3,
'!': 1,
'@': 2,
'#': 3,
}[e.key] ?? null
if (slot !== null) {
if (e.shiftKey) {
saveToSlot(slot)
console.log(`Game saved to slot ${slot}`)
} else if (hasSaveInSlot(slot)) {
loadFromSlot(slot)
console.log(`Game loaded from slot ${slot}`)
}
}
}
window.addEventListener('keydown', handleKeyDown)
const interval = setInterval(() => {
saveToSlot(0)
}, 30000)
console.log('Initalized save system')
return () => {
window.removeEventListener('keydown', handleKeyDown)
clearInterval(interval)
}
}, [saveToSlot, loadFromSlot])
// When starting the game, load from the autosave slot
useEffect(() => {
loadFromSlot(0)
}, [])
}

29
src/utils/saveSystem.ts Normal file
View file

@ -0,0 +1,29 @@
import { GameState } from '../types'
const SAVE_SLOT_PREFIX = 'dionysian_idle_save_'
export const saveGame = (slot: number, state: GameState) => {
try {
const saveData = JSON.stringify(state)
localStorage.setItem(`${SAVE_SLOT_PREFIX}${slot}`, saveData)
return true
} catch (error) {
console.error('Failed to save game:', error)
return false
}
}
export const loadGame = (slot: number): GameState | null => {
try {
const saveData = localStorage.getItem(`${SAVE_SLOT_PREFIX}${slot}`)
if (!saveData) return null
return JSON.parse(saveData)
} catch (error) {
console.error('Failed to load game:', error)
return null
}
}
export const hasSaveInSlot = (slot: number): boolean => {
return !!localStorage.getItem(`${SAVE_SLOT_PREFIX}${slot}`)
}