/* project-spinner-data.jsx — "The Spinner" build descriptor.
   A small DC motor you drive, protect, and speed-control with PWM.
   Sets window.PROJECT_BUILD for projectkit.jsx. Names pk-prefixed (SP_ for build data). */

const pkOhm = (r) => r >= 1000 ? `${(r / 1000).toFixed(r % 1000 ? 1 : 0)} kΩ` : `${r} Ω`;
const pkFar = (c) => c >= 1e-6 ? `${(c * 1e6).toFixed(c * 1e6 % 1 ? 2 : 0)} µF` : `${(c * 1e9).toFixed(0)} nF`;
const pkMA  = (a) => `${a.toFixed(a < 10 ? 1 : 0)} mA`;
const pkAmp = (a) => a >= 1 ? `${a} A` : `${(a * 1000).toFixed(0)} mA`;
const pkHz  = (f) => f >= 1000 ? `${(f / 1000).toFixed(2)} kHz` : `${f.toFixed(0)} Hz`;

const SP_VB = 9, SP_RUN = 0.25, SP_STALL = 0.45, SP_BETA = 100;

const SP_STAGES = [
  {
    id: "muscle", n: 1, schem: "motor-pick", part: "motor",
    title: "Pick the Muscle", teaches: "Transistor ratings",
    tagline: "A transistor tough enough to survive a stalled motor.",
    intro: {
      adult: "A logic signal can't drive a motor — you need a transistor doing the muscle work. But a motor that jams pulls far more than its running current. Size the transistor's collector rating above the 0.45 A stall, with margin.",
      kids: "A motor is too strong for a little signal to push, so a transistor does the heavy lifting. When a motor gets stuck it pulls way more power — so pick a transistor that can take the worst case.",
    },
    spec: "Transistor collector rating ≥ 1.5× the motor's 0.45 A stall current.",
    givens: ["Running current ≈ 0.25 A", "Stall current ≈ 0.45 A", "Need ≥ 1.5× stall", "Margin keeps it cool"],
    explain: "A motor's current peaks when it stalls (rotor not turning). The switch that drives it must be rated above that peak, not just the gentle running current — with headroom so it never runs at its limit.",
    picks: [{
      id: "q", label: "Drive transistor", unit: "A",
      hint: "Compare each part's current rating to the 0.45 A stall.",
      options: [{ label: "2N3904 · 200 mA", value: 0.2 }, { label: "2N2222 · 800 mA", value: 0.8 }, { label: "TIP120 · 5 A", value: 5 }],
    }],
    check: (v) => {
      const need = 1.5 * SP_STALL;
      const ok = v.q >= need;
      return {
        ok,
        headline: `${pkAmp(v.q)} rated vs ${pkAmp(SP_STALL)} stall`,
        formula: `need ≥ 1.5 × ${SP_STALL} A stall = ${need.toFixed(2)} A · you picked ${pkAmp(v.q)}`,
        lines: [
          { k: "Stall current", v: pkAmp(SP_STALL) },
          { k: "Need (1.5× margin)", v: `≥ ${need.toFixed(2)} A` },
          { k: "Your transistor", v: pkAmp(v.q), ok },
        ],
        note: {
          adult: ok ? (v.q === 0.8 ? "The 2N2222 (800 mA) clears the 0.45 A stall with margin and stays cheap — the right-sized pick." : "The TIP120 (5 A) works easily, though it's overkill for a motor this small.") : "200 mA can't survive a 0.45 A stall — the first time the motor binds, it cooks. Step up to a tougher transistor.",
          kids: ok ? "Strong enough — even a stuck motor won't hurt it!" : "Too weak — a stuck motor would burn it out. Pick a tougher one.",
        },
      };
    },
  },
  {
    id: "drive", n: 2, schem: "motor-drive", part: "transistor",
    title: "Open the Valve", teaches: "Transistor saturation",
    tagline: "Enough base current to switch the motor fully on.",
    intro: {
      adult: "To act like a closed switch, the transistor must be saturated — driven with base current well past what bare amplification needs. With β ≈ 100 and a 0.25 A motor, it just-conducts at 2.5 mA of base current; design for at least 2× that. Pick the base resistor.",
      kids: "A trickle into the transistor's base opens a big valve for the motor. Give it a strong-enough trickle and the valve opens all the way (not half-stuck and hot). Pick the resistor that sets the trickle.",
    },
    spec: "Saturate the driver: base current I_b ≥ 2× I_c ÷ β (≈ 5 mA) for the 0.25 A motor.",
    givens: ["Motor draws ≈ 0.25 A", "β ≈ 100 → just-on at 2.5 mA", "Target I_b ≥ 5 mA", "I_b = (9 − 0.7) ÷ R_b"],
    explain: "Collector current is β times base current — but only until the transistor saturates. To use it as a switch you over-drive the base (≥ 2× the I_c/β minimum) so it's fully on. A smaller base resistor means more base current.",
    picks: [{
      id: "Rb", label: "Base resistor", unit: "Ω",
      hint: "I_b = (9 − 0.7) ÷ R_b. Aim past 5 mA, but not wildly.",
      options: [470, 1000, 4700, 10000].map(r => ({ label: pkOhm(r), value: r })),
    }],
    check: (v) => {
      const Ib = (SP_VB - 0.7) / v.Rb * 1000;
      const ok = Ib >= 5 && Ib <= 30;
      return {
        ok,
        headline: `Base current I_b ≈ ${pkMA(Ib)}`,
        formula: `I_b = (9 V − 0.7 V) ÷ ${pkOhm(v.Rb)} = ${pkMA(Ib)}   (need ≥ 2× I_c/β = 5 mA)`,
        lines: [
          { k: "Just-saturate (I_c/β)", v: "2.5 mA" },
          { k: "Target (2× margin)", v: "≥ 5 mA" },
          { k: "Your I_b", v: pkMA(Ib), ok },
        ],
        note: {
          adult: ok ? "Solidly saturated — 1 kΩ gives ~8 mA of base drive and the transistor acts like a closed switch." : Ib < 5 ? "Not enough base current — the transistor stays half-open, drops voltage, and gets hot. Drop R_b." : "Far more base current than needed — wasteful and hard on the driving pin. Raise R_b.",
          kids: ok ? "The valve is wide open — full power to the motor!" : Ib < 5 ? "Not enough trickle — the valve barely opens. Smaller resistor." : "Way too much trickle — wasteful. Bigger resistor.",
        },
      };
    },
  },
  {
    id: "kick", n: 3, schem: "motor-flyback", part: "diode",
    title: "Catch the Kick", teaches: "Flyback protection",
    tagline: "Survive the spike when the motor switches off.",
    intro: {
      adult: "A motor is a coil — an inductor. Cut its current and the collapsing field spikes hundreds of volts, straight into your transistor. A flyback diode across the motor gives that current a loop to die out in, clamping the spike to about Vcc + 0.7 V. Place it the right way, and rate it for the motor's current.",
      kids: "A motor is a coil that hates being switched off — it kicks back with a big voltage spike that can break the transistor. A flyback diode catches the kick. Put it across the motor and make sure it's big enough.",
    },
    spec: "Clamp the switch-off spike with a flyback diode rated ≥ the 0.45 A motor current.",
    explain: "An inductor resists current change: cut it fast and V = L·(di/dt) spikes huge. A diode across the coil routes that current in a loop until it fades, clamping the spike to a safe level.",
    picks: [
      { id: "fb", label: "Handle the motor's kick…", hint: "Where does the current go when the transistor opens?",
        options: [{ label: "No diode", value: "none" }, { label: "Flyback diode across the motor", value: "across" }] },
      { id: "d", label: "Flyback diode rating", hint: "Must carry the full motor current at switch-off.",
        options: [{ label: "1N4148 · 200 mA", value: 0.2 }, { label: "1N4001 · 1 A", value: 1.0 }] },
    ],
    check: (v) => {
      const placed = v.fb === "across", rated = v.d >= SP_STALL, ok = placed && rated;
      const clamp = placed ? "clamped to Vcc + 0.7 ≈ 9.7 V (safe)" : "hundreds of volts — transistor destroyed";
      return {
        ok,
        headline: placed ? (rated ? "Spike clamped · diode sized right" : "Spike clamped — but diode too small") : "No path for the current — huge spike",
        formula: `switch-off spike → ${clamp} · diode must carry the ${SP_STALL} A motor current`,
        lines: [
          { k: "Flyback path", v: placed ? "across motor" : "none", ok: placed },
          { k: "Diode ≥ motor current (0.45 A)", v: pkAmp(v.d), ok: rated },
        ],
        note: {
          adult: ok ? "Textbook flyback: the 1N4001 routes the motor's current and clamps the spike to a safe ~9.7 V." : !placed ? "With no flyback path the collapsing field spikes the transistor to destruction. Put a diode across the motor (cathode to +)." : "Right idea — but a 200 mA diode can't carry the 0.45 A motor current. Use the 1 A.",
          kids: ok ? "Caught it! The kick is tamed and the motor driver is safe." : !placed ? "Ouch — the kick has nowhere to go and breaks the transistor. Add a diode across the motor." : "Right idea, too small — use the bigger 1 A diode.",
        },
      };
    },
  },
  {
    id: "pwm", n: 4, schem: "pwm-freq", part: "ic555",
    title: "Pulse the Power", teaches: "PWM frequency",
    tagline: "Switch fast enough that the motor feels a smooth average.",
    intro: {
      adult: "Instead of a weak steady voltage, you flick the motor fully on and off many times a second — PWM. If the switching is fast enough, the motor's inertia averages it into smooth torque; too slow and it judders. With R1 = 1 kΩ, R2 = 10 kΩ, pick the timing capacitor so the 555 runs between 150 Hz and 2 kHz.",
      kids: "Rather than a weak push, you blink full power on and off really fast. Too slow and the motor jerks; fast enough and it spins smoothly. Pick the timing bucket so the chip blinks hundreds of times a second.",
    },
    spec: "PWM frequency between 150 Hz and 2 kHz (smooth, but not wastefully fast).",
    givens: ["R1 = 1 kΩ", "R2 = 10 kΩ", "f = 1.44 ÷ ((R1 + 2·R2)·C)", "Motor smooths above ~150 Hz"],
    explain: "A 555 astable makes the on/off square wave. f = 1.44 ÷ ((R_1 + 2·R_2)·C): a bigger timing capacitor slows the switching. Above ~150 Hz the motor can't follow each pulse, so it feels a steady average.",
    picks: [{
      id: "C", label: "Timing capacitor", unit: "F",
      hint: "Bigger C → slower switching. Land between 150 Hz and 2 kHz.",
      options: [1e-6, 1e-7, 4.7e-8, 1e-8].map(c => ({ label: pkFar(c), value: c })),
    }],
    check: (v) => {
      const f = 1.44 / ((1000 + 2 * 10000) * v.C);
      const ok = f >= 150 && f <= 2000;
      return {
        ok,
        headline: `PWM ≈ ${pkHz(f)}`,
        formula: `f = 1.44 ÷ ((1 kΩ + 2×10 kΩ) × ${pkFar(v.C)}) = ${pkHz(f)}`,
        lines: [{ k: "Target", v: "150 Hz – 2 kHz" }, { k: "Your frequency", v: pkHz(f), ok }],
        note: {
          adult: ok ? "Smooth and efficient — the motor averages the pulses into steady torque with no audible whine." : f < 150 ? "Too slow — you'd feel the motor pulse and judder. Use a smaller capacitor." : "Needlessly fast — switching losses climb and it can whine. Use a bigger capacitor.",
          kids: ok ? "Fast enough — the motor spins nice and smooth!" : f < 150 ? "Too slow — the motor would jerk. Smaller bucket." : "Too fast — wasteful. Bigger bucket.",
        },
      };
    },
  },
  {
    id: "speed", n: 5, schem: "pwm-duty", part: "cap", final: true,
    title: "Dial the Speed", teaches: "Duty cycle → speed",
    tagline: "The on/off ratio sets the average power — and the speed.",
    intro: {
      adult: "Speed comes from duty cycle: the fraction of each cycle the motor is on. Average drive = duty × 9 V, and speed tracks that average. For a 555 astable, duty = (R1 + R2) ÷ (R1 + 2·R2) — equal resistors give about two-thirds. Tune R1 and R2 to cruise at roughly ⅔ speed.",
      kids: "How fast it spins depends on how much of the time it's on. On two-thirds of the time → about two-thirds speed. Pick the two resistors so it cruises at a nice middle pace.",
    },
    spec: "Cruise duty between 60% and 72% (≈ ⅔ speed). This is the finale — hit it and the Spinner is complete.",
    givens: ["duty = (R1 + R2) ÷ (R1 + 2·R2)", "avg drive = duty × 9 V", "speed ∝ average drive", "equal R's → ~67%"],
    explain: "The 555 charges C through R_1 + R_2 (output high) and discharges through R_2 (output low), so duty = (R_1 + R_2) ÷ (R_1 + 2·R_2). It can't go below 50%; equal resistors land at ⅔.",
    picks: [
      { id: "R1", label: "R1", unit: "Ω", options: [4700, 10000].map(r => ({ label: pkOhm(r), value: r })) },
      { id: "R2", label: "R2", unit: "Ω", options: [4700, 10000].map(r => ({ label: pkOhm(r), value: r })) },
    ],
    check: (v) => {
      const duty = (v.R1 + v.R2) / (v.R1 + 2 * v.R2);
      const Vavg = duty * SP_VB;
      const ok = duty >= 0.60 && duty <= 0.72;
      return {
        ok,
        headline: `duty ≈ ${(duty * 100).toFixed(0)}% · ${Vavg.toFixed(1)} V avg`,
        formula: `duty = (${pkOhm(v.R1)} + ${pkOhm(v.R2)}) ÷ (${pkOhm(v.R1)} + 2×${pkOhm(v.R2)}) = ${(duty * 100).toFixed(0)}%`,
        lines: [
          { k: "Target duty", v: "60 – 72%" },
          { k: "Your duty", v: `${(duty * 100).toFixed(0)}%`, ok },
          { k: "Average drive", v: `${Vavg.toFixed(1)} V` },
        ],
        note: {
          adult: ok ? "A steady cruise — equal R1 = R2 sets a 67% duty, about ⅔ speed, with room to dial up or down." : duty > 0.72 ? "Nearly flat-out — almost no headroom to speed up. Even up R1 and R2 toward a 2:3 duty." : "Below ~60% — sluggish, and a 555 can't go under 50% anyway. Bring R1 and R2 closer together.",
          kids: ok ? "Spinning at a nice cruise — the Spinner is ALIVE! 🎉" : duty > 0.72 ? "Almost full speed — leave room to go faster. Make the resistors more equal." : "A bit slow — make the resistors more equal.",
        },
      };
    },
  },
];

