File size: 13,856 Bytes
584ea9d 581222f 584ea9d 581222f 584ea9d 581222f 584ea9d 581222f 584ea9d 581222f 584ea9d 581222f 584ea9d 581222f 584ea9d 581222f 584ea9d 581222f 584ea9d 581222f 584ea9d 581222f 584ea9d 581222f 584ea9d 581222f 584ea9d 581222f 584ea9d 581222f 584ea9d 581222f 584ea9d 581222f 584ea9d 581222f 584ea9d 581222f 584ea9d 581222f 584ea9d 581222f 584ea9d 581222f 584ea9d 581222f 584ea9d 581222f 584ea9d 581222f 584ea9d 581222f 584ea9d |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 |
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fantasy Rally: Card Architect</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<style>
.card-foil {
background: linear-gradient(135deg, rgba(255,255,255,0.4) 0%, rgba(255,255,255,0) 50%, rgba(255,255,255,0.4) 100%);
background-size: 200% 200%;
animation: shine 3s infinite linear;
}
@keyframes shine { 0% { background-position: -200% -200%; } 100% { background-position: 200% 200%; } }
#three-container canvas { border-radius: 1rem; cursor: grab; }
#three-container canvas:active { cursor: grabbing; }
.thumbnail-preview { width: 45px; height: 60px; object-fit: cover; border-radius: 4px; border: 1px solid #475569; cursor: pointer; }
.thumbnail-preview:hover { border-color: #3b82f6; }
</style>
</head>
<body class="bg-slate-900 text-white font-sans p-8">
<div class="max-w-7xl mx-auto grid grid-cols-1 lg:grid-cols-2 gap-8">
<div class="space-y-6">
<header>
<h1 class="text-3xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-blue-400 to-purple-500">
Fantasy Rally: Card Architect
</h1>
<p class="text-slate-400">Manage car customizer rewards and rarities.</p>
</header>
<div class="bg-slate-800 rounded-xl p-6 border border-slate-700">
<div class="flex justify-between mb-4">
<h2 class="text-xl font-semibold">Reward Inventory</h2>
<div class="space-x-2">
<button onclick="selectAll()" class="text-xs bg-slate-700 px-3 py-1 rounded hover:bg-slate-600">Select All</button>
<button onclick="exportSQL()" class="text-xs bg-green-600 px-3 py-1 rounded hover:bg-green-500 font-bold">Export SQL</button>
</div>
</div>
<div class="overflow-x-auto max-h-96 overflow-y-auto">
<table id="rewardTable" class="w-full text-left text-sm">
<thead class="bg-slate-900 sticky top-0">
<tr>
<th class="p-2 w-8"><input type="checkbox" id="masterCheck" onclick="toggleAll(this)"></th>
<th class="p-2 w-16">Image</th>
<th class="p-2">Title</th>
<th class="p-2">Description</th>
<th class="p-2">Rarity</th>
<th class="p-2 w-8"></th>
</tr>
</thead>
<tbody id="tableBody"></tbody>
</table>
</div>
<button onclick="addRow()" class="mt-4 w-full py-2 bg-blue-600 rounded-lg hover:bg-blue-500 transition-colors">+ Add New Card</button>
<input type="file" id="hiddenFileInput" accept="image/*" class="hidden" onchange="handleFileSelect(event)">
</div>
</div>
<div class="sticky top-8 h-fit">
<div class="bg-slate-800 rounded-xl p-6 border border-slate-700 flex flex-col items-center">
<h2 class="text-xl font-semibold mb-4 w-full">3D Card Viewer</h2>
<div id="three-container" class="w-full aspect-[3/4] bg-black rounded-lg overflow-hidden shadow-2xl relative">
<canvas id="cardCanvas" width="512" height="716" style="display:none;"></canvas>
</div>
<div class="mt-4 text-center text-slate-400 text-sm italic">
Click a card thumbnail to upload image | Drag to rotate | Click 3D for auto-spin
</div>
</div>
</div>
</div>
<script>
let rewards = [
{ id: 1, title: "Neon Underglow", desc: "Cyberpunk aesthetic pulses.", color: "#3b82f6", rarity: "Rare", selected: false, img: "https://images.unsplash.com/photo-1552519507-da3b142c6e3d?auto=format&fit=crop&w=400&q=80" },
{ id: 2, title: "Quantum Turbo", desc: "Breaks the sound barrier.", color: "#a855f7", rarity: "Legendary", selected: false, img: "https://images.unsplash.com/photo-1503376780353-7e6692767b70?auto=format&fit=crop&w=400&q=80" }
];
let scene, camera, renderer, cardMesh, currentEditingIndex = null;
let isRotating = true;
function init3D() {
const container = document.getElementById('three-container');
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(45, container.clientWidth / container.clientHeight, 0.1, 1000);
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(container.clientWidth, container.clientHeight);
container.appendChild(renderer.domElement);
scene.add(new THREE.AmbientLight(0xffffff, 0.9));
const light = new THREE.PointLight(0xffffff, 1.2);
light.position.set(5, 5, 5);
scene.add(light);
previewCard(0);
camera.position.z = 5;
animate();
}
async function createCardTexture(data) {
const canvas = document.getElementById('cardCanvas');
const ctx = canvas.getContext('2d');
ctx.fillStyle = data.color;
ctx.fillRect(0, 0, canvas.width, canvas.height);
const img = new Image();
img.crossOrigin = "anonymous";
img.src = data.img;
return new Promise((resolve) => {
img.onload = () => {
ctx.drawImage(img, 20, 20, canvas.width - 40, canvas.height * 0.65);
ctx.fillStyle = "rgba(0,0,0,0.7)";
ctx.fillRect(0, canvas.height * 0.7, canvas.width, canvas.height * 0.3);
ctx.fillStyle = "white";
ctx.font = "bold 38px sans-serif";
ctx.fillText(data.title.toUpperCase(), 30, canvas.height * 0.78);
ctx.font = "22px sans-serif";
ctx.fillStyle = "#cbd5e1";
wrapText(ctx, data.desc, 30, canvas.height * 0.84, canvas.width - 60, 28);
ctx.fillStyle = data.color;
ctx.fillRect(canvas.width - 150, 30, 120, 45);
ctx.fillStyle = "white";
ctx.font = "bold 20px sans-serif";
ctx.textAlign = "center";
ctx.fillText(data.rarity, canvas.width - 90, 60);
ctx.textAlign = "left";
resolve(new THREE.CanvasTexture(canvas));
};
});
}
function wrapText(ctx, text, x, y, maxWidth, lineHeight) {
let words = text.split(' '), line = '';
for(let n = 0; n < words.length; n++) {
let testLine = line + words[n] + ' ';
if (ctx.measureText(testLine).width > maxWidth && n > 0) {
ctx.fillText(line, x, y);
line = words[n] + ' ';
y += lineHeight;
} else { line = testLine; }
}
ctx.fillText(line, x, y);
}
async function createCard(data) {
if (cardMesh) scene.remove(cardMesh);
const texture = await createCardTexture(data);
const materials = [
new THREE.MeshStandardMaterial({ color: 0x333333 }),
new THREE.MeshStandardMaterial({ color: 0x333333 }),
new THREE.MeshStandardMaterial({ color: 0x333333 }),
new THREE.MeshStandardMaterial({ color: 0x333333 }),
new THREE.MeshStandardMaterial({ map: texture }),
new THREE.MeshStandardMaterial({ color: 0x111111 })
];
cardMesh = new THREE.Mesh(new THREE.BoxGeometry(2.5, 3.5, 0.1), materials);
if (data.rarity === "Legendary") {
const foil = new THREE.Mesh(new THREE.PlaneGeometry(2.5, 3.5), new THREE.MeshPhongMaterial({
color: 0xffffff, specular: 0xffffff, shininess: 100, transparent: true, opacity: 0.25, map: texture
}));
foil.position.z = 0.051;
cardMesh.add(foil);
}
scene.add(cardMesh);
}
function animate() {
requestAnimationFrame(animate);
if (cardMesh && isRotating) cardMesh.rotation.y += 0.01;
renderer.render(scene, camera);
}
function renderTable() {
const tbody = document.getElementById('tableBody');
tbody.innerHTML = '';
rewards.forEach((reward, index) => {
const tr = document.createElement('tr');
tr.className = "border-b border-slate-700 hover:bg-slate-700/50 transition-colors cursor-pointer";
tr.onclick = (e) => { if(!['INPUT', 'BUTTON', 'SELECT'].includes(e.target.tagName)) previewCard(index); };
tr.innerHTML = `
<td class="p-2"><input type="checkbox" ${reward.selected ? 'checked' : ''} onchange="toggleSelect(${index})"></td>
<td class="p-2"><img src="${reward.img}" class="thumbnail-preview" onclick="triggerUpload(${index})"></td>
<td class="p-2"><input class="bg-transparent border-none focus:ring-1 ring-blue-500 rounded px-1 w-full" value="${reward.title}" onchange="updateReward(${index}, 'title', this.value)"></td>
<td class="p-2"><input class="bg-transparent border-none focus:ring-1 ring-blue-500 rounded px-1 w-full" value="${reward.desc}" onchange="updateReward(${index}, 'desc', this.value)"></td>
<td class="p-2 flex items-center gap-2">
<select class="bg-slate-900 border-none text-xs rounded p-1" onchange="updateReward(${index}, 'rarity', this.value)">
<option value="Common" ${reward.rarity === 'Common' ? 'selected' : ''}>Common</option>
<option value="Rare" ${reward.rarity === 'Rare' ? 'selected' : ''}>Rare</option>
<option value="Legendary" ${reward.rarity === 'Legendary' ? 'selected' : ''}>Legendary</option>
</select>
<input type="color" value="${reward.color}" onchange="updateReward(${index}, 'color', this.value)" class="w-6 h-6 bg-transparent border-none">
</td>
<td class="p-2"><button onclick="deleteRow(${index})" class="text-red-400 hover:text-red-300">✕</button></td>
`;
tbody.appendChild(tr);
});
}
function triggerUpload(index) { currentEditingIndex = index; document.getElementById('hiddenFileInput').click(); }
function handleFileSelect(e) {
const file = e.target.files[0];
if (file && currentEditingIndex !== null) {
const reader = new FileReader();
reader.onload = (event) => { updateReward(currentEditingIndex, 'img', event.target.result); };
reader.readAsDataURL(file);
}
}
function addRow() {
rewards.push({ id: Date.now(), title: "New Item", desc: "Description here", color: "#64748b", rarity: "Common", selected: false, img: "https://via.placeholder.com/400x300/334155/ffffff?text=Upload+Image" });
renderTable();
}
function deleteRow(index) { rewards.splice(index, 1); renderTable(); }
function updateReward(index, field, value) { rewards[index][field] = value; renderTable(); previewCard(index); }
function toggleSelect(index) { rewards[index].selected = !rewards[index].selected; }
function toggleAll(master) { rewards.forEach(r => r.selected = master.checked); renderTable(); }
function selectAll() { rewards.forEach(r => r.selected = true); document.getElementById('masterCheck').checked = true; renderTable(); }
function previewCard(index) { createCard(rewards[index]); }
function exportSQL() {
const selected = rewards.filter(r => r.selected);
if (selected.length === 0) return alert("Select cards to export.");
let sql = "INSERT INTO cards (title, description, rarity, color, img_data) VALUES \n";
const values = selected.map(r => `('${r.title.replace(/'/g, "''")}', '${r.desc.replace(/'/g, "''")}', '${r.rarity}', '${r.color}', '${r.img.substring(0,50)}...')`).join(",\n");
const blob = new Blob([sql + values + ";"], { type: 'text/sql' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a'); a.href = url; a.download = "cards.sql"; a.click();
}
let isDragging = false, prevX = 0;
const cont = document.getElementById('three-container');
cont.addEventListener('mousedown', (e) => { isDragging = true; isRotating = false; prevX = e.clientX; });
window.addEventListener('mouseup', () => isDragging = false);
window.addEventListener('mousemove', (e) => { if (isDragging && cardMesh) { cardMesh.rotation.y += (e.clientX - prevX) * 0.01; prevX = e.clientX; }});
cont.addEventListener('click', (e) => { if (e.target.tagName === 'CANVAS') isRotating = !isRotating; });
window.onload = () => { renderTable(); init3D(); };
</script>
</body>
</html> |