diff --git a/README.md b/README.md index cea1b25..3cea990 100644 --- a/README.md +++ b/README.md @@ -182,8 +182,12 @@ The detail lives in dedicated documents: stampers that fleet needs, drawn as a live schematic of the machine park with the stamper line (lathe, Pheenix galvanics, Sibert prep) feeding it, each station counted from the galvanics cycle times; - every assumption is editable, with its source, and a scenario can be - shared by link + plus the money a year: revenue (standard, specials, title services), + PVC, labels, sleeves, jackets, cartons, lacquers, plating chemistry, + electricity and steam, loaded labour, overheads, EBITDA, depreciation + and payback, with the revenue model's adverse, central and favourable + cases and USD, THB or EUR; every assumption is editable, with its + source, and a scenario can be shared by link - [Test site](site/README.md): `site/` builds https://cnxvinyl.lol/, the internal test site with demo pages from `docs/` (the plant operation page first) and any static site we want to share by link; Gitea Actions diff --git a/docs/finance/revenue-model.md b/docs/finance/revenue-model.md index d5c6dd9..e319b6e 100644 --- a/docs/finance/revenue-model.md +++ b/docs/finance/revenue-model.md @@ -523,7 +523,10 @@ logistics sit in overheads below. ## Operating picture at full capacity -Built from the components above rather than asserted. Three scenarios, +Built from the components above rather than asserted. The +[press fleet calculator](../fleet-calculator.html) runs the same P&L for +any fleet, output and specials share, with energy and headcount computed +from the fleet and these three cases as presets. Three scenarios, each internally consistent: adverse pairs low volume with the low price and high cost, favourable does the opposite, central takes midpoints. Title-services contribution assumes 60–70% after lacquer blanks, styli diff --git a/docs/fleet-calculator.html b/docs/fleet-calculator.html index ef13eb9..c7def95 100644 --- a/docs/fleet-calculator.html +++ b/docs/fleet-calculator.html @@ -52,6 +52,9 @@ document.documentElement.setAttribute("data-theme",t);})(); .pk-pipe { fill: none; stroke-width: 3; stroke-dasharray: 8 5; opacity: 0.85; } .pk-steam { stroke: #e2462a; } .pk-cool { stroke: #2f7de1; } .pk-hyd { stroke: #d48600; } .pk-air { stroke: #8a97b0; } .pk-pow { stroke: #c9a800; } + tr.tot td { font-weight: 600; border-top: 1px solid var(--stroke); } + td.neg { color: var(--bad); } + #ecoBar div { min-width: 0; } .themebtn { position: absolute; top: 24px; right: 20px; background: var(--panel); color: var(--fg); border: 1px solid var(--stroke); border-radius: 5px; padding: 6px 12px; font: inherit; font-size: 14px; cursor: pointer; @@ -150,7 +153,7 @@ document.documentElement.setAttribute("data-theme",t);})(); .grid > div { min-width: 0; } .tscroll { overflow-x: auto; } .tscroll table { min-width: 560px; } - #ladT { min-width: 860px; } + #ladT { min-width: 1040px; } @@ -215,6 +218,26 @@ document.documentElement.setAttribute("data-theme",t);})(); +
+

Prices and money

+
+ + + +
+

The price and cost ends of the revenue model; editing a figure makes it your own case.

+ + + +
Show money in
+
+ + + +
+

+
+

Press rates

@@ -238,6 +261,19 @@ document.documentElement.setAttribute("data-theme",t);})();
+
+

Money a year

+
+
+
+
+

Central is the revenue model's midpoint. The revenue model itself calls + that case optimistic: its EBITDA sits well above the 15 to 25 % pressing plants earn, because the cost + table still misses indirect labour, inbound freight, inventory, effluent and sales commission. Read the + adverse case as the honest one until those lines exist. Prices are a Western benchmark; Thai domestic + invoices run far higher (650 to 950 THB all-in at 300 units).

+
+

What the fleet needs

@@ -290,9 +326,16 @@ document.documentElement.setAttribute("data-theme",t);})();

