Compare commits
No commits in common. "9edb03007983e2d97fd84281cc511a445beec8c5" and "5724da535af839436f9906377fab1ff5ca143f9d" have entirely different histories.
9edb030079
...
5724da535a
|
|
@ -1,12 +0,0 @@
|
||||||
import eslint from '@eslint/js'
|
|
||||||
import tseslint from 'typescript-eslint'
|
|
||||||
|
|
||||||
export default tseslint.config(
|
|
||||||
eslint.configs.recommended,
|
|
||||||
tseslint.configs.recommended,
|
|
||||||
{
|
|
||||||
rules: {
|
|
||||||
'@typescript-eslint/no-unused-vars': ['error', { 'argsIgnorePattern': '^_' }]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
1368
package-lock.json
generated
1368
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -8,8 +8,7 @@
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"type-check": "tsc --noEmit",
|
"type-check": "tsc --noEmit",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"lint": "eslint --fix ./src",
|
"lint": "prettier --write src/"
|
||||||
"style": "prettier --write src/"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@linaria/core": "^6.3.0",
|
"@linaria/core": "^6.3.0",
|
||||||
|
|
@ -28,10 +27,8 @@
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"@wyw-in-js/vite": "^0.6.0",
|
"@wyw-in-js/vite": "^0.6.0",
|
||||||
"eslint": "^9.27.0",
|
|
||||||
"prettier": "3.5.3",
|
"prettier": "3.5.3",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"typescript-eslint": "^8.32.1",
|
|
||||||
"vite": "^6.0.1"
|
"vite": "^6.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,181 +0,0 @@
|
||||||
import { styled } from '@linaria/react'
|
|
||||||
import { useGameStore } from '../store/useGameStore'
|
|
||||||
import { EQUIPMENT, ITEMS } from '../constants'
|
|
||||||
import Modal from './Modal'
|
|
||||||
|
|
||||||
interface EquipmentModalProps {
|
|
||||||
equipmentId: string
|
|
||||||
onClose: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const Section = styled.div`
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
`
|
|
||||||
|
|
||||||
const SectionTitle = styled.h3`
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
`
|
|
||||||
|
|
||||||
const ActionButton = styled.button<{ disabled?: boolean }>`
|
|
||||||
background-color: ${props => props.disabled ? '#9ca3af' : '#4f46e5'};
|
|
||||||
color: white;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
border: none;
|
|
||||||
cursor: ${props => props.disabled ? 'not-allowed' : 'pointer'};
|
|
||||||
font-size: 1rem;
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 1rem;
|
|
||||||
`
|
|
||||||
|
|
||||||
const ProgressBar = styled.div`
|
|
||||||
width: 100%;
|
|
||||||
height: 0.5rem;
|
|
||||||
background-color: #e5e7eb;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
overflow: hidden;
|
|
||||||
`
|
|
||||||
|
|
||||||
const ProgressFill = styled.div<{ width: number }>`
|
|
||||||
height: 100%;
|
|
||||||
background-color: #4f46e5;
|
|
||||||
transition: width 0.1s linear;
|
|
||||||
width: ${props => props.width}%;
|
|
||||||
`
|
|
||||||
|
|
||||||
const RecipeButton = styled.button<{ selected?: boolean; disabled?: boolean }>`
|
|
||||||
background-color: ${props => props.selected ? '#e0e7ff' : '#f3f4f6'};
|
|
||||||
border: 1px solid ${props => props.selected ? '#4f46e5' : '#d1d5db'};
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
padding: 0.75rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
cursor: ${props => props.disabled ? 'not-allowed' : 'pointer'};
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
opacity: ${props => props.disabled ? 0.5 : 1};
|
|
||||||
`
|
|
||||||
|
|
||||||
const RecipeIcon = styled.span`
|
|
||||||
font-size: 1.25rem;
|
|
||||||
`
|
|
||||||
|
|
||||||
const RecipeInfo = styled.div`
|
|
||||||
flex: 1;
|
|
||||||
`
|
|
||||||
|
|
||||||
const RecipeName = styled.div`
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
`
|
|
||||||
|
|
||||||
const RecipeDetails = styled.div`
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: #6b7280;
|
|
||||||
`
|
|
||||||
|
|
||||||
const EquipmentModal = ({ equipmentId, onClose }: EquipmentModalProps) => {
|
|
||||||
const { inventory, configureEquipment, useEquipment, actionCooldown, equipment } = useGameStore()
|
|
||||||
const equipmentInstance = equipment[equipmentId]
|
|
||||||
const equipmentDef = EQUIPMENT[equipmentInstance.type]
|
|
||||||
|
|
||||||
const handleUse = () => {
|
|
||||||
useEquipment(equipmentInstance.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleRecipeSelect = (recipeId: string) => {
|
|
||||||
configureEquipment(equipmentInstance.id, recipeId)
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedRecipe = equipmentDef.recipes.find(r => r.id === equipmentInstance.selectedRecipeId)
|
|
||||||
const progress = selectedRecipe ? (equipmentInstance.progress / selectedRecipe.processTicks) * 100 : 0
|
|
||||||
|
|
||||||
const canUse =
|
|
||||||
!equipmentInstance.isProcessing &&
|
|
||||||
actionCooldown === 0 &&
|
|
||||||
selectedRecipe &&
|
|
||||||
(inventory[selectedRecipe.inputItem] || 0) >= selectedRecipe.inputAmount
|
|
||||||
|
|
||||||
const modalTitle = (
|
|
||||||
<>
|
|
||||||
<span>{equipmentDef.emoji}</span>
|
|
||||||
{equipmentDef.name}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal onClose={onClose} title={modalTitle}>
|
|
||||||
<Section>
|
|
||||||
<SectionTitle>Select Recipe</SectionTitle>
|
|
||||||
{equipmentDef.recipes.map((recipe) => (
|
|
||||||
<RecipeButton
|
|
||||||
key={recipe.id}
|
|
||||||
selected={recipe.id === equipmentInstance.selectedRecipeId}
|
|
||||||
onClick={() => handleRecipeSelect(recipe.id)}
|
|
||||||
disabled={equipmentInstance.isProcessing}
|
|
||||||
>
|
|
||||||
<RecipeIcon>
|
|
||||||
{ITEMS[recipe.inputItem].emoji} → {ITEMS[recipe.outputItem].emoji}
|
|
||||||
</RecipeIcon>
|
|
||||||
<RecipeInfo>
|
|
||||||
<RecipeName>{recipe.name}</RecipeName>
|
|
||||||
<RecipeDetails>
|
|
||||||
{recipe.inputAmount} {ITEMS[recipe.inputItem].name} → {recipe.outputAmount} {ITEMS[recipe.outputItem].name}
|
|
||||||
<br />
|
|
||||||
Time: {recipe.processTicks} ticks, Cooldown: {recipe.cooldownDuration / 1000}s
|
|
||||||
</RecipeDetails>
|
|
||||||
</RecipeInfo>
|
|
||||||
</RecipeButton>
|
|
||||||
))}
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
{selectedRecipe && (
|
|
||||||
<>
|
|
||||||
<Section>
|
|
||||||
<SectionTitle>Status</SectionTitle>
|
|
||||||
{equipmentInstance.isProcessing ? (
|
|
||||||
<>
|
|
||||||
<p>Processing {selectedRecipe.name}...</p>
|
|
||||||
<ProgressBar>
|
|
||||||
<ProgressFill width={progress} />
|
|
||||||
</ProgressBar>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<p>Ready to use</p>
|
|
||||||
)}
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Section>
|
|
||||||
<SectionTitle>Requirements</SectionTitle>
|
|
||||||
<p>
|
|
||||||
Input: {selectedRecipe.inputAmount} {ITEMS[selectedRecipe.inputItem].name}
|
|
||||||
{' '}
|
|
||||||
(You have: {inventory[selectedRecipe.inputItem] || 0})
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Output: {selectedRecipe.outputAmount} {ITEMS[selectedRecipe.outputItem].name}
|
|
||||||
</p>
|
|
||||||
<p>Processing time: {selectedRecipe.processTicks} ticks</p>
|
|
||||||
<p>Cooldown: {selectedRecipe.cooldownDuration / 1000} seconds</p>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<ActionButton
|
|
||||||
disabled={!canUse}
|
|
||||||
onClick={handleUse}
|
|
||||||
>
|
|
||||||
{equipmentInstance.isProcessing ? 'Processing...' : 'Use'}
|
|
||||||
</ActionButton>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Modal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default EquipmentModal
|
|
||||||
|
|
@ -2,7 +2,6 @@ import React, { useState } from 'react'
|
||||||
import { useGameStore } from '../store/useGameStore'
|
import { useGameStore } from '../store/useGameStore'
|
||||||
import { CROPS } from '../constants'
|
import { CROPS } from '../constants'
|
||||||
import { FieldTool, PlotState } from '../types'
|
import { FieldTool, PlotState } from '../types'
|
||||||
import Modal from './Modal'
|
|
||||||
|
|
||||||
const fieldContainerStyle: React.CSSProperties = {
|
const fieldContainerStyle: React.CSSProperties = {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|
@ -147,6 +146,39 @@ const getMaturityProgressBarStyle = (
|
||||||
boxShadow: '0 0 3px rgba(0, 0, 0, 0.5)',
|
boxShadow: '0 0 3px rgba(0, 0, 0, 0.5)',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const modalOverlayStyle: React.CSSProperties = {
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
zIndex: 1000,
|
||||||
|
}
|
||||||
|
|
||||||
|
const modalContentStyle: React.CSSProperties = {
|
||||||
|
backgroundColor: 'white',
|
||||||
|
padding: '2rem',
|
||||||
|
borderRadius: '0.5rem',
|
||||||
|
maxWidth: '400px',
|
||||||
|
width: '90%',
|
||||||
|
position: 'relative',
|
||||||
|
}
|
||||||
|
|
||||||
|
const modalCloseButtonStyle: React.CSSProperties = {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '0.5rem',
|
||||||
|
right: '0.5rem',
|
||||||
|
padding: '0.5rem',
|
||||||
|
border: 'none',
|
||||||
|
background: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '1.5rem',
|
||||||
|
}
|
||||||
|
|
||||||
const plotInfoStyle: React.CSSProperties = {
|
const plotInfoStyle: React.CSSProperties = {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
|
|
@ -171,7 +203,12 @@ const PlotInfoModal: React.FC<PlotInfoModalProps> = ({ plot, onClose }) => {
|
||||||
const formatPercentage = (value: number) => `${Math.round(value * 100)}%`
|
const formatPercentage = (value: number) => `${Math.round(value * 100)}%`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal onClose={onClose} title="Plot Information">
|
<div style={modalOverlayStyle} onClick={onClose}>
|
||||||
|
<div style={modalContentStyle} onClick={(e) => e.stopPropagation()}>
|
||||||
|
<button style={modalCloseButtonStyle} onClick={onClose}>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
<h3 style={{ marginTop: 0 }}>Plot Information</h3>
|
||||||
<div style={plotInfoStyle}>
|
<div style={plotInfoStyle}>
|
||||||
<div style={plotInfoItemStyle}>
|
<div style={plotInfoItemStyle}>
|
||||||
<span>Current Crop:</span>
|
<span>Current Crop:</span>
|
||||||
|
|
@ -207,7 +244,8 @@ const PlotInfoModal: React.FC<PlotInfoModalProps> = ({ plot, onClose }) => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import React, {
|
import React, {
|
||||||
|
useEffect,
|
||||||
useState,
|
useState,
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
forwardRef,
|
forwardRef,
|
||||||
|
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import { styled } from '@linaria/react'
|
|
||||||
|
|
||||||
interface ModalProps {
|
|
||||||
onClose: () => void
|
|
||||||
children: React.ReactNode
|
|
||||||
title?: React.ReactNode
|
|
||||||
maxWidth?: string
|
|
||||||
minWidth?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const ModalOverlay = styled.div`
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
z-index: 1000;
|
|
||||||
`
|
|
||||||
|
|
||||||
const ModalContent = styled.div<{ maxWidth?: string; minWidth?: string }>`
|
|
||||||
background-color: white;
|
|
||||||
padding: 2rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
min-width: ${props => props.minWidth || '300px'};
|
|
||||||
max-width: ${props => props.maxWidth || '500px'};
|
|
||||||
width: 90%;
|
|
||||||
position: relative;
|
|
||||||
`
|
|
||||||
|
|
||||||
const CloseButton = styled.button`
|
|
||||||
position: absolute;
|
|
||||||
top: 0.5rem;
|
|
||||||
right: 0.5rem;
|
|
||||||
padding: 0.5rem;
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
color: #6b7280;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 2rem;
|
|
||||||
height: 2rem;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: #f3f4f6;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const Title = styled.h2`
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
padding-right: 2rem;
|
|
||||||
`
|
|
||||||
|
|
||||||
const Modal: React.FC<ModalProps> = ({ onClose, children, title, maxWidth, minWidth }) => {
|
|
||||||
return (
|
|
||||||
<ModalOverlay onClick={onClose}>
|
|
||||||
<ModalContent onClick={(e) => e.stopPropagation()} maxWidth={maxWidth} minWidth={minWidth}>
|
|
||||||
<CloseButton onClick={onClose}>×</CloseButton>
|
|
||||||
{title && <Title>{title}</Title>}
|
|
||||||
{children}
|
|
||||||
</ModalContent>
|
|
||||||
</ModalOverlay>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Modal
|
|
||||||
|
|
@ -1,139 +1,68 @@
|
||||||
import React, { useState } from 'react'
|
import React from 'react'
|
||||||
import { styled } from '@linaria/react'
|
|
||||||
import { useGameStore } from '../store/useGameStore'
|
import { useGameStore } from '../store/useGameStore'
|
||||||
import { ITEMS, EQUIPMENT } from '../constants'
|
import { ITEMS } from '../constants'
|
||||||
import EquipmentModal from './EquipmentModal'
|
|
||||||
import { Equipment } from '../types'
|
|
||||||
|
|
||||||
const WarehouseContainer = styled.div`
|
const warehouseContainerStyle: React.CSSProperties = {
|
||||||
padding: 1rem;
|
padding: '1rem',
|
||||||
`
|
}
|
||||||
|
|
||||||
const Title = styled.h2`
|
const titleStyle: React.CSSProperties = {
|
||||||
font-size: 1.5rem;
|
fontSize: '1.5rem',
|
||||||
font-weight: bold;
|
fontWeight: 'bold',
|
||||||
margin-bottom: 1rem;
|
marginBottom: '1rem',
|
||||||
text-align: center;
|
textAlign: 'center',
|
||||||
`
|
}
|
||||||
|
|
||||||
const CashDisplay = styled.div`
|
const cashDisplayStyle: React.CSSProperties = {
|
||||||
text-align: center;
|
textAlign: 'center',
|
||||||
margin-bottom: 1.5rem;
|
marginBottom: '1.5rem',
|
||||||
padding: 1rem;
|
padding: '1rem',
|
||||||
background-color: #dcfce7;
|
backgroundColor: '#dcfce7',
|
||||||
border-radius: 0.375rem;
|
borderRadius: '0.375rem',
|
||||||
`
|
}
|
||||||
|
|
||||||
const CashAmount = styled.h3`
|
const cashAmountStyle: React.CSSProperties = {
|
||||||
font-size: 1.25rem;
|
fontSize: '1.25rem',
|
||||||
font-weight: 500;
|
fontWeight: 500,
|
||||||
`
|
}
|
||||||
|
|
||||||
const InventoryTitle = styled.h3`
|
const inventoryTitleStyle: React.CSSProperties = {
|
||||||
font-size: 1.25rem;
|
fontSize: '1.25rem',
|
||||||
font-weight: 500;
|
fontWeight: 500,
|
||||||
margin-bottom: 0.5rem;
|
marginBottom: '0.5rem',
|
||||||
`
|
}
|
||||||
|
|
||||||
const InventoryGrid = styled.div`
|
const getInventoryGridStyle = (): React.CSSProperties => {
|
||||||
display: grid;
|
const style: React.CSSProperties = {
|
||||||
grid-template-columns: repeat(2, 1fr);
|
display: 'grid',
|
||||||
gap: 1rem;
|
gridTemplateColumns: 'repeat(2, 1fr)',
|
||||||
margin-top: 1rem;
|
gap: '1rem',
|
||||||
`
|
marginTop: '1rem',
|
||||||
|
}
|
||||||
|
|
||||||
const InventoryItem = styled.div`
|
return style
|
||||||
background-color: #f3f4f6;
|
}
|
||||||
border: 1px solid #d1d5db;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
padding: 1rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
|
||||||
`
|
|
||||||
|
|
||||||
const ItemIcon = styled.div`
|
const inventoryItemStyle: React.CSSProperties = {
|
||||||
font-size: 1.5rem;
|
backgroundColor: '#f3f4f6',
|
||||||
margin-bottom: 0.5rem;
|
border: '1px solid #d1d5db',
|
||||||
`
|
borderRadius: '0.5rem',
|
||||||
|
padding: '1rem',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
textAlign: 'center',
|
||||||
|
}
|
||||||
|
|
||||||
const ItemName = styled.h4`
|
const itemIconStyle: React.CSSProperties = {
|
||||||
font-weight: 500;
|
fontSize: '1.5rem',
|
||||||
margin-bottom: 0.25rem;
|
marginBottom: '0.5rem',
|
||||||
`
|
}
|
||||||
|
|
||||||
const EquipmentTitle = styled.h3`
|
const itemNameStyle: React.CSSProperties = {
|
||||||
font-size: 1.25rem;
|
fontWeight: 500,
|
||||||
font-weight: 500;
|
marginBottom: '0.25rem',
|
||||||
margin-top: 2rem;
|
}
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
`
|
|
||||||
|
|
||||||
const EquipmentGrid = styled.div`
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
||||||
gap: 1rem;
|
|
||||||
margin-top: 1rem;
|
|
||||||
`
|
|
||||||
|
|
||||||
const EquipmentItem = styled.div`
|
|
||||||
background-color: #f3f4f6;
|
|
||||||
border: 1px solid #d1d5db;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
padding: 1rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
|
||||||
cursor: pointer;
|
|
||||||
position: relative;
|
|
||||||
`
|
|
||||||
|
|
||||||
const ProgressBarContainer = styled.div`
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 0.25rem;
|
|
||||||
background-color: #e5e7eb;
|
|
||||||
border-top-left-radius: 0.5rem;
|
|
||||||
border-top-right-radius: 0.5rem;
|
|
||||||
overflow: hidden;
|
|
||||||
`
|
|
||||||
|
|
||||||
const ProgressFill = styled.div<{ width: string }>`
|
|
||||||
height: 100%;
|
|
||||||
background-color: #4f46e5;
|
|
||||||
width: ${props => props.width};
|
|
||||||
`
|
|
||||||
|
|
||||||
const EquipmentIcon = styled.div`
|
|
||||||
font-size: 2rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
`
|
|
||||||
|
|
||||||
const EquipmentName = styled.h4`
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
`
|
|
||||||
|
|
||||||
const EquipmentStatus = styled.p`
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: #6b7280;
|
|
||||||
`
|
|
||||||
|
|
||||||
const BuyEquipmentButton = styled.button<{ disabled?: boolean }>`
|
|
||||||
background-color: ${props => props.disabled ? '#9ca3af' : '#4f46e5'};
|
|
||||||
color: white;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
border: none;
|
|
||||||
cursor: ${props => props.disabled ? 'not-allowed' : 'pointer'};
|
|
||||||
font-size: 1rem;
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 1rem;
|
|
||||||
`
|
|
||||||
|
|
||||||
const formatItemName = (id: string) => {
|
const formatItemName = (id: string) => {
|
||||||
return ITEMS[id]?.name || id
|
return ITEMS[id]?.name || id
|
||||||
|
|
@ -144,104 +73,31 @@ const getItemIcon = (id: string) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const WarehouseComponent: React.FC = () => {
|
const WarehouseComponent: React.FC = () => {
|
||||||
const { inventory, cash, equipment, buyEquipment } = useGameStore()
|
const { inventory, cash } = useGameStore()
|
||||||
const [selectedEquipment, setSelectedEquipment] = useState<Equipment | null>(null)
|
|
||||||
|
|
||||||
const handleEquipmentClick = (equipment: Equipment) => {
|
|
||||||
setSelectedEquipment(equipment)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleBuyEquipment = (equipmentId: string) => {
|
|
||||||
buyEquipment(equipmentId)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WarehouseContainer>
|
<div style={warehouseContainerStyle}>
|
||||||
<Title>Warehouse</Title>
|
<h2 style={titleStyle}>Warehouse</h2>
|
||||||
|
|
||||||
<CashDisplay>
|
<div style={cashDisplayStyle}>
|
||||||
<CashAmount>Cash: ${cash.toFixed(2)}</CashAmount>
|
<h3 style={cashAmountStyle}>Cash: ${cash.toFixed(2)}</h3>
|
||||||
</CashDisplay>
|
</div>
|
||||||
|
|
||||||
<InventoryTitle>Inventory:</InventoryTitle>
|
<h3 style={inventoryTitleStyle}>Inventory:</h3>
|
||||||
|
|
||||||
<InventoryGrid>
|
<div style={getInventoryGridStyle()}>
|
||||||
{Object.entries(inventory).map(
|
{Object.entries(inventory).map(
|
||||||
([id, count]) =>
|
([id, count]) =>
|
||||||
count > 0 && (
|
count > 0 && (
|
||||||
<InventoryItem key={id}>
|
<div key={id} style={inventoryItemStyle}>
|
||||||
<ItemIcon>{getItemIcon(id)}</ItemIcon>
|
<div style={itemIconStyle}>{getItemIcon(id)}</div>
|
||||||
<ItemName>{formatItemName(id)}</ItemName>
|
<h4 style={itemNameStyle}>{formatItemName(id)}</h4>
|
||||||
<p>Quantity: {count}</p>
|
<p>Quantity: {count}</p>
|
||||||
</InventoryItem>
|
</div>
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
</InventoryGrid>
|
</div>
|
||||||
|
</div>
|
||||||
<EquipmentTitle>Equipment:</EquipmentTitle>
|
|
||||||
|
|
||||||
<EquipmentGrid>
|
|
||||||
{/* Available equipment to buy */}
|
|
||||||
{Object.entries(EQUIPMENT).map(([id, equipmentDef]) => {
|
|
||||||
const canBuy = cash >= equipmentDef.cost
|
|
||||||
return (
|
|
||||||
<EquipmentItem key={`buy_${id}`}>
|
|
||||||
<EquipmentIcon>{equipmentDef.emoji}</EquipmentIcon>
|
|
||||||
<EquipmentName>{equipmentDef.name}</EquipmentName>
|
|
||||||
<EquipmentStatus>Cost: ${equipmentDef.cost}</EquipmentStatus>
|
|
||||||
<BuyEquipmentButton
|
|
||||||
disabled={!canBuy}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
handleBuyEquipment(id)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Buy
|
|
||||||
</BuyEquipmentButton>
|
|
||||||
</EquipmentItem>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
|
|
||||||
{/* Owned equipment */}
|
|
||||||
{Object.entries(equipment).map(([id, equipmentInstance]) => {
|
|
||||||
const recipe = equipmentInstance.selectedRecipeId
|
|
||||||
? EQUIPMENT[equipmentInstance.type].recipes.find(r => r.id === equipmentInstance.selectedRecipeId)
|
|
||||||
: null
|
|
||||||
const progress = recipe && equipmentInstance.isProcessing
|
|
||||||
? (equipmentInstance.progress / recipe.processTicks) * 100
|
|
||||||
: 0
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EquipmentItem
|
|
||||||
key={id}
|
|
||||||
onClick={() => handleEquipmentClick(equipmentInstance)}
|
|
||||||
>
|
|
||||||
{equipmentInstance.isProcessing && (
|
|
||||||
<ProgressBarContainer>
|
|
||||||
<ProgressFill width={`${progress}%`} />
|
|
||||||
</ProgressBarContainer>
|
|
||||||
)}
|
|
||||||
<EquipmentIcon>{equipmentInstance.emoji}</EquipmentIcon>
|
|
||||||
<EquipmentName>{equipmentInstance.name}</EquipmentName>
|
|
||||||
<EquipmentStatus>
|
|
||||||
{equipmentInstance.isProcessing
|
|
||||||
? 'Processing...'
|
|
||||||
: equipmentInstance.selectedRecipeId
|
|
||||||
? `Ready (${recipe?.name})`
|
|
||||||
: 'Not configured'}
|
|
||||||
</EquipmentStatus>
|
|
||||||
</EquipmentItem>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</EquipmentGrid>
|
|
||||||
|
|
||||||
{selectedEquipment && (
|
|
||||||
<EquipmentModal
|
|
||||||
equipmentId={selectedEquipment.id}
|
|
||||||
onClose={() => setSelectedEquipment(null)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</WarehouseContainer>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { CropDefinitions, Upgrade, Equipment } from '../types'
|
import { CropDefinitions, Upgrade } from '../types'
|
||||||
|
|
||||||
export const INITIAL_CASH = 50
|
export const INITIAL_CASH = 50
|
||||||
export const INITIAL_FIELD_SIZE = 3
|
export const INITIAL_FIELD_SIZE = 3
|
||||||
|
|
@ -116,40 +116,6 @@ export interface Item {
|
||||||
sellPrice: number | null // null means not sellable
|
sellPrice: number | null // null means not sellable
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EQUIPMENT: Record<string, Equipment> = {
|
|
||||||
press: {
|
|
||||||
id: 'press',
|
|
||||||
type: 'press',
|
|
||||||
name: 'Press',
|
|
||||||
emoji: '🛢️',
|
|
||||||
cost: 100,
|
|
||||||
recipes: [
|
|
||||||
{
|
|
||||||
id: 'olive_oil',
|
|
||||||
name: 'Olive Oil',
|
|
||||||
inputItem: 'olive',
|
|
||||||
inputAmount: 10,
|
|
||||||
outputItem: 'olive_oil',
|
|
||||||
outputAmount: 1,
|
|
||||||
processTicks: 10,
|
|
||||||
cooldownDuration: 10000, // 10 seconds
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'grape_juice',
|
|
||||||
name: 'Grape Juice',
|
|
||||||
inputItem: 'grape',
|
|
||||||
inputAmount: 20,
|
|
||||||
outputItem: 'grape_juice',
|
|
||||||
outputAmount: 1,
|
|
||||||
processTicks: 20,
|
|
||||||
cooldownDuration: 15000, // 15 seconds
|
|
||||||
},
|
|
||||||
],
|
|
||||||
progress: 0,
|
|
||||||
isProcessing: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ITEMS: Record<string, Item> = {
|
export const ITEMS: Record<string, Item> = {
|
||||||
additional_land: {
|
additional_land: {
|
||||||
id: 'additional_land',
|
id: 'additional_land',
|
||||||
|
|
@ -193,32 +159,4 @@ export const ITEMS: Record<string, Item> = {
|
||||||
buyPrice: 15,
|
buyPrice: 15,
|
||||||
sellPrice: null,
|
sellPrice: null,
|
||||||
},
|
},
|
||||||
olive_oil: {
|
|
||||||
id: 'olive_oil',
|
|
||||||
name: 'Olive Oil',
|
|
||||||
emoji: '🫒',
|
|
||||||
buyPrice: null,
|
|
||||||
sellPrice: 50,
|
|
||||||
},
|
|
||||||
grape: {
|
|
||||||
id: 'grape',
|
|
||||||
name: 'Grape',
|
|
||||||
emoji: '🍇',
|
|
||||||
buyPrice: null,
|
|
||||||
sellPrice: 20,
|
|
||||||
},
|
|
||||||
grape_juice: {
|
|
||||||
id: 'grape_juice',
|
|
||||||
name: 'Grape Juice',
|
|
||||||
emoji: '🍇juice',
|
|
||||||
buyPrice: null,
|
|
||||||
sellPrice: 100,
|
|
||||||
},
|
|
||||||
wine: {
|
|
||||||
id: 'wine',
|
|
||||||
name: 'Wine',
|
|
||||||
emoji: '🍷',
|
|
||||||
buyPrice: null,
|
|
||||||
sellPrice: 100,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,8 @@ import {
|
||||||
COOLDOWN_DURATION,
|
COOLDOWN_DURATION,
|
||||||
ITEMS,
|
ITEMS,
|
||||||
UPGRADES,
|
UPGRADES,
|
||||||
EQUIPMENT,
|
|
||||||
} from '../constants'
|
} from '../constants'
|
||||||
import { GameState, PlotState, Equipment } from '../types'
|
import { GameState, PlotState } from '../types'
|
||||||
|
|
||||||
const initializeField = (size: number): PlotState[][] => {
|
const initializeField = (size: number): PlotState[][] => {
|
||||||
return Array(size)
|
return Array(size)
|
||||||
|
|
@ -113,9 +112,6 @@ export const useGameStore = create<
|
||||||
loadFromSlot: (slot: number) => void
|
loadFromSlot: (slot: number) => void
|
||||||
purchaseUpgrade: (upgradeId: string) => void
|
purchaseUpgrade: (upgradeId: string) => void
|
||||||
pray: () => void
|
pray: () => void
|
||||||
buyEquipment: (equipmentId: string) => void
|
|
||||||
configureEquipment: (equipmentId: string, recipeId: string) => void
|
|
||||||
useEquipment: (equipmentId: string) => void
|
|
||||||
}
|
}
|
||||||
>((set, get) => ({
|
>((set, get) => ({
|
||||||
cash: INITIAL_CASH,
|
cash: INITIAL_CASH,
|
||||||
|
|
@ -131,7 +127,6 @@ export const useGameStore = create<
|
||||||
purchasedUpgrades: [],
|
purchasedUpgrades: [],
|
||||||
piety: 50,
|
piety: 50,
|
||||||
landPurchases: 0,
|
landPurchases: 0,
|
||||||
equipment: {},
|
|
||||||
|
|
||||||
assignCrop: (row, col, cropId) => {
|
assignCrop: (row, col, cropId) => {
|
||||||
set(
|
set(
|
||||||
|
|
@ -346,36 +341,6 @@ export const useGameStore = create<
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Update equipment progress
|
|
||||||
Object.entries(state.equipment).forEach(([, equipment]) => {
|
|
||||||
// TODO: this type cast shouldn't be necessary
|
|
||||||
const equipmentInstance = equipment as Equipment
|
|
||||||
if (equipmentInstance.isProcessing) {
|
|
||||||
const recipe = EQUIPMENT[equipmentInstance.type].recipes.find(
|
|
||||||
r => r.id === equipmentInstance.selectedRecipeId
|
|
||||||
)
|
|
||||||
if (!recipe) return
|
|
||||||
|
|
||||||
equipmentInstance.progress += 1
|
|
||||||
|
|
||||||
// Check if processing is complete
|
|
||||||
if (equipmentInstance.progress >= recipe.processTicks) {
|
|
||||||
equipmentInstance.isProcessing = false
|
|
||||||
equipmentInstance.progress = 0
|
|
||||||
|
|
||||||
// Add output items
|
|
||||||
state.inventory[recipe.outputItem] =
|
|
||||||
(state.inventory[recipe.outputItem] || 0) +
|
|
||||||
recipe.outputAmount
|
|
||||||
|
|
||||||
addConsoleMessage(
|
|
||||||
state,
|
|
||||||
`${EQUIPMENT[equipmentInstance.type].name} produced ${recipe.outputAmount} ${ITEMS[recipe.outputItem].name}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
state.tickCount = state.tickCount + 1
|
state.tickCount = state.tickCount + 1
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
@ -421,7 +386,7 @@ export const useGameStore = create<
|
||||||
},
|
},
|
||||||
|
|
||||||
buyItem: (itemId) => {
|
buyItem: (itemId) => {
|
||||||
const { cash, landPurchases } = get()
|
const { cash, landPurchases, plots, fieldSize } = get()
|
||||||
const item = ITEMS[itemId]
|
const item = ITEMS[itemId]
|
||||||
|
|
||||||
if (!item || item.buyPrice === null || item.buyPrice === undefined) {
|
if (!item || item.buyPrice === null || item.buyPrice === undefined) {
|
||||||
|
|
@ -499,7 +464,23 @@ export const useGameStore = create<
|
||||||
|
|
||||||
saveToSlot: (slot: number) => {
|
saveToSlot: (slot: number) => {
|
||||||
const state = get()
|
const state = get()
|
||||||
const { ...gameState } = state
|
const {
|
||||||
|
plant,
|
||||||
|
water,
|
||||||
|
harvest,
|
||||||
|
tick,
|
||||||
|
assignCrop,
|
||||||
|
upgradeField,
|
||||||
|
setGameSpeed,
|
||||||
|
setActionCooldown,
|
||||||
|
buyItem,
|
||||||
|
sellItem,
|
||||||
|
saveToSlot,
|
||||||
|
loadFromSlot,
|
||||||
|
purchaseUpgrade,
|
||||||
|
pray,
|
||||||
|
...gameState
|
||||||
|
} = state
|
||||||
saveGame(slot, gameState)
|
saveGame(slot, gameState)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -579,87 +560,4 @@ export const useGameStore = create<
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
buyEquipment: (equipmentId) => {
|
|
||||||
const { cash, equipment } = get()
|
|
||||||
const equipmentDef = EQUIPMENT[equipmentId]
|
|
||||||
|
|
||||||
if (!equipmentDef || cash < equipmentDef.cost) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a unique ID for this instance of equipment
|
|
||||||
const instanceId = `${equipmentId}_${Object.keys(equipment).length + 1}`
|
|
||||||
|
|
||||||
set(
|
|
||||||
produce((state) => {
|
|
||||||
state.cash -= equipmentDef.cost
|
|
||||||
state.equipment[instanceId] = {
|
|
||||||
id: instanceId,
|
|
||||||
type: equipmentId,
|
|
||||||
progress: 0,
|
|
||||||
isProcessing: false,
|
|
||||||
}
|
|
||||||
addConsoleMessage(state, `Purchased ${equipmentDef.name}`)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|
|
||||||
configureEquipment: (equipmentId, recipeId) => {
|
|
||||||
const { equipment } = get()
|
|
||||||
const equipmentInstance = equipment[equipmentId]
|
|
||||||
|
|
||||||
if (!equipmentInstance || equipmentInstance.isProcessing) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the recipe exists for this equipment
|
|
||||||
const recipe = EQUIPMENT[equipmentInstance.type].recipes.find(r => r.id === recipeId)
|
|
||||||
if (!recipe) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
set(
|
|
||||||
produce((state) => {
|
|
||||||
state.equipment[equipmentId].selectedRecipeId = recipeId
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|
|
||||||
useEquipment: (equipmentId) => {
|
|
||||||
const { equipment, inventory, actionCooldown } = get()
|
|
||||||
const equipmentInstance = equipment[equipmentId]
|
|
||||||
|
|
||||||
if (
|
|
||||||
!equipmentInstance ||
|
|
||||||
equipmentInstance.isProcessing ||
|
|
||||||
actionCooldown > 0 ||
|
|
||||||
!equipmentInstance.selectedRecipeId
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const recipe = EQUIPMENT[equipmentInstance.type].recipes.find(r => r.id === equipmentInstance.selectedRecipeId)
|
|
||||||
if (!recipe) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we have enough input items
|
|
||||||
const inputCount = inventory[recipe.inputItem] || 0
|
|
||||||
if (inputCount < recipe.inputAmount) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
set(
|
|
||||||
produce((state) => {
|
|
||||||
// Deduct input items
|
|
||||||
state.inventory[recipe.inputItem] -= recipe.inputAmount
|
|
||||||
|
|
||||||
// Start processing
|
|
||||||
state.equipment[equipmentId].isProcessing = true
|
|
||||||
state.equipment[equipmentId].progress = 0
|
|
||||||
state.actionCooldown = recipe.cooldownDuration
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}))
|
}))
|
||||||
|
|
|
||||||
|
|
@ -51,33 +51,6 @@ export interface Upgrade {
|
||||||
cost: UpgradeCost[]
|
cost: UpgradeCost[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Recipe {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
inputItem: string
|
|
||||||
inputAmount: number
|
|
||||||
outputItem: string
|
|
||||||
outputAmount: number
|
|
||||||
processTicks: number
|
|
||||||
cooldownDuration: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Equipment {
|
|
||||||
id: string
|
|
||||||
type: string
|
|
||||||
name: string
|
|
||||||
emoji: string
|
|
||||||
cost: number
|
|
||||||
recipes: Recipe[]
|
|
||||||
selectedRecipeId?: string
|
|
||||||
progress: number
|
|
||||||
isProcessing: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EquipmentState {
|
|
||||||
[id: string]: Equipment
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GameState {
|
export interface GameState {
|
||||||
cash: number
|
cash: number
|
||||||
inventory: Record<string, number>
|
inventory: Record<string, number>
|
||||||
|
|
@ -91,7 +64,6 @@ export interface GameState {
|
||||||
purchasedUpgrades: string[]
|
purchasedUpgrades: string[]
|
||||||
piety: number
|
piety: number
|
||||||
landPurchases: number
|
landPurchases: number
|
||||||
equipment: EquipmentState
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MarketTransaction {
|
export interface MarketTransaction {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue