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:
2026-02-20 23:15:29 +03:30
parent bb83ab506e
commit 56bb0c6281
@@ -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>