Enhance PlantSimulator with yield and yield rate tracking
- Added yield and yield rate properties to the PlantState interface. - Implemented computeYieldRate function to calculate yield based on environmental factors and plant growth. - Updated GrowthChart component to visualize yield and yield rate alongside height and leaf count. - Modified PlantSimulator state management to include yield and yield rate history. - Enhanced UI to display current yield and yield rate in the simulator.
This commit is contained in:
@@ -31,6 +31,8 @@ interface PlantState {
|
|||||||
height: number // current stem height px (0 → MAX_HEIGHT)
|
height: number // current stem height px (0 → MAX_HEIGHT)
|
||||||
leaves: Leaf[]
|
leaves: Leaf[]
|
||||||
tick: number
|
tick: number
|
||||||
|
yield: number // accumulated yield in grams (0 → MAX_YIELD)
|
||||||
|
yieldRate: number // current yield production rate g/s
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EnvironmentSettings {
|
interface EnvironmentSettings {
|
||||||
@@ -41,6 +43,7 @@ interface EnvironmentSettings {
|
|||||||
// ─── Constants ───────────────────────────────────────────────────────────────
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const MAX_HEIGHT = 260
|
const MAX_HEIGHT = 260
|
||||||
|
const MAX_YIELD = 500 // max yield in grams
|
||||||
const SVG_WIDTH = 200
|
const SVG_WIDTH = 200
|
||||||
const SVG_HEIGHT = 320
|
const SVG_HEIGHT = 320
|
||||||
const STEM_X = SVG_WIDTH / 2
|
const STEM_X = SVG_WIDTH / 2
|
||||||
@@ -56,6 +59,16 @@ function growthRate(env: EnvironmentSettings, speed: number): number {
|
|||||||
return speed * lightFactor * waterFactor
|
return speed * lightFactor * waterFactor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Yield rate in g/s: leaves and height progress amplify yield production
|
||||||
|
function computeYieldRate(env: EnvironmentSettings, leafCount: number, heightProgress: number): number {
|
||||||
|
const lightFactor = 0.2 + (env.light / 100) * 0.8
|
||||||
|
const waterFactor = 0.2 + (env.water / 100) * 0.8
|
||||||
|
const leafFactor = leafCount / MAX_LEAVES
|
||||||
|
// yield only starts after 20% growth and accelerates with more leaves
|
||||||
|
const maturityFactor = Math.max(0, (heightProgress - 0.2) / 0.8)
|
||||||
|
return parseFloat((MAX_YIELD * 0.012 * lightFactor * waterFactor * leafFactor * maturityFactor).toFixed(3))
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Plant SVG Component ──────────────────────────────────────────────────────
|
// ─── Plant SVG Component ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
function PlantSVG({ plant, tick }: { plant: PlantState; tick: number }) {
|
function PlantSVG({ plant, tick }: { plant: PlantState; tick: number }) {
|
||||||
@@ -125,10 +138,14 @@ function PlantSVG({ plant, tick }: { plant: PlantState; tick: number }) {
|
|||||||
|
|
||||||
function GrowthChart({
|
function GrowthChart({
|
||||||
heightHistory,
|
heightHistory,
|
||||||
leafHistory
|
leafHistory,
|
||||||
|
yieldHistory,
|
||||||
|
yieldRateHistory
|
||||||
}: {
|
}: {
|
||||||
heightHistory: number[]
|
heightHistory: number[]
|
||||||
leafHistory: number[]
|
leafHistory: number[]
|
||||||
|
yieldHistory: number[]
|
||||||
|
yieldRateHistory: number[]
|
||||||
}) {
|
}) {
|
||||||
const labels = heightHistory.map((_, i) => `${i}s`)
|
const labels = heightHistory.map((_, i) => `${i}s`)
|
||||||
|
|
||||||
@@ -139,7 +156,7 @@ function GrowthChart({
|
|||||||
label: 'ارتفاع (px)',
|
label: 'ارتفاع (px)',
|
||||||
data: heightHistory,
|
data: heightHistory,
|
||||||
borderColor: '#4a7c59',
|
borderColor: '#4a7c59',
|
||||||
backgroundColor: 'rgba(74,124,89,0.15)',
|
backgroundColor: 'rgba(74,124,89,0.10)',
|
||||||
fill: true,
|
fill: true,
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
@@ -149,11 +166,32 @@ function GrowthChart({
|
|||||||
label: 'تعداد برگ',
|
label: 'تعداد برگ',
|
||||||
data: leafHistory,
|
data: leafHistory,
|
||||||
borderColor: '#f9c74f',
|
borderColor: '#f9c74f',
|
||||||
backgroundColor: 'rgba(249,199,79,0.15)',
|
backgroundColor: 'rgba(249,199,79,0.10)',
|
||||||
fill: true,
|
fill: true,
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
yAxisID: 'yLeaf'
|
yAxisID: 'yLeaf'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'محصول (g)',
|
||||||
|
data: yieldHistory,
|
||||||
|
borderColor: '#f97316',
|
||||||
|
backgroundColor: 'rgba(249,115,22,0.10)',
|
||||||
|
fill: true,
|
||||||
|
tension: 0.4,
|
||||||
|
pointRadius: 0,
|
||||||
|
yAxisID: 'yYield'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'سرعت محصول (g/s)',
|
||||||
|
data: yieldRateHistory,
|
||||||
|
borderColor: '#a78bfa',
|
||||||
|
backgroundColor: 'rgba(167,139,250,0.10)',
|
||||||
|
fill: true,
|
||||||
|
tension: 0.4,
|
||||||
|
pointRadius: 0,
|
||||||
|
borderDash: [4, 3],
|
||||||
|
yAxisID: 'yYieldRate'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -162,7 +200,7 @@ function GrowthChart({
|
|||||||
responsive: true,
|
responsive: true,
|
||||||
animation: { duration: 0 },
|
animation: { duration: 0 },
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: { labels: { color: '#e2e8f0', font: { size: 12 } } },
|
legend: { labels: { color: '#e2e8f0', font: { size: 11 } } },
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
text: 'نمودار رشد گیاه',
|
text: 'نمودار رشد گیاه',
|
||||||
@@ -192,6 +230,21 @@ function GrowthChart({
|
|||||||
ticks: { color: '#f9c74f' },
|
ticks: { color: '#f9c74f' },
|
||||||
grid: { display: false },
|
grid: { display: false },
|
||||||
title: { display: true, text: 'برگ', color: '#f9c74f' }
|
title: { display: true, text: 'برگ', color: '#f9c74f' }
|
||||||
|
},
|
||||||
|
yYield: {
|
||||||
|
type: 'linear' as const,
|
||||||
|
position: 'left' as const,
|
||||||
|
min: 0,
|
||||||
|
max: MAX_YIELD,
|
||||||
|
display: false,
|
||||||
|
grid: { display: false }
|
||||||
|
},
|
||||||
|
yYieldRate: {
|
||||||
|
type: 'linear' as const,
|
||||||
|
position: 'right' as const,
|
||||||
|
min: 0,
|
||||||
|
display: false,
|
||||||
|
grid: { display: false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,18 +259,22 @@ export default function PlantSimulator() {
|
|||||||
const [speed, setSpeed] = useState(1.5) // px per tick base
|
const [speed, setSpeed] = useState(1.5) // px per tick base
|
||||||
const [env, setEnv] = useState<EnvironmentSettings>({ light: 75, water: 65 })
|
const [env, setEnv] = useState<EnvironmentSettings>({ light: 75, water: 65 })
|
||||||
|
|
||||||
const [plant, setPlant] = useState<PlantState>({ height: 0, leaves: [], tick: 0 })
|
const [plant, setPlant] = useState<PlantState>({ height: 0, leaves: [], tick: 0, yield: 0, yieldRate: 0 })
|
||||||
const [heightHistory, setHeightHistory] = useState<number[]>([0])
|
const [heightHistory, setHeightHistory] = useState<number[]>([0])
|
||||||
const [leafHistory, setLeafHistory] = useState<number[]>([0])
|
const [leafHistory, setLeafHistory] = useState<number[]>([0])
|
||||||
|
const [yieldHistory, setYieldHistory] = useState<number[]>([0])
|
||||||
|
const [yieldRateHistory, setYieldRateHistory] = useState<number[]>([0])
|
||||||
|
|
||||||
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||||
const tickRef = useRef(0)
|
const tickRef = useRef(0)
|
||||||
const historyIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
const historyIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||||
|
|
||||||
const reset = useCallback(() => {
|
const reset = useCallback(() => {
|
||||||
setPlant({ height: 0, leaves: [], tick: 0 })
|
setPlant({ height: 0, leaves: [], tick: 0, yield: 0, yieldRate: 0 })
|
||||||
setHeightHistory([0])
|
setHeightHistory([0])
|
||||||
setLeafHistory([0])
|
setLeafHistory([0])
|
||||||
|
setYieldHistory([0])
|
||||||
|
setYieldRateHistory([0])
|
||||||
tickRef.current = 0
|
tickRef.current = 0
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@@ -258,7 +315,12 @@ export default function PlantSimulator() {
|
|||||||
scale: Math.min(l.scale + 0.025, 1)
|
scale: Math.min(l.scale + 0.025, 1)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return { height: newHeight, leaves: grownLeaves, tick: t }
|
const heightProgress = newHeight / MAX_HEIGHT
|
||||||
|
const currentYieldRate = computeYieldRate(env, grownLeaves.length, heightProgress)
|
||||||
|
// accumulate yield each tick (33ms ≈ 0.033s)
|
||||||
|
const newYield = Math.min(prev.yield + currentYieldRate * 0.033, MAX_YIELD)
|
||||||
|
|
||||||
|
return { height: newHeight, leaves: grownLeaves, tick: t, yield: newYield, yieldRate: currentYieldRate }
|
||||||
})
|
})
|
||||||
}, 33)
|
}, 33)
|
||||||
|
|
||||||
@@ -278,6 +340,8 @@ export default function PlantSimulator() {
|
|||||||
setPlant(prev => {
|
setPlant(prev => {
|
||||||
setHeightHistory(h => [...h.slice(-59), Math.round(prev.height)])
|
setHeightHistory(h => [...h.slice(-59), Math.round(prev.height)])
|
||||||
setLeafHistory(l => [...l.slice(-59), prev.leaves.length])
|
setLeafHistory(l => [...l.slice(-59), prev.leaves.length])
|
||||||
|
setYieldHistory(y => [...y.slice(-59), parseFloat(prev.yield.toFixed(1))])
|
||||||
|
setYieldRateHistory(r => [...r.slice(-59), prev.yieldRate])
|
||||||
return prev
|
return prev
|
||||||
})
|
})
|
||||||
}, 1000)
|
}, 1000)
|
||||||
@@ -312,6 +376,14 @@ export default function PlantSimulator() {
|
|||||||
<div className='text-yellow-400 font-semibold text-lg'>{plant.leaves.length}</div>
|
<div className='text-yellow-400 font-semibold text-lg'>{plant.leaves.length}</div>
|
||||||
<div className='text-slate-400'>تعداد برگ</div>
|
<div className='text-slate-400'>تعداد برگ</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className='bg-orange-900/40 border border-orange-700/40 rounded-xl p-3 text-center'>
|
||||||
|
<div className='text-orange-400 font-semibold text-lg'>{plant.yield.toFixed(1)}</div>
|
||||||
|
<div className='text-slate-400'>محصول (g)</div>
|
||||||
|
</div>
|
||||||
|
<div className='bg-violet-900/40 border border-violet-700/40 rounded-xl p-3 text-center'>
|
||||||
|
<div className='text-violet-400 font-semibold text-lg'>{plant.yieldRate.toFixed(3)}</div>
|
||||||
|
<div className='text-slate-400'>سرعت محصول (g/s)</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isFinished && (
|
{isFinished && (
|
||||||
@@ -400,10 +472,15 @@ export default function PlantSimulator() {
|
|||||||
|
|
||||||
{/* ── Right: Chart ── */}
|
{/* ── Right: Chart ── */}
|
||||||
<div className='lg:col-span-2 bg-slate-800 border border-slate-700 rounded-2xl p-6 shadow-xl'>
|
<div className='lg:col-span-2 bg-slate-800 border border-slate-700 rounded-2xl p-6 shadow-xl'>
|
||||||
<GrowthChart heightHistory={heightHistory} leafHistory={leafHistory} />
|
<GrowthChart
|
||||||
|
heightHistory={heightHistory}
|
||||||
|
leafHistory={leafHistory}
|
||||||
|
yieldHistory={yieldHistory}
|
||||||
|
yieldRateHistory={yieldRateHistory}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Info cards */}
|
{/* Info cards */}
|
||||||
<div className='mt-6 grid grid-cols-3 gap-4 text-sm'>
|
<div className='mt-6 grid grid-cols-2 lg:grid-cols-4 gap-4 text-sm'>
|
||||||
<div className='bg-slate-700/60 rounded-xl p-4 border border-slate-600'>
|
<div className='bg-slate-700/60 rounded-xl p-4 border border-slate-600'>
|
||||||
<div className='text-slate-400 mb-1'>پیشرفت رشد</div>
|
<div className='text-slate-400 mb-1'>پیشرفت رشد</div>
|
||||||
<div className='w-full bg-slate-600 rounded-full h-2 mb-1'>
|
<div className='w-full bg-slate-600 rounded-full h-2 mb-1'>
|
||||||
@@ -438,6 +515,20 @@ export default function PlantSimulator() {
|
|||||||
</div>
|
</div>
|
||||||
<div className='text-blue-400 font-semibold'>{env.water}%</div>
|
<div className='text-blue-400 font-semibold'>{env.water}%</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className='bg-slate-700/60 rounded-xl p-4 border border-slate-600'>
|
||||||
|
<div className='text-slate-400 mb-1'>محصول دهی</div>
|
||||||
|
<div className='w-full bg-slate-600 rounded-full h-2 mb-1'>
|
||||||
|
<div
|
||||||
|
className='bg-orange-400 h-2 rounded-full transition-all'
|
||||||
|
style={{ width: `${(plant.yield / MAX_YIELD) * 100}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-between items-center'>
|
||||||
|
<span className='text-orange-400 font-semibold'>{plant.yield.toFixed(1)}g</span>
|
||||||
|
<span className='text-violet-400 text-xs'>{plant.yieldRate.toFixed(3)} g/s</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
@@ -445,8 +536,9 @@ export default function PlantSimulator() {
|
|||||||
<p>
|
<p>
|
||||||
این شبیهساز رشد گیاه را بر اساس سرعت پایه، میزان نور خورشید و آب دریافتی
|
این شبیهساز رشد گیاه را بر اساس سرعت پایه، میزان نور خورشید و آب دریافتی
|
||||||
محاسبه میکند. هر برگ به صورت تدریجی روی ساقه ظاهر شده و با حرکت طبیعی
|
محاسبه میکند. هر برگ به صورت تدریجی روی ساقه ظاهر شده و با حرکت طبیعی
|
||||||
در باد نمایش داده میشود. نمودار تغییرات ارتفاع و تعداد برگها را در طول
|
در باد نمایش داده میشود. <strong className='text-slate-300'>محصولدهی (g)</strong> پس از ۲۰٪ رشد شروع شده
|
||||||
زمان ثبت میکند.
|
و با تعداد برگ، نور و آب شتاب میگیرد. <strong className='text-slate-300'>سرعت محصول (g/s)</strong> نشاندهنده
|
||||||
|
نرخ لحظهای تولید است. نمودار تغییرات همه شاخصها را در طول زمان ثبت میکند.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user