Peaks add up. Steam, cooling and air are summed at each machine's peak, as Newbilt states them. A real plant staggers cycles, so the boiler and chiller can be sized lower once a utility engineer has the cycle data.

-

Not scaled here: mastering (1 to 2), QC (1 to 2), maintenance (2) and admin, sales and - logistics (3 to 5), from scale.md; cover for leave and sickness; the boiler fuel and the PV - share of the power.

+

Money. Prices, per-record costs, wages, overheads and depreciation come from + finance/revenue-model.md, capex from finance/total-investment.md (the start fleet plan plus each + Duplex system or Classic added or removed), the electricity tariff and PV from scale.md, the biomass + heat price from biomass.md. Electricity and steam follow the fleet: machines, chiller and compressors + at the hours run, steam from the presses' running time, plus an estimated base load for the rest of + the plant. Lacquer blanks, shipping cartons and the cutting engineer's wage have no figure on file + and are estimates. Missing as the revenue model lists: indirect labour, freight, inventory, effluent, + commission, extra galvanics units' capex.

+

Not scaled here: admin, sales and logistics (in the overheads line); cover for leave + and sickness.

Sources in the CNX Vinyl planning repository: equipment/pressing-line.md, equipment/newbilt/ (Duplex site requirements V14, offer 240620-04, 2026 flyers), equipment/galvanics.md (cycle times per step; the stamper line counts each station from them, @@ -307,6 +350,7 @@ document.documentElement.setAttribute("data-theme",t);})(); const S0 = { mode: "target", target: 1000000, spec: 20, grow: "classic", dMin: 2, cMin: 1, dFleet: 2, cFleet: 1, shifts: 2, hrs: 8, days: 300, eff: 65, rateD: 3, rateC: 2, specF: 100, + priceStd: 3.8, premSpec: 1.15, titleFee: 1155, cur: "USD", }; // [group, id, label, value, unit, kind, source] const AS = []; @@ -375,6 +419,46 @@ grp("Mastering, galvanics and stamper prep", [ ["vspMin", "Centre punch per stamper (Sibert VSP)", 4, "min", "est", "galvanics.md: 3 to 5 min, estimate"], ["vscMin", "Coin per stamper (Sibert VSC)", 2, "min", "est", "galvanics.md: 1 to 2 min, estimate"], ]); +grp("Money", [ + ["thbUsd", "THB per USD", 33, "THB", "doc", "revenue-model.md and total-investment.md, FX of 5 August 2026"], + ["usdEur", "USD per EUR", 1.155, "USD", "doc", "revenue-model.md"], + ["overheads", "Cash overheads a year", 330000, "USD", "doc", "revenue-model.md: 200 to 450k (admin, sales, insurance, facility, IT, fees, marketing)"], + ["capexBase", "Capex, start fleet plan", 6800000, "USD", "doc", "total-investment.md: 4.7 to 8.9M full vision; added scope not yet rolled up"], + ["capexD", "Capex per Duplex system beyond two", 265000, "USD", "doc", "total-investment.md: system 2 ~230 to 300k, estimate"], + ["capexC", "Capex per Classic beyond one", 375000, "USD", "doc", "total-investment.md: ~290 to 460k, no Newbilt price on file"], + ["deprYears", "Depreciation period", 12.5, "years", "doc", "revenue-model.md: 10 to 15 years"], +]); +grp("Materials and packaging", [ + ["pvcPrice", "PVC compound", 2.3, "USD/kg", "doc", "revenue-model.md: 1.75 to 2.90 USD/kg record grade"], + ["labelCost", "Labels, a pair", 0.06, "USD", "doc", "revenue-model.md: 0.03 to 0.09"], + ["sleeveCost", "Inner sleeve", 0.13, "USD", "doc", "revenue-model.md: 0.09 to 0.17, poly-lined"], + ["jacketCost", "Jacket (printed carton)", 0.66, "USD", "doc", "revenue-model.md: 0.40 to 0.92, local carton partners"], + ["shrinkCost", "Shrink wrap", 0.025, "USD", "doc", "revenue-model.md: 0.02 to 0.03"], + ["cartonCost", "Shipping cartons and pallets, a record", 0.04, "USD", "est", "Estimate: a 25-record shipping carton at about 1 USD"], + ["maintCost", "Maintenance, spares, consumables, a record", 0.17, "USD", "doc", "revenue-model.md: 0.12 to 0.23"], +]); +grp("Stamper making", [ + ["lacquerCost", "Lacquer blank and stylus wear, a side", 45, "USD", "est", "Estimate; no lacquer or stylus quote on file"], + ["chemPart", "Nickel, silver, chemistry, an electroformed part", 25, "USD", "doc", "revenue-model.md: 60 to 150 USD marginal per three-generation set, so 20 to 50 a part"], +]); +grp("Energy", [ + ["kwhPrice", "Electricity, blended", 3.81, "THB/kWh", "doc", "scale.md: PEA TOU 3.2.2 at two shifts, before the demand charge (Ft September to December 2026)"], + ["demandPrice", "Demand charge", 132.93, "THB/kW a month", "doc", "PEA 3.2.2 (scale.md)"], + ["restKW", "Rest of the plant while running", 120, "kW", "est", "Estimate: HVAC, lighting, galvanics, finishing, offices; puts the start fleet in scale.md's 4.3 to 6.4M THB a year before PV"], + ["pvKWp", "Roof PV", 247, "kWp", "doc", "scale.md"], + ["pvYield", "PV yield, all self-consumed", 1400, "kWh/kWp", "doc", "scale.md: 1,400 to 1,500 kWh/kWp a year in northern Thailand"], + ["heatPrice", "Steam heat from biomass", 0.7, "THB/kWh", "doc", "biomass.md: 0.5 to 0.9 at 1.35 to 2 THB/kg delivered"], + ["steamKWh", "Heat per kg of steam", 0.68, "kWh/kg", "est", "Estimate: 10 bar saturated steam from ~80 °C feed water"], +]); +grp("Labour (THB a month a head, loaded ×1.3)", [ + ["wageOp", "Press and finishing operator", 19500, "THB", "doc", "revenue-model.md labour build-up"], + ["wageQC", "QC", 26000, "THB", "doc", "revenue-model.md"], + ["wageMaint", "Maintenance", 32500, "THB", "doc", "revenue-model.md"], + ["wageGalv", "Galvanics", 32500, "THB", "doc", "revenue-model.md"], + ["wageCut", "Cutting engineer", 45000, "THB", "est", "Estimate; the revenue model counts mastering as title-services labour"], + ["qcPerShift", "QC inspectors per shift", 1, "", "est", "Estimate; scale.md: 1 to 2 QC"], + ["pressPerTech", "Presses per maintenance technician (at least 2)", 8, "", "est", "Estimate; scale.md: 2 in maintenance for the start fleet"], +]); grp("Planned plant, for the checks", [ ["boiler", "Steam boiler", 400, "kg/h", "doc", "scale.md: sized for one Duplex system"], ["chiller", "Chiller", 250, "kW", "doc", "scale.md: sized for one Duplex system"], @@ -382,6 +466,22 @@ grp("Planned plant, for the checks", [ ]); const A0 = Object.fromEntries(AS.map(r => [r[1], r[3]])); const KIND = { vendor: "vendor", doc: "doc", est: "estimate", user: "your figure" }; +// revenue-model.md ranges: adverse pairs low price with high cost, favourable the reverse +const CASES = { + adverse: { S: { priceStd: 3.4, premSpec: 0.6, titleFee: 580 }, + A: { pvcPrice: 2.9, labelCost: 0.09, sleeveCost: 0.17, jacketCost: 0.92, shrinkCost: 0.03, maintCost: 0.23, overheads: 450000, capexBase: 8900000, heatPrice: 0.9, deprYears: 10 } }, + central: { S: { priceStd: 3.8, premSpec: 1.15, titleFee: 1155 }, + A: { pvcPrice: 2.3, labelCost: 0.06, sleeveCost: 0.13, jacketCost: 0.66, shrinkCost: 0.025, maintCost: 0.17, overheads: 330000, capexBase: 6800000, heatPrice: 0.7, deprYears: 12.5 } }, + favourable: { S: { priceStd: 4.25, premSpec: 1.7, titleFee: 1730 }, + A: { pvcPrice: 1.75, labelCost: 0.03, sleeveCost: 0.09, jacketCost: 0.4, shrinkCost: 0.02, maintCost: 0.12, overheads: 200000, capexBase: 4700000, heatPrice: 0.5, deprYears: 15 } }, +}; +function caseNow() { + for (const k in CASES) { + const c = CASES[k]; + if (Object.keys(c.S).every(x => S[x] === c.S[x]) && Object.keys(c.A).every(x => A[x] === c.A[x])) return k; + } + return ""; +} const LADDER = [ ["Start fleet (current plan)", 2, 1], ["Step 1", 2, 3], ["Step 2", 2, 5], ["Step 3", 2, 8], ["Full hall, Classic-led", 2, 11], ["Full hall, mixed", 4, 7], ["Full hall, specials-led", 6, 3], @@ -476,6 +576,50 @@ function compute(D, C, X, b) { trimT: X * (1 + A.reject / 100) * A.trim / 1e6, }; } +function headsOf(r) { + return { + ops: r.staff, qc: r.X > 0 ? Math.ceil(A.qcPerShift * S.shifts) : 0, galv: r.galv, + maint: r.P > 0 ? Math.max(2, Math.ceil(r.P / Math.max(1, A.pressPerTech))) : 0, cut: r.gst.lathe.n, + }; +} +// Money a year in USD. Lines: [group, label, USD, basis]. +function econ(r, b) { + const fx = A.thbUsd > 0 ? A.thbUsd : 33, t2u = v => v / fx; + const X = r.X, pressed = X * (1 + A.reject / 100), st = X - r.sp, L = []; + const add = (g, l, v, s) => L.push([g, l, v, s]); + const revStd = st * S.priceStd, revSpec = r.sp * (S.priceStd + S.premSpec), revTitle = r.titles * S.titleFee; + const rev = revStd + revSpec + revTitle; + add("mat", "PVC compound", r.compound * 1000 * A.pvcPrice, `${f0(r.compound)} t at ${f2(A.pvcPrice)} USD/kg incl. rejects; trim back as regrind`); + add("mat", "Labels", pressed * A.labelCost, "a printed pair per pressed record"); + add("pack", "Inner sleeves", X * A.sleeveCost, ""); + add("pack", "Jackets (printed cartons)", X * A.jacketCost, "local carton partners"); + add("pack", "Shrink wrap", X * A.shrinkCost, ""); + add("pack", "Shipping cartons and pallets", X * A.cartonCost, "estimate"); + add("maint", "Maintenance, spares, consumables", X * A.maintCost, ""); + add("metal", "Lacquer blanks and styli", r.titles * 2 * A.lacquerCost, `${f0(r.titles * 2)} lacquer sides`); + add("metal", "Nickel, silver, plating chemistry", r.partsYr * A.chemPart, `${f0(r.partsYr)} electroformed parts`); + const cap = capacityOf(r.D, r.C, b), use = cap > 0 ? Math.min(1, X / cap) : 0; + const runH = b.hoursYr * S.eff / 100 * use; // press running hours a year + const steamT = r.P * (A.steamLo + A.steamHi) / 2 * runH / 1000; + const heatMWh = steamT * A.steamKWh; + const elecMWh = X > 0 ? r.mwh + heatMWh / Math.max(0.5, A.cop) + (r.compKW + A.restKW) * b.hoursYr / 1000 : 0; + const pvMWh = Math.min(elecMWh, A.pvKWp * A.pvYield / 1000); + const demandKW = X > 0 ? r.kw + r.chillerKW + r.compKW + A.restKW : 0; + add("energy", "Electricity", t2u((elecMWh - pvMWh) * 1000 * A.kwhPrice + demandKW * A.demandPrice * 12), + `${f0(elecMWh - pvMWh)} MWh from the grid after ${f0(pvMWh)} MWh roof PV; ~${f0(demandKW)} kW demand`); + add("energy", "Steam heat (biomass)", t2u(heatMWh * 1000 * A.heatPrice), `${f0(steamT)} t of steam, ${f0(heatMWh)} MWh of heat`); + const h = headsOf(r), wage = (n, w) => t2u(n * w * 12), ppl = n => n + (n === 1 ? " person" : " people"); + add("lab", "Press operators and finishing", wage(h.ops, A.wageOp), ppl(h.ops)); + add("lab", "QC", wage(h.qc, A.wageQC), ppl(h.qc)); + add("lab", "Maintenance", wage(h.maint, A.wageMaint), ppl(h.maint)); + add("lab", "Galvanics", wage(h.galv, A.wageGalv), ppl(h.galv)); + add("lab", "Cutting engineers", wage(h.cut, A.wageCut), ppl(h.cut)); + const sum = gs => L.filter(l => gs.includes(l[0])).reduce((a, l) => a + l[2], 0); + const direct = L.reduce((a, l) => a + l[2], 0), contrib = rev - direct, ebitda = contrib - A.overheads; + const capex = A.capexBase + (r.D - 2) * A.capexD + (r.C - 1) * A.capexC, depr = capex / Math.max(1, A.deprYears); + return { rev, revStd, revSpec, revTitle, L, sum, direct, contrib, ebitda, capex, depr, op: ebitda - depr, + payback: ebitda > 0 ? capex / ebitda : Infinity, heads: h }; +} function scenario() { const b = basis(S); let D, C, X, cap; @@ -495,6 +639,14 @@ const nf = d => new Intl.NumberFormat("en-US", { maximumFractionDigits: d, minim const f0 = x => nf(0).format(Math.round(x)); const f1 = x => nf(1).format(x); const big = x => x >= 1e6 ? nf(2).format(x / 1e6) + " M" : f0(x); +const f2 = x => nf(2).format(x); +const fix2 = new Intl.NumberFormat("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); +const cv = usd => S.cur === "THB" ? usd * A.thbUsd : S.cur === "EUR" ? usd / Math.max(0.01, A.usdEur) : usd; +function money(usd) { + const v = cv(usd), a = Math.abs(v); + return (a >= 1e6 ? nf(2).format(v / 1e6) + " M" : a >= 1e4 ? f0(v / 1e3) + " k" : f0(v)) + " " + S.cur; +} +const unit = usd => fix2.format(cv(usd)) + " " + S.cur; const pct = x => Math.round(x * 100) + " %"; const esc = s => String(s).replace(/[&<>"]/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c])); const $ = id => document.getElementById(id); @@ -506,7 +658,9 @@ function render() { $("specV").textContent = S.spec + " %"; $("targetBox").hidden = S.mode !== "target"; $("fleetBox").hidden = S.mode !== "fleet"; - for (const g of ["mode", "grow"]) for (const bt of $(g).children) bt.classList.toggle("on", bt.dataset.v === S[g]); + for (const g of ["mode", "grow", "cur"]) for (const bt of $(g).children) bt.classList.toggle("on", bt.dataset.v === S[g]); + const cs = caseNow(); + for (const bt of $("case").children) bt.classList.toggle("on", bt.dataset.v === cs); $("fleetK").innerHTML = kpi(r.D, `Duplex systems
${2 * r.D} HRP 12 presses, ${r.D} EX 50, ${r.D} EX 35, ${r.D} trimmers`, "d") + @@ -552,6 +706,46 @@ function render() { `Duplex system (2 presses)${r.D}${big(r.D * b.capD)}${big(r.sp + r.stD)}${pct(r.dUse)}${pct((r.sp + r.stD) / W)}` + `Classic${r.C}${big(r.C * b.capC)}${big(r.stC)}${pct(r.cUse)}${pct(r.stC / W)}`; + // ---- money + const e = econ(r, b), Xr = Math.max(1, r.X), perRec = usd => r.X > 0 ? unit(usd / Xr) : "–"; + const blended = r.X > 0 ? (e.revStd + e.revSpec) / r.X : S.priceStd; + $("priceHint").textContent = `Blended record price at this mix: ${unit(blended)}. Prices are entered in USD; 1 USD = ${f2(A.thbUsd)} THB, 1 EUR = ${nf(3).format(A.usdEur)} USD.`; + $("ecoK").innerHTML = + kpi(money(e.rev), "revenue a year") + + kpi(money(e.ebitda), `EBITDA, ${e.rev > 0 ? pct(e.ebitda / e.rev) : "0 %"} of revenue`) + + kpi(money(e.op), "operating profit after depreciation") + + kpi(perRec(e.direct), "direct cost per record") + + kpi(isFinite(e.payback) ? f1(e.payback) + " years" : "never", `payback on ${money(e.capex)} capex`); + const SEG = [["PVC and labels", e.sum(["mat"]), "#b07d48"], ["Packaging", e.sum(["pack"]), "#c9a24a"], + ["Lacquers and plating", e.sum(["metal"]), "#8e98a6"], ["Energy", e.sum(["energy"]), "#e2462a"], + ["Labour", e.sum(["lab"]), "#2f7de1"], ["Maintenance", e.sum(["maint"]), "#7a6fd6"], + ["Overheads", A.overheads, "#6b7a90"], ["Depreciation", e.depr, "#4a5f85"]]; + const costAll = SEG.reduce((a, x) => a + x[1], 0), scale = Math.max(e.rev, costAll, 1); + $("ecoBar").innerHTML = SEG.map(([l, v, c]) => `