window.PROJECT_BUILD = {
  id: "spinner", name: "The Spinner", em: "Spinner", kicker: "Project 02 · motion",
  hero: {
    adult: "A motor is electricity made physical. Across five stages you'll pick a transistor tough enough to drive it, open the valve with the right base resistor, catch the inductive kick, then hand the throttle to a 555 — PWM that dials the speed without wasting power.",
    kids: "Make a real motor spin — and control how fast! You'll pick the muscle to drive it, protect it from its own kick, and use the blinking chip to speed it up and slow it down.",
  },
  stages: SP_STAGES,
  learned: {
    muscle: ["A transistor's collector rating must beat the motor's stall current, with margin.", "A stalled motor pulls far more than its running current."],
    drive:  ["Saturate a transistor with base current I_b ≥ 2× I_c ÷ β.", "Smaller base resistor → more base drive."],
    kick:   ["A motor is inductive; cutting its current spikes the voltage.", "A flyback diode across it clamps the spike — rate it for the motor current."],
    pwm:    ["PWM switches the motor on/off fast; above ~150 Hz it averages into smooth torque.", "f = 1.44 ÷ ((R_1 + 2·R_2)·C)."],
    speed:  ["Average drive = duty × supply, and motor speed tracks the average.", "555 duty = (R_1 + R_2) ÷ (R_1 + 2·R_2); equal R's give ~⅔."],
  },
  finale: {
    adult: "Five stages — a right-sized transistor, a saturated base, a flyback diode, a smooth PWM frequency, and a duty cycle you chose — and you've built a motor whose speed you command. That's motion control.",
    kids: "You drove a real motor and set its speed yourself! From a tiny signal to a spinning shaft — that's how every fan, drone, and robot moves.",
  },
  bench: {
    adult: "You've specified every value. Wire it on the breadboard in this order — transistor and motor first, then the flyback diode, then the 555 throttle feeding the base:",
    kids: "You have the plan. Build it for real — motor and transistor first, then the kick-catcher diode, then the speed chip.",
  },
  specFoot: "Supply 9 V · motor ≈ 0.25 A running, 0.45 A stall · β ≈ 100 · every value meets its target spec.",
};
