Program Showcase

About Me

Hi! I’m Nolan T a Web Developer, Programmer, and Semi Artist who has been coding for 7 years, drawing for 5 and been on Roblox since 2020

Showcase

I have multiple programs that I have to show but here are just a few:

3D Modeling Software

Blockquote

<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Ultra3D — Web 3D Modeler</title>
<style>
    body {
        margin: 0;
        overflow: hidden;
        font-family: Poppins, sans-serif;
        background: #222;
    }

    #uiPanel {
        position: absolute;
        top: 10px;
        left: 10px;
        width: 260px;
        padding: 14px;
        background: rgba(20, 20, 20, 0.9);
        border-radius: 8px;
        color: white;
        font-size: 14px;
        box-shadow: 0 0 15px #000;
        backdrop-filter: blur(6px);
    }

    #uiPanel h2 {
        margin-top: 0;
        text-align: center;
    }

    button, select, input {
        width: 100%;
        margin-top: 6px;
        padding: 8px;
        border: none;
        border-radius: 4px;
        background: #333;
        color: white;
        font-size: 14px;
    }

    button:hover {
        background: #444;
        cursor: pointer;
    }

    #objectList {
        width: 100%;
        height: 80px;
        background: #111;
        margin-top: 8px;
        overflow-y: auto;
        padding: 5px;
        border-radius: 5px;
        font-size: 13px;
    }

    #objectList div {
        padding: 4px;
        background: #333;
        margin-bottom: 4px;
        border-radius: 3px;
        cursor: pointer;
    }

    #objectList div:hover {
        background: #444;
    }
</style>
</head>
<body>

<div id="uiPanel">
    <h2>Ultra3D</h2>

    <button onclick="addCube()">Add Cube</button>
    <button onclick="addSphere()">Add Sphere</button>
    <button onclick="addCylinder()">Add Cylinder</button>

    <select id="toolSelect" onchange="setTool(this.value)">
        <option value="translate">Move</option>
        <option value="rotate">Rotate</option>
        <option value="scale">Scale</option>
    </select>

    <label>Material Color:</label>
    <input type="color" id="colorPicker" value="#ffffff" onchange="updateColor()">

    <button onclick="exportOBJ()">Export as OBJ</button>

    <h3>Objects</h3>
    <div id="objectList"></div>
</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128/examples/js/controls/OrbitControls.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128/examples/js/controls/TransformControls.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128/examples/js/exporters/OBJExporter.js"></script>

<script>
    let scene, camera, renderer, orbit, transformControl;
    let selectedObject = null;

    init();
    animate();

    function init() {
        // Scene
        scene = new THREE.Scene();
        scene.background = new THREE.Color(0x1a1a1a);

        // Camera
        camera = new THREE.PerspectiveCamera(
            60,
            window.innerWidth / window.innerHeight,
            0.1,
            1000
        );
        camera.position.set(4, 4, 6);

        // Renderer
        renderer = new THREE.WebGLRenderer({ antialias: true });
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.shadowMap.enabled = true;
        document.body.appendChild(renderer.domElement);

        // Grid
        const grid = new THREE.GridHelper(20, 20);
        scene.add(grid);

        // Lights
        const light = new THREE.DirectionalLight(0xffffff, 1);
        light.position.set(5, 10, 7);
        light.castShadow = true;
        scene.add(light);

        scene.add(new THREE.AmbientLight(0x777777));

        // Orbit Controls
        orbit = new THREE.OrbitControls(camera, renderer.domElement);

        // Transform Controls
        transformControl = new THREE.TransformControls(camera, renderer.domElement);
        transformControl.addEventListener("change", () => renderer.render(scene, camera));
        transformControl.addEventListener("dragging-changed", event => {
            orbit.enabled = !event.value;
        });
        scene.add(transformControl);

        window.addEventListener("resize", onWindowResize);
    }

    function onWindowResize() {
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(window.innerWidth, window.innerHeight);
    }

    // ------------ OBJECT CREATION ---------------
    function addMesh(geometry) {
        const material = new THREE.MeshStandardMaterial({ color: 0xffffff });
        const mesh = new THREE.Mesh(geometry, material);
        mesh.castShadow = true;
        mesh.receiveShadow = true;
        mesh.name = "Object " + (scene.children.length - 3);

        scene.add(mesh);
        addObjectToList(mesh);
        selectObject(mesh);
    }

    function addCube() {
        addMesh(new THREE.BoxGeometry(1, 1, 1));
    }

    function addSphere() {
        addMesh(new THREE.SphereGeometry(0.7, 32, 32));
    }

    function addCylinder() {
        addMesh(new THREE.CylinderGeometry(0.5, 0.5, 1.4, 32));
    }

    // ------------ OBJECT LIST UI ---------------
    function addObjectToList(obj) {
        const list = document.getElementById("objectList");
        const div = document.createElement("div");
        div.innerText = obj.name;
        div.onclick = () => selectObject(obj);
        list.appendChild(div);
    }

    // ------------ SELECTION + TRANSFORM ---------------
    function selectObject(obj) {
        selectedObject = obj;
        transformControl.attach(obj);
    }

    function setTool(tool) {
        transformControl.setMode(tool);
    }

    function updateColor() {
        if (!selectedObject) return;
        selectedObject.material.color.set(
            document.getElementById("colorPicker").value
        );
    }

    // ------------ EXPORT ----------------
    function exportOBJ() {
        const exporter = new THREE.OBJExporter();
        const result = exporter.parse(scene);

        const blob = new Blob([result], { type: "text/plain" });
        const url = URL.createObjectURL(blob);

        const a = document.createElement("a");
        a.href = url;
        a.download = "Ultra3D_Model.obj";
        a.click();
    }

    // ------------ RENDER LOOP ---------------
    function animate() {
        requestAnimationFrame(animate);
        renderer.render(scene, camera);
    }
</script>

</body>
</html>

Recipe List

<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>Artisan Pastries — 900 Found Recipes</title>

  <!-- Google Fonts -->
  <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&family=Dancing+Script&family=Inter:wght@300;400;600&display=swap" rel="stylesheet">

  <style>
    :root{
      --bg:#fff8f2;
      --card:#fff;
      --muted:#7a6b5a;
      --accent:#d87c6b;
      --accent-2:#f1cbb7;
      --shadow: 0 6px 20px rgba(24,20,16,0.08);
      --glass: rgba(255,255,255,0.65);
    }
    *{box-sizing:border-box}
    html,body{height:100%}
    body{
      margin:0;
      font-family: Inter, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
      background: linear-gradient(180deg, #fff8f2 0%, #fffdfc 100%);
      color:#2b2b2b;
      -webkit-font-smoothing:antialiased;
      -moz-osx-font-smoothing:grayscale;
      padding:28px;
    }

    /* Header / Brand */
    header{
      display:flex;
      align-items:center;
      gap:18px;
      margin-bottom:18px;
    }
    .logo{
      width:72px;
      height:72px;
      border-radius:14px;
      background:
        radial-gradient(circle at 30% 30%, rgba(255,255,255,0.4), transparent 10%),
        linear-gradient(135deg,var(--accent),#c96455);
      display:flex;
      align-items:center;
      justify-content:center;
      box-shadow: var(--shadow);
      color:white;
      font-family:"Dancing Script", "Playfair Display", serif;
      font-size:28px;
      border:4px solid rgba(255,255,255,0.25);
    }
    .brand{
      line-height:1;
    }
    .brand h1{
      margin:0;
      font-family: "Playfair Display", serif;
      font-weight:700;
      font-size:22px;
      color:#3b2f2a;
    }
    .brand p{
      margin:2px 0 0 0;
      color:var(--muted);
      font-size:13px;
    }

    /* Controls */
    .controls{
      margin-top:12px;
      display:flex;
      gap:12px;
      flex-wrap:wrap;
      align-items:center;
    }
    .search {
      display:flex;
      flex:1 1 360px;
      background:var(--glass);
      padding:8px;
      border-radius:10px;
      border:1px solid rgba(0,0,0,0.04);
      box-shadow: 0 2px 6px rgba(16,12,8,0.04) inset;
    }
    .search input{
      border:0;
      outline:0;
      flex:1;
      padding:8px;
      font-size:15px;
      background:transparent;
      color:#231b18;
    }
    .select, .btn{
      background:var(--card);
      padding:8px 12px;
      border-radius:10px;
      border:1px solid rgba(0,0,0,0.06);
      box-shadow: var(--shadow);
      cursor:pointer;
      font-size:14px;
    }
    .small-meta{
      margin-left:auto;
      color:var(--muted);
      font-size:13px;
      display:flex;
      gap:10px;
      align-items:center;
    }

    /* Gallery */
    .summary {
      margin:18px 0;
      display:flex;
      align-items:center;
      gap:12px;
      flex-wrap:wrap;
    }
    .summary .tag{
      background:linear-gradient(180deg,rgba(255,255,255,0.6), rgba(255,255,255,0.4));
      padding:8px 12px;
      border-radius:999px;
      border:1px solid rgba(0,0,0,0.03);
      box-shadow: 0 4px 10px rgba(0,0,0,0.03);
      font-size:13px;
      color: #5c4e44;
    }

    .grid{
      display:grid;
      grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
      gap:18px;
      margin-top:12px;
    }

    .card{
      background:var(--card);
      border-radius:14px;
      overflow:hidden;
      box-shadow:var(--shadow);
      display:flex;
      flex-direction:column;
      transition:transform .18s ease, box-shadow .18s ease;
      border:1px solid rgba(0,0,0,0.03);
    }
    .card:hover{ transform:translateY(-6px); box-shadow: 0 20px 40px rgba(22,16,12,0.12); }

    .thumb{
      height:140px;
      display:flex;
      align-items:flex-end;
      padding:12px;
      position:relative;
      color:white;
    }
    .thumb .badge{
      background: rgba(0,0,0,0.22);
      font-size:12px;
      padding:6px 8px;
      border-radius:8px;
      backdrop-filter: blur(6px);
      margin-right:6px;
    }
    .thumb svg{ position:absolute; inset:0; width:100%; height:100%; object-fit:cover; }

    .content{
      padding:12px;
      display:flex;
      flex-direction:column;
      gap:8px;
      flex:1;
    }
    .title{
      font-family:"Playfair Display", serif;
      font-size:15px;
      margin:0;
      color:#2b2b2b;
    }
    .meta{
      color:var(--muted);
      font-size:13px;
      display:flex;
      justify-content:space-between;
      gap:8px;
    }
    .actions{
      margin-top:auto;
      display:flex;
      gap:8px;
      align-items:center;
    }
    .link{
      font-size:13px;
      color:var(--accent);
      text-decoration:none;
      cursor:pointer;
      background:transparent;
      border:0;
      padding:8px 10px;
      border-radius:8px;
    }

    /* Modal */
    .overlay{
      position:fixed;
      inset:0;
      display:none;
      align-items:center;
      justify-content:center;
      background:linear-gradient(180deg, rgba(11,8,6,0.35), rgba(11,8,6,0.65));
      z-index:90;
      padding:18px;
    }
    .overlay.open{ display:flex; }
    .modal{
      width:100%;
      max-width:920px;
      max-height:92vh;
      overflow:auto;
      background: linear-gradient(180deg,#fff,#fffbf8);
      border-radius:12px;
      box-shadow: 0 30px 80px rgba(12,8,6,0.5);
      padding:22px;
      position:relative;
    }
    .modal .modal-head{
      display:flex;
      gap:12px;
      align-items:center;
    }
    .modal h2{ margin:0; font-family:"Playfair Display", serif; font-size:24px; }
    .close{
      margin-left:auto;
      background:transparent;
      border:0;
      font-size:22px;
      cursor:pointer;
      color:var(--muted);
    }
    .modal .recipe{
      margin-top:12px;
      display:grid;
      grid-template-columns: 1fr 350px;
      gap:18px;
    }
    .modal .recipe .left{}
    .modal .ingredients{
      background:linear-gradient(180deg,#fffaf6,#fff8f2);
      padding:12px;
      border-radius:10px;
      border:1px dashed rgba(0,0,0,0.04);
    }
    .chip{
      display:inline-block;
      padding:6px 8px;
      border-radius:999px;
      background:var(--accent-2);
      color: #5a4037;
      font-size:13px;
      margin-right:6px;
    }

    /* Footer */
    footer{
      margin-top:22px;
      color:var(--muted);
      font-size:13px;
      display:flex;
      justify-content:space-between;
      align-items:center;
      gap:12px;
      flex-wrap:wrap;
    }

    /* Responsive */
    @media (max-width:900px){
      .modal .recipe{ grid-template-columns: 1fr; }
    }

    /* small decorative paper texture */
    body::after{
      content:"";
      position:fixed;
      inset:0;
      background-image: radial-gradient(circle at 20% 10%, rgba(255,255,255,0.15) 0px, transparent 60%), repeating-linear-gradient(45deg, rgba(0,0,0,0.01) 0 2px, transparent 2px 6px);
      opacity:0.65;
      pointer-events:none;
      mix-blend-mode:overlay;
    }

  </style>
</head>
<body>

  <header>
    <div class="logo">AP</div>
    <div class="brand">
      <h1>Artisan Pastries</h1>
      <p>900 found recipes • hand-curated, bakery-style inspiration</p>
      <div class="controls" style="margin-top:10px;">
        <div class="search" role="search">
          <svg xmlns="http://www.w3.org/2000/svg" style="width:18px;height:18px;margin-right:8px;opacity:0.6" viewBox="0 0 24 24" fill="none" stroke="#5a453b"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-4.35-4.35M17 11a6 6 0 11-12 0 6 6 0 0112 0z"/></svg>
          <input id="q" placeholder="Search pastries, e.g. 'almond croissant', 'tart', 'gluten-free'..." />
          <button class="select" id="clear">Clear</button>
        </div>

        <select id="typeFilter" class="select" title="Filter by pastry type">
          <option value="all">All types</option>
        </select>

        <select id="sort" class="select" title="Sort">
          <option value="popular">Sort: Popular</option>
          <option value="time-asc">Sort: Time ↑</option>
          <option value="time-desc">Sort: Time ↓</option>
          <option value="difficulty">Sort: Difficulty</option>
        </select>

        <div class="small-meta">
          <div id="found" class="tag">900 recipes</div>
        </div>
      </div>
    </div>
  </header>

  <div class="summary">
    <div class="tag">Handmade-style photos</div>
    <div class="tag">Vegetarian-friendly</div>
    <div class="tag">Joined textures & vintage vibes</div>
  </div>

  <main>
    <section class="grid" id="grid" aria-live="polite"></section>
  </main>

  <div class="overlay" id="overlay" aria-hidden="true">
    <div class="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
      <div class="modal-head">
        <h2 id="modalTitle">Recipe Title</h2>
        <button class="close" id="closeBtn" aria-label="Close">✕</button>
      </div>

      <div class="recipe">
        <div class="left">
          <p id="modalDesc" style="color:#5a473f"></p>

          <h3 style="margin-top:14px;font-size:16px;font-family:Playfair Display, serif">Method</h3>
          <div id="modalMethod" style="line-height:1.6;color:#3d2f2b"></div>

          <h3 style="margin-top:14px;font-size:16px;font-family:Playfair Display, serif">Notes</h3>
          <div id="modalNotes" style="color:var(--muted);font-size:14px"></div>
        </div>

        <aside>
          <div style="display:flex;align-items:center;gap:12px;">
            <div style="width:86px;height:86px;border-radius:12px;overflow:hidden;box-shadow:var(--shadow)" id="modalThumb"></div>
            <div>
              <div class="chip" id="modalType">Type</div>
              <div style="color:var(--muted);font-size:13px">Time: <strong id="modalTime">45m</strong></div>
              <div style="color:var(--muted);font-size:13px">Difficulty: <strong id="modalDiff">Easy</strong></div>
            </div>
          </div>

          <div style="height:12px"></div>
          <div class="ingredients">
            <h4 style="margin:0 0 10px 0;font-size:15px">Ingredients</h4>
            <ul id="modalIngredients" style="margin:0;padding-left:18px;color:#3d2f2b"></ul>
          </div>
        </aside>
      </div>
    </div>
  </div>

  <footer>
    <div>Designed with care • Vintage-styled UI</div>
    <div>© Artisan Pastries • <span id="timeStamp"></span></div>
  </footer>

  <script>
    // --- Data generation ---
    const pastryTypes = [
      "Croissant","Tart","Danish","Pie","Turnover","Brioche","Scone","Éclair","Macaron",
      "Galette","Baklava","Strudel","Cookie","Bun","Puff Pastry","Choux","Mille-Feuille","Tartelette"
    ];

    const flavors = [
      "almond","chocolate","lemon","blueberry","raspberry","vanilla","maple","honey","pistachio",
      "orange blossom","cardamom","cinnamon","apple","pear","strawberry","matcha","hazelnut","cheese"
    ];

    const descriptors = [
      "rustic","buttery","flaky","glazed","honeyed","zesty","decadent","airy","golden","seeded",
      "brown-butter","sourdough","spiced","sweet-savoury","heritage"
    ];

    const difficulties = ["Easy","Easy","Medium","Medium","Hard"];

    function capitalize(s){ return s.charAt(0).toUpperCase() + s.slice(1); }

    function rand(arr){ return arr[Math.floor(Math.random()*arr.length)]; }
    function randInt(min,max){ return Math.floor(Math.random()*(max-min+1))+min; }

    // create 900 recipes
    const RECIPES_COUNT = 900;
    const recipes = [];

    for(let i=1;i<=RECIPES_COUNT;i++){
      const type = rand(pastryTypes);
      const flavor = rand(flavors);
      const desc = rand(descriptors);
      const name = `${capitalize(flavor)} ${type} — ${desc}`;
      const time = randInt(15,200); // minutes
      const difficulty = rand(difficulties);
      const popularity = Math.random();
      const ingredients = generateIngredients(type, flavor);
      const method = generateMethod(type, flavor, time);
      const notes = generateNotes(type);
      recipes.push({
        id: i,
        name,
        type,
        flavor,
        desc,
        time,
        difficulty,
        popularity,
        ingredients,
        method,
        notes
      });
    }

    // Fill type filter
    const typeFilter = document.getElementById('typeFilter');
    pastryTypes.forEach(t=>{
      const opt = document.createElement('option');
      opt.value = t;
      opt.textContent = t;
      typeFilter.appendChild(opt);
    });

    // --- Small SVG thumbnail generator (unique-ish) ---
    function svgThumb(seed, title){
      // simple decorative SVG with warm gradients and pastry initial
      const colors = [
        ["#ffd9c2","#f4b49f"],
        ["#ffeede","#f7d7c1"],
        ["#ffdfe8","#f9cfe5"],
        ["#f7f0d6","#ffd9a8"],
        ["#ffeedb","#ffd6c4"]
      ];
      const c = colors[seed % colors.length];
      const initial = title ? title.charAt(0).toUpperCase() : "P";
      const svg = `
        <svg xmlns="http://www.w3.org/2000/svg" width="600" height="400" viewBox="0 0 600 400">
          <defs>
            <linearGradient id="g${seed}" x1="0" x2="1">
              <stop offset="0" stop-color="${c[0]}"/>
              <stop offset="1" stop-color="${c[1]}"/>
            </linearGradient>
            <filter id="grain${seed}">
              <feTurbulence baseFrequency="0.8" numOctaves="1" seed="${seed}"/>
              <feColorMatrix type="saturate" values="0"/>
              <feBlend mode="overlay"/>
            </filter>
          </defs>
          <rect width="100%" height="100%" fill="url(#g${seed})"/>
          <g transform="translate(40,20)">
            <rect x="12" y="12" width="560" height="360" rx="22" fill="white" opacity="0.18"/>
            <g font-family="Playfair Display, serif">
              <text x="42" y="320" font-size="54" fill="#5a3f37" opacity="0.92">${initial}</text>
              <text x="120" y="48" font-size="22" fill="#5a3f37" opacity="0.9">${escapeXml(title)}</text>
            </g>
          </g>
        </svg>`;
      return 'data:image/svg+xml;utf8,' + encodeURIComponent(svg);
    }
    function escapeXml(s){ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }

    function generateIngredients(type, flavor){
      const base = ["All-purpose flour","Butter","Granulated sugar","Salt","Eggs"];
      const extras = [];
      if(/croissant|brioche|puff/i.test(type)) extras.push("Milk","Yeast");
      if(/tart|tartelette|pie/i.test(type)) extras.push("Cold butter for crust","Icing sugar");
      if(/macaron/i.test(type)) extras.push("Almond flour","Confectioners' sugar");
      if(/baklava/i.test(type)) extras.push("Phyllo sheets","Walnuts","Honey");
      extras.push(capitalize(flavor) + " (as flavoring)");
      // pick 6-10
      const list = [...new Set([...base, ...extras])];
      return list.slice(0, Math.min(list.length, 8));
    }

    function generateMethod(type, flavor, time){
      const paragraphs = [];
      paragraphs.push(`Start by preparing the base for this ${type.toLowerCase()}. Preheat oven as required.`);
      paragraphs.push(`Combine dry ingredients, then add butter and ${/macaron/i.test(type) ? 'egg whites' : 'eggs'}.`);
      paragraphs.push(`Fold in the ${flavor} flavor - ${/puff|croissant|brioche/i.test(type) ? 'laminate the dough if required' : 'mix until smooth'}.`);
      paragraphs.push(`Proof/rest/let chill as needed. Bake for approximately ${Math.round(time*0.6)} to ${time} minutes depending on size and oven.`);
      paragraphs.push(`Finish with glaze or dusting and serve warm or at room temperature. Enjoy!`);
      return paragraphs.join('\n\n');
    }

    function generateNotes(type){
      const notes = [
        "Tip: Use high-quality butter for deeper flavor.",
        "Variation: Add citrus zest for brightness.",
        "Storage: Keep in an airtight container for up to 2 days.",
        "Make ahead: Dough can be frozen for up to 1 month."
      ];
      return rand(notes);
    }

    // --- Render grid ---
    const grid = document.getElementById('grid');
    const found = document.getElementById('found');

    function cardHTML(r){
      // small inline svg as background image
      const thumb = svgThumb(r.id, r.name);
      return `
        <article class="card" data-name="${r.name.toLowerCase()}" data-type="${r.type}" data-time="${r.time}" data-diff="${r.difficulty}" data-pop="${r.popularity}">
          <div class="thumb" style="background-image:url('${thumb}'); background-size:cover; background-position:center;">
            <div style="display:flex;gap:8px;">
              <div class="badge">${r.type}</div>
              <div class="badge">${r.time}m</div>
            </div>
          </div>
          <div class="content">
            <h3 class="title">${r.name}</h3>
            <div class="meta"><span>${capitalize(r.flavor)}</span><span>${r.difficulty}</span></div>
            <div class="actions">
              <button class="link" data-id="${r.id}">View recipe</button>
              <div style="margin-left:auto;color:var(--muted);font-size:13px">#${r.id}</div>
            </div>
          </div>
        </article>
      `;
    }

    function renderList(list){
      grid.innerHTML = list.map(cardHTML).join('');
      found.textContent = `${list.length} recipes`;
      attachCardListeners();
    }

    // initial render: show all (900)
    renderList(recipes);

    // --- Interactions: search, filter, sort ---
    const q = document.getElementById('q');
    const clearBtn = document.getElementById('clear');
    const sortSel = document.getElementById('sort');

    function filterAndSort(){
      const term = q.value.trim().toLowerCase();
      const type = typeFilter.value;
      let result = recipes.filter(r=>{
        if(type !== 'all' && r.type !== type) return false;
        if(!term) return true;
        const hay = `${r.name} ${r.type} ${r.flavor} ${r.desc}`.toLowerCase();
        return hay.includes(term);
      });

      // sorting
      const sortVal = sortSel.value;
      if(sortVal === 'time-asc') result.sort((a,b)=>a.time-b.time);
      else if(sortVal === 'time-desc') result.sort((a,b)=>b.time-a.time);
      else if(sortVal === 'difficulty') result.sort((a,b)=>a.difficulty.localeCompare(b.difficulty));
      else result.sort((a,b)=>b.popularity - a.popularity); // popular default

      renderList(result);
    }

    q.addEventListener('input', debounce(filterAndSort, 220));
    typeFilter.addEventListener('change', filterAndSort);
    sortSel.addEventListener('change', filterAndSort);
    clearBtn.addEventListener('click', ()=>{ q.value=''; filterAndSort(); });

    // --- modal handling ---
    const overlay = document.getElementById('overlay');
    const closeBtn = document.getElementById('closeBtn');
    const modalTitle = document.getElementById('modalTitle');
    const modalDesc = document.getElementById('modalDesc');
    const modalMethod = document.getElementById('modalMethod');
    const modalNotes = document.getElementById('modalNotes');
    const modalIngredients = document.getElementById('modalIngredients');
    const modalType = document.getElementById('modalType');
    const modalTime = document.getElementById('modalTime');
    const modalDiff = document.getElementById('modalDiff');
    const modalThumb = document.getElementById('modalThumb');

    function attachCardListeners(){
      document.querySelectorAll('.link').forEach(btn=>{
        btn.addEventListener('click', (e)=>{
          const id = Number(btn.getAttribute('data-id'));
          openModalFor(id);
        });
      });
    }

    function openModalFor(id){
      const r = recipes.find(x=>x.id===id);
      if(!r) return;
      modalTitle.textContent = r.name;
      modalDesc.textContent = `${r.desc.charAt(0).toUpperCase() + r.desc.slice(1)} pastry with ${r.flavor} notes.`;
      modalMethod.innerText = r.method;
      modalNotes.textContent = r.notes;
      modalType.textContent = r.type;
      modalTime.textContent = r.time + ' min';
      modalDiff.textContent = r.difficulty;
      modalIngredients.innerHTML = r.ingredients.map(ing=>`<li>${ing}</li>`).join('');
      modalThumb.style.backgroundImage = `url('${svgThumb(r.id, r.name)}')`;
      modalThumb.style.backgroundSize = 'cover';
      modalThumb.style.backgroundPosition = 'center';
      overlay.classList.add('open');
      overlay.setAttribute('aria-hidden','false');
      document.body.style.overflow = 'hidden';
    }

    closeBtn.addEventListener('click', closeModal);
    overlay.addEventListener('click', (e)=>{
      if(e.target === overlay) closeModal();
    });
    document.addEventListener('keydown', (e)=>{ if(e.key === 'Escape') closeModal(); });

    function closeModal(){
      overlay.classList.remove('open');
      overlay.setAttribute('aria-hidden','true');
      document.body.style.overflow = '';
    }

    // Debounce helper
    function debounce(fn, wait){
      let t;
      return function(...args){
        clearTimeout(t);
        t = setTimeout(()=>fn.apply(this,args), wait);
      };
    }

    // Timestamp
    document.getElementById('timeStamp').textContent = (new Date()).toLocaleDateString();

    // Accessibility: announce initial count
    document.getElementById('found').setAttribute('aria-live','polite');

    // Done
  </script>
</body>
</html>

Lua Puzzle Game Luau Code
Luau Puzzle Solving Game
local Module = {}

-- distance tolerance (studs) to snap
Module.SNAP_DISTANCE = 3

-- Validate that pieceId and target exist and compute distance
function Module.getTargetForPiece(pieceId)
    local snapFolder = game.Workspace:FindFirstChild("SnapTargets")
    if not snapFolder then return nil end
    -- try by Name first
    local target = snapFolder:FindFirstChild(tostring(pieceId))
    if target then return target end
    -- fallback: search attributes
    for _, v in ipairs(snapFolder:GetChildren()) do
        if v:GetAttribute("PieceId") == pieceId then
            return v
        end
    end
    return nil
end

-- checks if piece is within snap distance
function Module.isCloseEnough(piecePrimaryPart, targetPart, maxDistance)
    maxDistance = maxDistance or Module.SNAP_DISTANCE
    if not (piecePrimaryPart and targetPart) then return false end
    local dist = (piecePrimaryPart.Position - targetPart.Position).Magnitude
    return dist <= maxDistance, dist
end

-- welds a piece model to the target: makes it fixed on server
-- returns the created WeldConstraint so you can later break it
function Module.weldPieceToTarget(pieceModel, targetPart)
    if not pieceModel.PrimaryPart then return nil end
    -- Move piece to exact target CFrame
    local targetCFrame = targetPart.CFrame
    pieceModel:SetPrimaryPartCFrame(targetCFrame)
    -- Make collision and anchored state: anchor primary part, set others non-collidable if you want
    -- We'll anchor the whole model by setting PrimaryPart.Anchored = true and disabling collisions optionally
    local prim = pieceModel.PrimaryPart
    prim.Anchored = true

    -- Optionally, create a WeldConstraint for consistency (though anchored is enough)
    local weld = Instance.new("WeldConstraint")
    weld.Name = "PuzzleWeld"
    weld.Part0 = prim
    weld.Part1 = targetPart
    weld.Parent = prim

    -- Lock network ownership (server has authority)
    -- Note: setNetworkOwner only works on BaseParts and only on the client for physics, server keeps authoritative anchor
    return weld
end

return Module

-- PuzzleServer (Script) - ServerScriptService
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local PuzzleRemotes = ReplicatedStorage:WaitForChild("PuzzleRemotes")
local RequestPlacePiece = PuzzleRemotes:WaitForChild("RequestPlacePiece")
local PiecePlaced = PuzzleRemotes:WaitForChild("PiecePlaced")

local PuzzleUtils = require(ReplicatedStorage:WaitForChild("PuzzleUtils"))

-- track placed pieces (so they can't be re-placed)
local placedPieces = {}

-- Validate and place piece
RequestPlacePiece.OnServerEvent:Connect(function(player, pieceInstance, pieceId)
    -- Security checks:
    -- 1) Make sure pieceInstance is actually a child of Workspace.PuzzlePieces and is a Model
    if typeof(pieceInstance) ~= "Instance" or not pieceInstance:IsDescendantOf(game.Workspace) then
        return
    end
    local piecesFolder = game.Workspace:FindFirstChild("PuzzlePieces")
    if not piecesFolder then return end
    if not pieceInstance:IsDescendantOf(piecesFolder) then
        return
    end
    if placedPieces[pieceId] then
        -- already placed, ignore
        return
    end

    -- 2) find the server-side target for piece
    local target = PuzzleUtils.getTargetForPiece(pieceId)
    if not target then
        return
    end

    -- 3) Ensure the piece has a PrimaryPart and compute distance
    local primary = pieceInstance.PrimaryPart
    if not primary then return end

    local closeEnough, dist = PuzzleUtils.isCloseEnough(primary, target)
    if not closeEnough then
        -- Not close enough: ignore or optionally send failure feedback
        return
    end

    -- 4) All good: weld/anchor it on the server and mark placed
    local weld = PuzzleUtils.weldPieceToTarget(pieceInstance, target)
    placedPieces[pieceId] = {
        piece = pieceInstance,
        target = target,
        weld = weld,
        placedBy = player
    }

    -- 5) Broadcast placement to all clients (so they can update client visuals)
    PiecePlaced:FireAllClients(pieceInstance, pieceId, player)

    -- 6) Check completion
    -- A simple completion check: if all pieces in PuzzlePieces are placed
    local allPlaced = true
    for _, piece in ipairs(piecesFolder:GetChildren()) do
        local id = piece:GetAttribute("PieceId") or piece.Name
        if not placedPieces[tostring(id)] then
            allPlaced = false
            break
        end
    end

    if allPlaced then
        print(player.Name .. " finished the puzzle!")
        -- Optionally fire a different event or award the player
        -- e.g. Reward the player:
        -- player:Kick("Nice job!") -- don't actually do that
    end
end)

-- PuzzleClient (LocalScript) - StarterPlayerScripts
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local player = Players.LocalPlayer
local mouse = player:GetMouse()

local PuzzleRemotes = ReplicatedStorage:WaitForChild("PuzzleRemotes")
local RequestPlacePiece = PuzzleRemotes:WaitForChild("RequestPlacePiece")
local PiecePlaced = PuzzleRemotes:WaitForChild("PiecePlaced")

local holding = nil -- current model
local holdOffset = CFrame.new()
local dragDistance = 8 -- vertical offset while holding
local rotateStep = math.rad(15)

-- helper to get the model under mouse
local function getPieceUnderMouse()
    local target = mouse.Target
    if not target then return nil end
    local model = target:FindFirstAncestorOfClass("Model")
    if not model then return nil end
    if model:IsDescendantOf(game.Workspace:FindFirstChild("PuzzlePieces") or {}) then
        return model
    end
    return nil
end

-- when a piece gets placed by server, if we are holding it, we should release it
PiecePlaced.OnClientEvent:Connect(function(pieceInstance, pieceId, placedBy)
    if holding and pieceInstance == holding then
        -- drop it locally to avoid weird client interference
        holding = nil
    end
    -- Optionally: play sound or highlight
end)

-- pick up on click
mouse.Button1Down:Connect(function()
    if holding then return end
    local piece = getPieceUnderMouse()
    if not piece then return end
    -- store local state
    holding = piece

    -- compute offset between primary part CFrame and mouse.Hit
    if holding.PrimaryPart then
        local hit = mouse.Hit
        holdOffset = holding.PrimaryPart.CFrame:ToObjectSpace(hit)
    end

    -- optional: change appearance to indicate holding
    if holding.PrimaryPart then
        holding.PrimaryPart.LocalTransparencyModifier = 0 -- client-only visual change (doesn't replicate)
    end
end)

-- drop / place on release
mouse.Button1Up:Connect(function()
    if not holding then return end

    -- send request to server to place. Provide the Instance (must be in Workspace) and the pieceId
    local id = holding:GetAttribute("PieceId") or holding.Name
    RequestPlacePiece:FireServer(holding, tostring(id))

    -- We don't instantly anchor on the client; server will tell all clients via PiecePlaced
    holding = nil
end)

-- rotate piece while holding (Q/E)
UserInputService.InputBegan:Connect(function(input, gameProcessed)
    if gameProcessed then return end
    if not holding then return end
    if input.KeyCode == Enum.KeyCode.Q then
        -- rotate left around Y
        local prim = holding.PrimaryPart
        if prim then
            prim.CFrame = prim.CFrame * CFrame.Angles(0, rotateStep, 0)
        end
    elseif input.KeyCode == Enum.KeyCode.E then
        local prim = holding.PrimaryPart
        if prim then
            prim.CFrame = prim.CFrame * CFrame.Angles(0, -rotateStep, 0)
        end
    end
end)

-- smooth follow while holding
RunService.RenderStepped:Connect(function(dt)
    if not holding or not holding.PrimaryPart then return end
    -- Move the piece to the mouse.Hit position plus a vertical offset so it doesn't clip into floor
    local hit = mouse.Hit
    local targetCFrame = CFrame.new(hit.Position + Vector3.new(0, dragDistance, 0)) * CFrame.Angles(0, holding.PrimaryPart.Orientation.Y * math.pi/180, 0)
    -- A gentle lerp for smoothing
    local current = holding.PrimaryPart.CFrame
    local newCFrame = current:Lerp(targetCFrame, math.clamp(15 * dt, 0, 1))
    holding:SetPrimaryPartCFrame(newCFrame)
end)

Availability

I am currently not available until Christmas 2025 because my laptop was broken but I will take pre orders if you want.

Payment

Payment is negotiable and I would perfect being paid in Robux since Cashapp and Venmo is difficult.

Contact

You can contact me through the devforum or through Roblox since my discord is suspended. I am also working on getting a twitter..

Thanks for reading! :slight_smile:

2 Likes

Sorry the organizing is a little messed up the last part of the 2nd box is actually supposed to be apart of the 3rd box and the 3rd box of code is a puzzle game script

1 Like