`).join("") + + (e.op > 0 ? `
` : ""); + $("ecoLeg").innerHTML = `Per record, revenue ${perRec(e.rev)}:` + + SEG.map(([l, v, c]) => `${l} ${perRec(v)}`).join("") + + (e.op >= 0 ? `Profit ${perRec(e.op)}` + : `Loss ${perRec(-e.op)} a record`); + const P = []; + const PG = t => P.push(`${t}`); + const PR = (l, v, s, tot) => P.push(`${l}${money(v)}${perRec(v)}${s || ""}`); + PG("Revenue"); + PR("Standard records", e.revStd, `${big(r.X - r.sp)} at ${unit(S.priceStd)}`); + PR("Splatter and specials", e.revSpec, `${big(r.sp)} at ${unit(S.priceStd + S.premSpec)}`); + PR("Title services", e.revTitle, `${f0(r.titles)} titles at ${money(S.titleFee)}`); + PR("Revenue", e.rev, "", true); + const GRP = [["Materials, packaging, upkeep", ["mat", "pack", "maint"]], ["Stamper making", ["metal"]], ["Energy", ["energy"]], ["Labour, loaded wages", ["lab"]]]; + for (const [t, gs] of GRP) { PG(t); e.L.filter(l => gs.includes(l[0])).forEach(l => PR(l[1], l[2], l[3])); } + PG("Result"); + PR("Direct cost", e.direct, "everything above", true); + PR("Contribution", e.contrib, e.rev > 0 ? pct(e.contrib / e.rev) + " of revenue" : "", true); + PR("Overheads", A.overheads, "admin, sales, insurance, facility, IT, fees, marketing"); + PR("EBITDA", e.ebitda, e.rev > 0 ? pct(e.ebitda / e.rev) + " of revenue" : "", true); + PR("Depreciation", e.depr, `${money(e.capex)} capex over ${f1(A.deprYears)} years`); + PR("Operating profit", e.op, isFinite(e.payback) ? `payback ${f1(e.payback)} years on EBITDA` : "no payback", true); + $("ecoT").innerHTML = `LineA yearA recordBasis${P.join("")}`; + const rows = []; const G = t => rows.push(`${t}`); const R = (l, v, s) => rows.push(`${l}${v}${s || ""}`); @@ -560,8 +754,11 @@ function render() { R("Classic attendants, per shift", r.att, `one per ${f0(A.cPerAtt)} Classics (assumption)`); R("Finishing: inspection, sleeving, packing, per shift", r.fin, `at ${f0(A.finRate)} records an hour per person (estimate)`); R("Production staff on all shifts", r.staff, `${r.perShift} per shift × ${S.shifts}; add cover for leave`); + R("QC inspectors, all shifts", e.heads.qc, `${f1(A.qcPerShift)} per shift (estimate; scale.md 1 to 2)`); + R("Maintenance technicians", e.heads.maint, `one per ${f0(A.pressPerTech)} presses, at least 2 (estimate)`); R("Galvanics operators (day work)", r.galv, `${f1(r.partsDay)} parts a day at about ${f0(A.galvRate)} per person (OMS)`); - R("Not scaled here", "7 to 11", "mastering, QC, maintenance, admin and logistics (scale.md)"); + R("Cutting engineers (day work)", e.heads.cut, "one per lathe (estimate)"); + R("Production headcount", e.heads.ops + e.heads.qc + e.heads.maint + e.heads.galv + e.heads.cut, "admin, sales and logistics sit in overheads; add cover for leave"); G("Machines"); R("HRP 12 presses (Duplex)", 2 * r.D, ""); R("Classic automatic presses", r.C, "extruder built in; trimming not stated by Newbilt"); @@ -608,11 +805,11 @@ function render() { $("reqT").innerHTML = `${rows.join("")}`; $("ladT").innerHTML = - `FleetDuplexClassicPressesRecords a yearSpecials a yearStaff per shiftSteam, kg/hCooling, kWConnected, kVAPress floor, m²` + + `FleetDuplexClassicPressesRecords a yearSpecials a yearStaff per shiftSteam, kg/hCooling, kWConnected, kVAPress floor, m²RevenueEBITDA` + LADDER.map(([n, d, c], i) => { - const x = capacityOf(d, c, b), q = compute(d, c, x, b); + const x = capacityOf(d, c, b), q = compute(d, c, x, b), qe = econ(q, b); const cur = r.D === d && r.C === c ? " cur" : ""; - return `${n}${d}${c}${q.P}${big(x)}${big(q.sp)}${q.perShift}${f0(q.steam[1])}${f0(q.coolPeak)}${f0(q.kva)}${f0(q.floor)}`; + return `${n}${d}${c}${q.P}${big(x)}${big(q.sp)}${q.perShift}${f0(q.steam[1])}${f0(q.coolPeak)}${f0(q.kva)}${f0(q.floor)}${money(qe.rev)}${money(qe.ebitda)}`; }).join("") + ""; drawPark(r, b); @@ -755,7 +952,7 @@ function drawPark(r, b) { } // ---------------------------------------------------------------- inputs and state -const NUM = ["target", "dMin", "cMin", "dFleet", "cFleet", "shifts", "hrs", "days", "eff", "rateD", "rateC", "specF"]; +const NUM = ["target", "dMin", "cMin", "dFleet", "cFleet", "shifts", "hrs", "days", "eff", "rateD", "rateC", "specF", "priceStd", "premSpec", "titleFee"]; function syncInputs() { for (const k of NUM) $(k).value = S[k]; $("targetR").value = S.target; @@ -787,6 +984,7 @@ function readHash() { } if (!["target", "fleet"].includes(S.mode)) S.mode = "target"; if (!["classic", "duplex"].includes(S.grow)) S.grow = "classic"; + if (!["USD", "THB", "EUR"].includes(S.cur)) S.cur = "USD"; } function num(el, fallback) { const v = parseFloat(el.value); return isFinite(v) ? v : fallback; } @@ -796,7 +994,11 @@ syncInputs(); for (const k of NUM) $(k).addEventListener("input", e => { S[k] = num(e.target, S0[k]); if (k === "target") $("targetR").value = S.target; render(); }); $("targetR").addEventListener("input", e => { S.target = +e.target.value; $("target").value = S.target; render(); }); $("spec").addEventListener("input", e => { S.spec = +e.target.value; render(); }); -for (const g of ["mode", "grow"]) $(g).addEventListener("click", e => { +$("case").addEventListener("click", e => { + const v = e.target.dataset && e.target.dataset.v; if (!v || !CASES[v]) return; + Object.assign(S, CASES[v].S); Object.assign(A, CASES[v].A); syncInputs(); render(); +}); +for (const g of ["mode", "grow", "cur"]) $(g).addEventListener("click", e => { const v = e.target.dataset && e.target.dataset.v; if (!v) return; if (g === "mode" && v === "fleet" && S.mode === "target") { const { r } = scenario(); S.dFleet = r.D; S.cFleet = r.C; syncInputs(); } S[g] = v; render();