Compare commits
No commits in common. "9a9ec999ff940fa4123d8e1b1097d3ffa497fbc6" and "d75e9f3cbb1d42cda9642f9cb6e334fe912e4b97" have entirely different histories.
9a9ec999ff
...
d75e9f3cbb
34
src/App.tsx
34
src/App.tsx
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import {
|
import {
|
||||||
Tabs,
|
Tabs,
|
||||||
TabsContent,
|
TabsContent,
|
||||||
|
|
@ -10,7 +10,8 @@ import Field from './components/Field'
|
||||||
import Warehouse from './components/Warehouse'
|
import Warehouse from './components/Warehouse'
|
||||||
import Market from './components/Market'
|
import Market from './components/Market'
|
||||||
import { ActionCooldown } from './components/ActionCooldown'
|
import { ActionCooldown } from './components/ActionCooldown'
|
||||||
import { useSaveSystem } from './store/useSaveSystem'
|
import { useGameStore } from './store/useGameStore'
|
||||||
|
import { hasSaveInSlot } from './utils/saveSystem'
|
||||||
|
|
||||||
const appContainerStyle: React.CSSProperties = {
|
const appContainerStyle: React.CSSProperties = {
|
||||||
maxWidth: '1200px',
|
maxWidth: '1200px',
|
||||||
|
|
@ -26,7 +27,34 @@ const tabsListStyles: React.CSSProperties = {
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
useGameTick()
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
59
src/components/FloatingMessage.tsx
Normal file
59
src/components/FloatingMessage.tsx
Normal 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
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useRef } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { useGameStore } from '../store/useGameStore'
|
import { useGameStore } from '../store/useGameStore'
|
||||||
import { MARKET_ITEMS } from '../constants'
|
import { MARKET_ITEMS } from '../constants'
|
||||||
import FloatingMessage, { FloatingMessagesHandle } from './FloatingMessages'
|
import FloatingMessage from './FloatingMessage'
|
||||||
import { styled } from '@linaria/react'
|
import { styled } from '@linaria/react'
|
||||||
|
|
||||||
const MarketContainer = styled.div`
|
const MarketContainer = styled.div`
|
||||||
|
|
@ -71,9 +71,17 @@ const ActionButton = styled.button<{ disabled?: boolean }>`
|
||||||
border: none;
|
border: none;
|
||||||
`
|
`
|
||||||
|
|
||||||
|
interface FloatingMessageData {
|
||||||
|
id: string
|
||||||
|
message: string
|
||||||
|
position: { x: number; y: number }
|
||||||
|
}
|
||||||
|
|
||||||
const MarketComponent: React.FC = () => {
|
const MarketComponent: React.FC = () => {
|
||||||
const { inventory, cash, buyItem, sellItem } = useGameStore()
|
const { inventory, cash, buyItem, sellItem } = useGameStore()
|
||||||
const floatingMessagesRef = useRef<FloatingMessagesHandle>(null)
|
const [floatingMessages, setFloatingMessages] = useState<
|
||||||
|
FloatingMessageData[]
|
||||||
|
>([])
|
||||||
|
|
||||||
const handleBuy = (itemId: string, e: React.MouseEvent) => {
|
const handleBuy = (itemId: string, e: React.MouseEvent) => {
|
||||||
const item = MARKET_ITEMS[itemId]
|
const item = MARKET_ITEMS[itemId]
|
||||||
|
|
@ -84,10 +92,14 @@ const MarketComponent: React.FC = () => {
|
||||||
const rect = (e.target as HTMLElement).getBoundingClientRect()
|
const rect = (e.target as HTMLElement).getBoundingClientRect()
|
||||||
const position = { x: rect.left + rect.width / 2, y: rect.top }
|
const position = { x: rect.left + rect.width / 2, y: rect.top }
|
||||||
|
|
||||||
floatingMessagesRef.current?.addMessage(
|
setFloatingMessages([
|
||||||
`+1 ${item.emoji} ${item.name}`,
|
...floatingMessages,
|
||||||
position
|
{
|
||||||
)
|
id: `buy-${itemId}-${Date.now()}`,
|
||||||
|
message: `+1 ${item.emoji} ${item.name}`,
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSell = (itemId: string, e: React.MouseEvent) => {
|
const handleSell = (itemId: string, e: React.MouseEvent) => {
|
||||||
|
|
@ -105,10 +117,18 @@ const MarketComponent: React.FC = () => {
|
||||||
const rect = (e.target as HTMLElement).getBoundingClientRect()
|
const rect = (e.target as HTMLElement).getBoundingClientRect()
|
||||||
const position = { x: rect.left + rect.width / 2, y: rect.top }
|
const position = { x: rect.left + rect.width / 2, y: rect.top }
|
||||||
|
|
||||||
floatingMessagesRef.current?.addMessage(
|
setFloatingMessages([
|
||||||
`-1 ${item.emoji} ${item.name}`,
|
...floatingMessages,
|
||||||
position
|
{
|
||||||
)
|
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)
|
const sellableItems = Object.entries(MARKET_ITEMS)
|
||||||
|
|
@ -196,7 +216,14 @@ const MarketComponent: React.FC = () => {
|
||||||
</tbody>
|
</tbody>
|
||||||
</MarketTable>
|
</MarketTable>
|
||||||
|
|
||||||
<FloatingMessage ref={floatingMessagesRef} />
|
{floatingMessages.map((msg) => (
|
||||||
|
<FloatingMessage
|
||||||
|
key={msg.id}
|
||||||
|
message={msg.message}
|
||||||
|
startPosition={msg.position}
|
||||||
|
onComplete={() => removeFloatingMessage(msg.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</MarketContainer>
|
</MarketContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { CropDefinitions } from '../types'
|
import { CropDefinitions } from '../types'
|
||||||
|
|
||||||
export const INITIAL_CASH = 50
|
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 COOLDOWN_DURATION = 2000 // 2 seconds in milliseconds
|
||||||
export const TICK_INTERVAL = 12000 // 12 seconds in milliseconds
|
export const TICK_INTERVAL = 12000 // 12 seconds in milliseconds
|
||||||
export const GAME_SPEEDS = {
|
export const GAME_SPEEDS = {
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
MARKET_ITEMS,
|
MARKET_ITEMS,
|
||||||
} from '../constants'
|
} from '../constants'
|
||||||
import { GameState, PlotState } from '../types'
|
import { GameState, PlotState } from '../types'
|
||||||
|
import { saveGame, loadGame } from '../utils/saveSystem'
|
||||||
|
|
||||||
const initializeField = (size: number): PlotState[][] => {
|
const initializeField = (size: number): PlotState[][] => {
|
||||||
return Array(size)
|
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<
|
export const useGameStore = create<
|
||||||
GameState & {
|
GameState & {
|
||||||
plant: () => void
|
plant: () => void
|
||||||
|
|
|
||||||
|
|
@ -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
29
src/utils/saveSystem.ts
Normal 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}`)
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue