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,'&').replace(/</g,'<').replace(/>/g,'>'); }
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! ![]()