black-dragon-backend / index.html
roky-dev's picture
Upload index.html
3976584 verified
Raw
History Blame Contribute Delete
162 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>Black Dragon AI</title>
<script src="black-dragon.config.js"></script>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--primary: #35f6a3;
--primary-2: #ffd166;
--accent: #8f7cff;
--danger: #ff4f64;
--warn: #ffd166;
--bg: #07090c;
--panel: #10151a;
--panel-2: #182029;
--ink: #eef7f2;
--muted: #9aabb0;
--line: #24343b;
--shadow: 0 18px 40px rgba(0, 0, 0, .38);
}
body {
min-height: 100vh;
overflow: hidden;
display: flex;
color: var(--ink);
background:
radial-gradient(circle at 18% 10%, rgba(53, 246, 163, .15), transparent 24rem),
radial-gradient(circle at 92% 70%, rgba(143, 124, 255, .12), transparent 28rem),
radial-gradient(circle at 52% 100%, rgba(255, 207, 90, .08), transparent 22rem),
var(--bg);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
button, input, select, textarea { font: inherit; }
button { cursor: pointer; }
.sidebar {
width: 280px;
min-width: 280px;
height: 100vh;
display: flex;
flex-direction: column;
background: rgba(6, 10, 12, .92);
border-right: 1px solid rgba(53, 246, 163, .38);
transition: transform .25s ease;
z-index: 30;
}
.sidebar.hidden { transform: translateX(-100%); position: absolute; left: 0; top: 0; }
.side-head, .topbar, .composer { border-color: rgba(53, 246, 163, .34); }
.side-head {
padding: 14px;
border-bottom: 1px solid;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.brand, .side-title {
display: flex;
align-items: center;
gap: 8px;
color: var(--primary);
font-weight: 800;
letter-spacing: .02em;
}
.round, .pill, .chip, .tab, .icon-btn, .game-btn {
border: 1px solid var(--line);
background: var(--panel-2);
color: var(--ink);
}
.round, .icon-btn {
width: 40px;
height: 40px;
border-radius: 50%;
display: grid;
place-items: center;
flex: none;
}
.top-actions .round, .composer .round {
width: auto;
min-width: 42px;
border-radius: 8px;
padding: 0 12px;
font-size: 13px;
white-space: nowrap;
}
.round.primary, .pill.primary, .game-btn.primary { background: var(--primary); color: #00120b; border-color: var(--primary); font-weight: 800; }
.chat-list { overflow-y: auto; padding: 12px; flex: 1; }
.chat-item {
padding: 12px 36px 12px 12px;
border: 1px solid var(--line);
background: rgba(23, 31, 38, .86);
border-radius: 8px;
margin-bottom: 9px;
position: relative;
}
.chat-item.active { border-color: var(--primary); background: rgba(53, 246, 163, .11); }
.chat-item strong { display: block; color: var(--primary); font-size: 13px; margin-bottom: 4px; }
.chat-item small { color: var(--muted); font-size: 11px; }
.chat-delete { position: absolute; right: 10px; top: 10px; color: var(--danger); opacity: .72; }
.app {
height: 100vh;
min-width: 0;
flex: 1;
display: flex;
flex-direction: column;
}
.topbar {
height: 62px;
flex: none;
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
border-bottom: 1px solid;
background: rgba(8, 12, 14, .84);
backdrop-filter: blur(12px);
}
.top-left, .top-actions, .status {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.status { color: var(--muted); font-size: 12px; }
.dot { width: 8px; height: 8px; border-radius: 50%; background: var(--primary); box-shadow: 0 0 16px var(--primary); }
.chat {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 18px clamp(12px, 3vw, 34px);
}
.welcome {
max-width: 980px;
margin: 0 auto 18px;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
}
.quick {
background: rgba(17, 22, 27, .78);
border: 1px solid var(--line);
border-radius: 8px;
padding: 12px;
color: var(--muted);
min-height: 78px;
text-align: left;
}
.quick strong { display: block; color: var(--ink); margin-bottom: 4px; }
.message {
max-width: 860px;
margin: 10px auto;
display: flex;
gap: 10px;
align-items: flex-start;
}
.message.user { flex-direction: row-reverse; }
.avatar {
width: 34px;
height: 34px;
border-radius: 50%;
flex: none;
display: grid;
place-items: center;
background: var(--panel-2);
border: 1px solid var(--line);
}
.bubble {
max-width: min(720px, calc(100vw - 110px));
border: 1px solid var(--line);
background: rgba(17, 22, 27, .9);
border-radius: 8px;
padding: 12px 14px;
line-height: 1.5;
white-space: pre-wrap;
overflow-wrap: anywhere;
box-shadow: 0 8px 18px rgba(0,0,0,.18);
}
.user .bubble {
background: rgba(53, 246, 163, .12);
border-color: rgba(53, 246, 163, .5);
}
.meta { margin-top: 6px; color: #74848a; font-size: 10px; text-align: right; }
.typing { display: flex; gap: 5px; width: fit-content; }
.typing span {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--primary);
animation: bounce 1.2s infinite ease-in-out;
}
.typing span:nth-child(2) { animation-delay: .15s; }
.typing span:nth-child(3) { animation-delay: .3s; }
@keyframes bounce { 0%, 80%, 100% { transform: translateY(0); opacity: .55; } 40% { transform: translateY(-7px); opacity: 1; } }
.composer {
flex: none;
border-top: 1px solid;
background: rgba(8, 12, 14, .9);
padding: 12px;
}
.input-row {
max-width: 980px;
margin: 0 auto;
display: flex;
align-items: flex-end;
gap: 8px;
}
.attachment-strip {
max-width: 980px;
margin: 8px auto 0;
display: none;
flex-wrap: wrap;
gap: 8px;
}
.attachment-strip.show { display: flex; }
.attachment-chip {
min-height: 34px;
display: inline-flex;
align-items: center;
gap: 8px;
max-width: min(310px, 100%);
padding: 7px 10px;
border: 1px solid var(--line);
border-radius: 8px;
background: rgba(23, 31, 38, .92);
color: var(--muted);
font-size: 12px;
}
.attachment-chip strong {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--ink);
}
.attachment-chip button {
width: 22px;
height: 22px;
border: 0;
border-radius: 50%;
background: rgba(255, 79, 100, .18);
color: var(--danger);
flex: none;
}
textarea {
flex: 1;
min-height: 46px;
max-height: 140px;
resize: none;
border: 1px solid var(--line);
border-radius: 8px;
padding: 12px 14px;
color: var(--ink);
background: var(--panel);
outline: none;
}
textarea:focus { border-color: var(--primary); }
.menu {
position: fixed;
right: 12px;
bottom: 74px;
width: min(360px, calc(100vw - 24px));
max-height: min(470px, calc(100vh - 132px));
overflow-y: auto;
display: none;
padding: 10px;
background: rgba(17, 22, 27, .98);
border: 1px solid rgba(53, 246, 163, .42);
border-radius: 10px;
box-shadow: var(--shadow);
z-index: 50;
scrollbar-width: thin;
}
.menu.show { display: block; }
.section { padding: 8px 0; border-bottom: 1px solid var(--line); }
.section:last-child { border-bottom: 0; }
.menu .section:first-child { padding-top: 0; }
.section-title { color: var(--muted); font-size: 10px; font-weight: 800; margin-bottom: 7px; text-transform: uppercase; letter-spacing: .04em; }
.field {
width: 100%;
border: 1px solid var(--line);
border-radius: 8px;
background: #090d10;
color: var(--ink);
padding: 10px 11px;
margin-bottom: 8px;
outline: none;
}
.field:focus { border-color: var(--primary); }
.hint {
color: var(--muted);
font-size: 11px;
line-height: 1.35;
margin: -2px 0 8px;
}
.row { display: flex; gap: 8px; align-items: center; }
.grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px; }
.menu .grid { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; }
.pill, .tab, .game-btn {
border-radius: 8px;
padding: 10px 12px;
text-align: center;
min-height: 40px;
}
.menu .pill, .menu .tab {
min-height: 34px;
padding: 7px 8px;
font-size: 12px;
line-height: 1.15;
}
.chip { border-radius: 20px; padding: 7px 10px; font-size: 12px; color: var(--muted); }
.tab.active, .pill.active { border-color: var(--primary); color: #00120b; background: var(--primary); font-weight: 800; }
.notice {
position: fixed;
top: 74px;
right: 16px;
background: var(--panel-2);
border: 1px solid var(--primary);
color: var(--primary);
padding: 10px 14px;
border-radius: 8px;
box-shadow: var(--shadow);
z-index: 80;
max-width: 320px;
font-size: 13px;
}
.game-zone {
display: none;
position: fixed;
inset: 74px 14px 84px 14px;
background: rgba(8, 12, 14, .98);
border: 1px solid rgba(53, 246, 163, .45);
border-radius: 10px;
z-index: 60;
box-shadow: var(--shadow);
overflow: hidden;
}
.game-zone.show { display: grid; grid-template-rows: auto 1fr; }
.mode-zone {
display: none;
position: fixed;
inset: 74px 14px 84px 14px;
background: rgba(8, 12, 14, .98);
border: 1px solid rgba(53, 246, 163, .45);
border-radius: 10px;
z-index: 65;
box-shadow: var(--shadow);
overflow: hidden;
}
.mode-zone.show { display: grid; grid-template-rows: auto 1fr; }
.mode-tabs, .mode-head {
display: flex;
align-items: center;
gap: 8px;
}
.mode-head {
justify-content: space-between;
padding: 12px;
border-bottom: 1px solid var(--line);
}
.mode-body {
min-height: 0;
overflow: auto;
padding: 14px;
}
.mode-panel { display: none; max-width: 1120px; margin: 0 auto; }
.mode-panel.active { display: block; }
.split {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.tool-surface {
border: 1px solid var(--line);
border-radius: 8px;
background: rgba(17, 22, 27, .82);
padding: 12px;
}
.output {
min-height: 120px;
max-height: 320px;
overflow: auto;
white-space: pre-wrap;
overflow-wrap: anywhere;
color: var(--ink);
background: #090d10;
border: 1px solid var(--line);
border-radius: 8px;
padding: 10px;
margin-top: 8px;
font-size: 13px;
}
.draw-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.draw-canvas, .view-canvas {
width: 100%;
aspect-ratio: 4 / 3;
height: auto;
background: #061016;
border: 1px solid var(--line);
border-radius: 8px;
touch-action: none;
}
.secure-list {
display: grid;
gap: 8px;
margin-top: 8px;
}
.secure-msg {
border: 1px solid var(--line);
border-radius: 8px;
padding: 8px 10px;
background: rgba(23, 31, 38, .72);
}
.secure-msg strong { color: var(--primary); }
.game-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 12px;
border-bottom: 1px solid var(--line);
}
.game-head .brand span:first-child { font-size: 0; }
.game-head .brand span:first-child::after { content: "GAME"; font-size: 13px; }
.game-tabs { display: flex; gap: 8px; overflow-x: auto; }
.game-area {
display: grid;
grid-template-columns: 1fr 250px;
min-height: 0;
}
.stage-wrap { display: grid; place-items: center; min-width: 0; padding: 12px; }
#gameCanvas {
width: min(100%, 860px);
height: min(100%, 540px);
max-height: calc(100vh - 190px);
aspect-ratio: 16 / 10;
background: #071018;
border: 1px solid var(--line);
border-radius: 8px;
}
.game-side {
border-left: 1px solid var(--line);
padding: 12px;
background: rgba(17, 22, 27, .78);
color: var(--muted);
overflow-y: auto;
}
.score { color: var(--primary); font-size: 26px; font-weight: 900; margin: 8px 0; }
.game-zone[data-game="race"] { border-color: rgba(255, 209, 102, .62); }
.game-zone[data-game="race"] .game-side {
background: linear-gradient(180deg, rgba(34, 25, 14, .94), rgba(12, 15, 17, .86));
}
.game-zone[data-game="breaker"] { border-color: rgba(143, 124, 255, .66); }
.game-zone[data-game="pong"] { border-color: rgba(53, 246, 163, .66); }
.game-stat-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
margin: 12px 0;
}
.game-stat {
min-height: 48px;
border: 1px solid var(--line);
border-radius: 8px;
background: rgba(8, 12, 14, .72);
padding: 8px;
}
.game-stat span {
display: block;
color: var(--muted);
font-size: 10px;
text-transform: uppercase;
letter-spacing: .08em;
}
.game-stat strong {
color: var(--ink);
font-size: 15px;
}
.game-control-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
margin-top: 12px;
}
.game-control {
min-height: 42px;
border-radius: 8px;
border: 1px solid var(--line);
background: rgba(53, 246, 163, .12);
color: var(--ink);
font-weight: 800;
touch-action: none;
user-select: none;
}
.game-control:active { background: var(--primary); color: #00120b; }
.image-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
margin-top: 8px;
}
.image-grid img {
width: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;
border-radius: 8px;
border: 1px solid var(--line);
background: #000;
}
.image-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
}
.image-actions a {
display: inline-flex;
min-height: 36px;
align-items: center;
border-radius: 8px;
border: 1px solid var(--line);
background: var(--panel-2);
color: var(--primary);
padding: 8px 10px;
text-decoration: none;
font-size: 13px;
}
.terms-overlay {
position: fixed;
inset: 0;
z-index: 200;
display: none;
place-items: center;
padding: 18px;
background: rgba(2, 6, 8, .88);
backdrop-filter: blur(10px);
}
.terms-overlay.show { display: grid; }
.terms-card {
width: min(560px, 100%);
border: 1px solid rgba(53, 246, 163, .42);
border-radius: 10px;
background: #0d1317;
box-shadow: var(--shadow);
padding: 18px;
}
.terms-card h2 {
color: var(--primary);
font-size: 21px;
margin-bottom: 8px;
}
.terms-card p, .terms-card li {
color: var(--muted);
font-size: 13px;
line-height: 1.45;
}
.terms-card ul { margin: 10px 0 12px 20px; }
.terms-links {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 12px 0;
}
.terms-links a {
color: var(--primary);
text-decoration: none;
border: 1px solid var(--line);
border-radius: 8px;
padding: 8px 10px;
background: var(--panel-2);
}
.terms-check {
display: flex;
gap: 10px;
align-items: flex-start;
margin: 12px 0;
color: var(--ink);
font-size: 13px;
}
.terms-check input { margin-top: 3px; }
.terms-card button:disabled { opacity: .45; cursor: not-allowed; }
.profile-row {
display: grid;
grid-template-columns: 1fr;
gap: 8px;
margin-top: 12px;
}
.profile-row input {
min-height: 42px;
border: 1px solid var(--line);
border-radius: 8px;
padding: 10px 12px;
background: var(--panel);
color: var(--ink);
outline: none;
}
.profile-stack {
display: grid;
gap: 10px;
margin-top: 12px;
}
.profile-divider {
color: var(--muted);
font-size: 12px;
text-align: center;
}
.search-results {
display: grid;
gap: 8px;
margin-top: 10px;
}
.search-result {
display: block;
border: 1px solid var(--line);
border-radius: 8px;
padding: 9px;
color: var(--ink);
text-decoration: none;
background: rgba(23, 31, 38, .8);
}
.search-result strong { color: var(--primary); display: block; margin-bottom: 3px; }
.search-result span { color: var(--muted); font-size: 12px; }
.generated-image {
width: 100%;
max-height: 480px;
object-fit: contain;
border: 1px solid var(--line);
border-radius: 8px;
background: #050809;
margin-top: 10px;
}
body.terms-locked .app,
body.terms-locked .sidebar,
body.terms-locked .menu,
body.terms-locked .game-zone,
body.terms-locked .mode-zone,
body.profile-locked .app,
body.profile-locked .sidebar,
body.profile-locked .menu,
body.profile-locked .game-zone,
body.profile-locked .mode-zone {
pointer-events: none;
user-select: none;
filter: blur(2px);
}
@media (max-width: 860px) {
.sidebar { position: absolute; width: min(86vw, 300px); min-width: 0; }
.menu { left: 12px; right: 12px; width: auto; max-height: min(440px, calc(100vh - 128px)); }
.menu .grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.status { display: none; }
.welcome { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.top-actions { gap: 6px; overflow-x: auto; padding-bottom: 2px; }
.mode-tabs, .game-tabs { overflow-x: auto; }
.game-area { grid-template-columns: 1fr; grid-template-rows: 1fr auto; }
.game-side { border-left: 0; border-top: 1px solid var(--line); }
.split, .draw-grid { grid-template-columns: 1fr; }
}
@media (max-width: 520px) {
.brand span:last-child { display: none; }
.welcome { grid-template-columns: 1fr; }
.grid { grid-template-columns: 1fr; }
.chat { padding: 12px 10px; }
.message { gap: 8px; margin: 8px auto; }
.avatar { display: none; }
.bubble { max-width: 100%; width: 100%; }
.topbar { padding-inline: 9px; }
.top-actions .round, .composer .round { min-width: 38px; padding: 0 9px; }
.composer { padding: 8px; }
.input-row { gap: 6px; }
.game-zone, .mode-zone { inset: 64px 6px 74px 6px; }
.mode-head, .game-head { align-items: flex-start; }
.mode-head .brand, .game-head .brand { display: none; }
}
/* Device fit upgrade: fluid desktop, laptop, tablet, phone, and touch layouts. */
html {
height: 100%;
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
}
body {
width: 100%;
min-height: 100dvh;
overscroll-behavior: none;
}
.app, .sidebar { min-height: 100dvh; }
.sidebar { height: 100dvh; }
.app {
width: 100%;
container-type: inline-size;
}
.topbar {
min-height: clamp(56px, 7dvh, 66px);
height: auto;
gap: 10px;
padding-top: max(10px, env(safe-area-inset-top));
}
.top-left { flex: 1 1 auto; }
.top-actions {
flex: 0 1 auto;
overflow-x: auto;
scrollbar-width: none;
}
.top-actions::-webkit-scrollbar,
.game-tabs::-webkit-scrollbar,
.mode-tabs::-webkit-scrollbar { display: none; }
.brand, .side-title {
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
letter-spacing: 0;
}
.chat {
padding-inline: clamp(10px, 3.2vw, 42px);
scroll-padding-bottom: 120px;
}
.welcome {
max-width: min(1120px, 100%);
grid-template-columns: repeat(auto-fit, minmax(min(190px, 100%), 1fr));
}
.quick {
min-width: 0;
overflow-wrap: anywhere;
}
.quick.power {
border-color: rgba(255, 209, 102, .52);
background: rgba(255, 209, 102, .08);
}
.message {
width: min(100%, 960px);
max-width: 960px;
}
.bubble {
max-width: min(760px, calc(100vw - 120px));
min-width: 0;
}
.composer {
padding-bottom: max(12px, env(safe-area-inset-bottom));
}
.input-row, .attachment-strip {
max-width: min(1120px, calc(100vw - 20px));
}
textarea {
width: 100%;
min-width: 0;
}
.menu {
width: min(430px, calc(100vw - 24px));
max-height: min(560px, calc(100dvh - 128px));
bottom: max(74px, calc(64px + env(safe-area-inset-bottom)));
border-radius: 8px;
}
.menu .grid {
grid-template-columns: repeat(auto-fit, minmax(min(116px, 100%), 1fr));
}
.grid {
grid-template-columns: repeat(auto-fit, minmax(min(150px, 100%), 1fr));
}
.pill, .tab, .game-btn, .game-control {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 0;
overflow-wrap: anywhere;
}
.game-zone, .mode-zone {
inset: calc(70px + env(safe-area-inset-top)) max(12px, env(safe-area-inset-right)) calc(82px + env(safe-area-inset-bottom)) max(12px, env(safe-area-inset-left));
border-radius: 8px;
}
.mode-head, .game-head {
min-width: 0;
flex-wrap: wrap;
}
.mode-tabs, .game-tabs {
min-width: 0;
flex: 1 1 auto;
overflow-x: auto;
padding-bottom: 2px;
}
.mode-panel {
max-width: min(1220px, 100%);
}
.split, .draw-grid {
grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
}
.tool-surface {
min-width: 0;
}
.draw-canvas, .view-canvas {
max-height: min(52dvh, 560px);
}
.game-area {
grid-template-columns: minmax(0, 1fr) minmax(220px, 280px);
}
#gameCanvas {
width: min(100%, 920px);
height: auto;
max-height: min(64dvh, 620px);
}
.game-side {
min-width: 0;
}
.terms-card {
max-height: calc(100dvh - 36px);
overflow: auto;
border-radius: 8px;
}
@media (min-width: 1280px) {
.sidebar { width: 300px; min-width: 300px; }
.message { max-width: 1040px; }
.bubble { max-width: 820px; }
.welcome, .input-row, .attachment-strip { max-width: 1180px; }
}
@media (max-width: 1180px) and (min-width: 861px) {
.sidebar { width: 248px; min-width: 248px; }
.top-actions .round { min-width: 40px; padding-inline: 10px; }
.bubble { max-width: min(700px, calc(100vw - 360px)); }
.menu { width: min(400px, calc(100vw - 24px)); }
.game-area { grid-template-columns: minmax(0, 1fr) 230px; }
}
@media (max-width: 860px) {
body { overflow: hidden; }
.sidebar {
height: 100dvh;
box-shadow: var(--shadow);
}
.sidebar:not(.hidden)::after {
content: "";
position: fixed;
inset: 0;
transform: translateX(100%);
background: rgba(0, 0, 0, .32);
}
.topbar { align-items: center; }
.top-actions { max-width: 58vw; }
.chat { padding-bottom: 12px; }
.bubble { max-width: calc(100vw - 84px); }
.menu {
left: 10px;
right: 10px;
width: auto;
max-height: min(520px, calc(100dvh - 122px));
}
.game-zone, .mode-zone {
inset: calc(62px + env(safe-area-inset-top)) 8px calc(74px + env(safe-area-inset-bottom)) 8px;
}
.game-area {
grid-template-columns: 1fr;
grid-template-rows: minmax(0, 1fr) auto;
}
.game-side {
max-height: min(34dvh, 230px);
}
#gameCanvas {
max-height: min(48dvh, 420px);
}
}
@media (max-width: 640px) {
.topbar {
padding-inline: 8px;
}
.top-actions { max-width: 62vw; }
.top-actions .round { min-width: 42px; }
.composer { padding-inline: 8px; }
.input-row {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 7px;
}
#userInput {
grid-column: 1 / -1;
grid-row: 1;
min-height: 48px;
}
#menuButton, #micButton, #fileButton, #sendButton {
width: 100%;
height: 44px;
min-width: 0;
}
.menu .grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.mode-body {
padding: 10px;
}
.output {
max-height: 240px;
}
.draw-canvas, .view-canvas {
max-height: 42dvh;
}
}
@media (max-width: 420px) {
.side-head { padding-inline: 10px; }
.top-actions { max-width: 58vw; }
.top-actions .round {
min-width: 40px;
padding-inline: 8px;
font-size: 12px;
}
.menu .grid, .grid {
grid-template-columns: 1fr;
}
.game-tabs .game-btn, .mode-tabs .game-btn {
min-width: 112px;
}
.game-zone, .mode-zone {
inset: calc(58px + env(safe-area-inset-top)) 4px calc(68px + env(safe-area-inset-bottom)) 4px;
}
.bubble {
padding: 11px 12px;
}
}
@media (max-height: 520px) and (orientation: landscape) {
.welcome { display: none; }
.topbar { min-height: 50px; }
.composer { padding-block: 6px; }
.menu {
top: 64px;
bottom: 64px;
max-height: calc(100dvh - 128px);
}
.game-zone, .mode-zone {
inset: 52px 6px 58px 6px;
}
.game-area {
grid-template-columns: minmax(0, 1fr) minmax(180px, 220px);
grid-template-rows: 1fr;
}
.game-side {
max-height: none;
border-left: 1px solid var(--line);
border-top: 0;
}
#gameCanvas {
max-height: calc(100dvh - 130px);
}
}
@media (hover: none), (pointer: coarse) {
.round, .pill, .tab, .game-btn, .game-control, .quick, .field, textarea {
min-height: 44px;
}
.chat-item {
min-height: 54px;
}
.chat, .menu, .mode-body, .game-side {
-webkit-overflow-scrolling: touch;
}
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: .01ms !important;
animation-iteration-count: 1 !important;
scroll-behavior: auto !important;
transition-duration: .01ms !important;
}
}
</style>
</head>
<body>
<div class="terms-overlay" id="profileOverlay" role="dialog" aria-modal="true" aria-labelledby="profileTitle">
<div class="terms-card">
<h2 id="profileTitle">Start Black Dragon</h2>
<p>Enter a display name so Black Dragon can keep your chats, notes, and memory separated on this device. This is just a local nickname stored in your browser only — it is not an account, there's no password, and nothing is sent to a server.</p>
<div class="profile-stack">
<div class="profile-row">
<input id="profileNameInput" placeholder="Display name" maxlength="80" autocomplete="off" onkeydown="if(event.key==='Enter'){ai.saveProfile()}">
<button class="pill primary" id="profileLoginButton" type="button" onclick="ai.saveProfile()">Continue</button>
</div>
</div>
<div class="hint" id="profileLoginStatus" style="margin-top:10px;"></div>
<div class="hint" style="margin-top:10px;">No API key is stored in this page. Server keys stay in the backend or hosting secrets.</div>
</div>
</div>
<aside class="sidebar" id="sidebar">
<div class="side-head">
<div class="side-title">BD Chats</div>
<button class="round primary" onclick="ai.newChat()" title="New chat">+</button>
</div>
<div class="chat-list" id="chatList"></div>
</aside>
<main class="app">
<header class="topbar">
<div class="top-left">
<button class="round" onclick="ai.toggleSidebar()" id="sidebarToggle" title="Chats">menu</button>
<div class="brand"><span>BD</span><span>BLACK DRAGON</span></div>
</div>
<div class="top-actions">
<div class="status"><span class="dot"></span><span id="statusText">Ready</span></div>
<button class="round" onclick="ai.showProfileGate(true)" id="profileButton" title="Change local display name">Profile</button>
<button class="round" onclick="ai.openMode('pc')" title="PC assistant">PC</button>
<button class="round" onclick="ai.openMode('draw')" title="Air Draw">3D</button>
<button class="round" onclick="ai.openMode('notes')" title="Notes">Notes</button>
<button class="round" onclick="ai.openGameZone()" title="Gaming zone">Games</button>
</div>
</header>
<section class="chat" id="chat">
<div class="welcome" id="welcome">
<button class="quick power" onclick="ai.toolPrompt('build')"><strong>Build feature</strong>Plan, code, verify</button>
<button class="quick power" onclick="ai.toolPrompt('review')"><strong>Review code</strong>Find bugs first</button>
<button class="quick" onclick="ai.quick('Search browser for latest AI news')"><strong>Browser search</strong>Find anything online</button>
<button class="quick" onclick="ai.quick('Show image of a black dragon')"><strong>Image lookup</strong>Get related pictures</button>
<button class="quick" onclick="ai.quick('Create a clear step-by-step production launch plan for Black Dragon AI, including frontend, backend, login, privacy, hosting, testing, security, mobile support, and release steps.')"><strong>Project plan</strong>Think like an agent</button>
<button class="quick" onclick="ai.openGameZone()"><strong>Gaming zone</strong>Play dragon games</button>
</div>
</section>
<footer class="composer">
<div class="input-row">
<button class="round" onclick="ai.toggleMenu()" id="menuButton" title="Tools">Tools</button>
<button class="round" onclick="ai.toggleRecording()" id="micButton" title="Voice input">Voice</button>
<button class="round" onclick="ai.triggerFilePicker()" id="fileButton" title="Attach file">File</button>
<textarea id="userInput" placeholder="Ask Black Dragon..." rows="1"></textarea>
<button class="round primary" onclick="ai.send()" id="sendButton" title="Send">Send</button>
</div>
<div class="attachment-strip" id="attachmentStrip"></div>
</footer>
</main>
<input id="fileInput" type="file" multiple hidden onchange="ai.handleFiles(this.files); this.value=''">
<div class="menu" id="toolMenu">
<div class="section">
<div class="section-title">Voice And Live Talk</div>
<div class="grid">
<button class="pill" onclick="ai.toggleVoice()" id="voiceBtn">Voice out: Off</button>
<button class="pill" onclick="ai.toggleLiveTalk()" id="liveBtn">Live talk: Off</button>
<button class="pill" onclick="ai.testMic()">Test mic</button>
<button class="pill" onclick="ai.stopSpeech()">Stop speech</button>
</div>
</div>
<div class="section">
<div class="section-title">Modes</div>
<div class="grid">
<button class="tab active" onclick="ai.setMode('balanced')" data-mode="balanced">Balanced</button>
<button class="tab" onclick="ai.setMode('creative')" data-mode="creative">Creative</button>
<button class="tab" onclick="ai.setMode('precise')" data-mode="precise">Precise</button>
<button class="tab" onclick="ai.setMode('coding')" data-mode="coding">Coding</button>
<button class="tab" onclick="ai.setMode('study')" data-mode="study">Study</button>
<button class="tab" onclick="ai.setMode('research')" data-mode="research">Research</button>
</div>
</div>
<div class="section">
<div class="section-title">Assistant Tools</div>
<div class="grid">
<button class="pill" onclick="ai.toolPrompt('summarize')">Summarize</button>
<button class="pill" onclick="ai.toolPrompt('rewrite')">Rewrite</button>
<button class="pill" onclick="ai.toolPrompt('translate')">Translate</button>
<button class="pill" onclick="ai.toolPrompt('code')">Code help</button>
<button class="pill" onclick="ai.toolPrompt('debug')">Debug</button>
<button class="pill" onclick="ai.toolPrompt('plan')">Plan</button>
<button class="pill" onclick="ai.toolPrompt('study')">Teach me</button>
<button class="pill" onclick="ai.toolPrompt('compare')">Compare</button>
<button class="pill" onclick="ai.toolPrompt('table')">Make table</button>
<button class="pill" onclick="ai.toolPrompt('email')">Write email</button>
<button class="pill" onclick="ai.showSystemStatus()">App status</button>
</div>
</div>
<div class="section">
<div class="section-title">Power Workbench</div>
<div class="grid">
<button class="pill primary" onclick="ai.toolPrompt('build')">Build feature</button>
<button class="pill" onclick="ai.toolPrompt('review')">Review code</button>
<button class="pill" onclick="ai.toolPrompt('fix')">Fix bug</button>
<button class="pill" onclick="ai.toolPrompt('tests')">Write tests</button>
<button class="pill" onclick="ai.toolPrompt('refactor')">Refactor</button>
<button class="pill" onclick="ai.toolPrompt('spec')">Product spec</button>
</div>
</div>
<div class="section">
<div class="section-title">Memory</div>
<div class="grid">
<button class="pill" onclick="ai.toolPrompt('remember')">Remember</button>
<button class="pill" onclick="ai.showMemory()">Show memory</button>
<button class="pill" onclick="ai.clearMemory()">Forget memory</button>
<button class="pill" onclick="ai.quick('What do you remember about me?')">Ask memory</button>
</div>
</div>
<div class="section">
<div class="section-title">Fast Commands</div>
<div class="grid">
<button class="pill" onclick="ai.browserSearch()">Browser search</button>
<button class="pill" onclick="ai.imagePrompt()">Show image</button>
<button class="pill" onclick="ai.openAppPrompt()">Open app</button>
<button class="pill" onclick="ai.triggerFilePicker()">Attach file</button>
<button class="pill" onclick="ai.openMode('pc')">PC Assistant</button>
<button class="pill" onclick="ai.openMode('draw')">Air Draw 3D</button>
<button class="pill" onclick="ai.openMode('notes')">Notes</button>
<button class="pill" onclick="ai.quick('Summarize the attached file')">Summarize file</button>
<button class="pill" onclick="ai.quick('Explain this image')">Explain image</button>
<button class="pill" onclick="ai.copyLastAnswer()">Copy answer</button>
<button class="pill" onclick="ai.downloadLastAnswer()">Download answer</button>
<button class="pill" onclick="ai.openPolicy('terms')">Terms</button>
<button class="pill" onclick="ai.openPolicy('privacy')">Privacy</button>
<button class="pill" onclick="ai.openGameZone()">Gaming zone</button>
<button class="pill" onclick="ai.exportChats()">Export chats</button>
<button class="pill" onclick="ai.resetAll()">Reset all</button>
</div>
</div>
</div>
<section class="game-zone" id="gameZone" aria-label="Gaming zone">
<div class="game-head">
<div class="brand"><span>🎮</span><span>Gaming Zone</span></div>
<div class="game-tabs">
<button class="game-btn primary" onclick="games.start('flap')">Flap Dragon</button>
<button class="game-btn" onclick="games.start('race')">Dragon Race</button>
<button class="game-btn" onclick="games.start('space')">Space Shooter</button>
<button class="game-btn" onclick="games.start('snake')">Feed Dragon</button>
<button class="game-btn" onclick="games.start('catch')">Dragon Catch</button>
<button class="game-btn" onclick="games.start('breaker')">Crystal Breaker</button>
<button class="game-btn" onclick="games.start('pong')">Dragon Pong under construction</button>
</div>
<button class="round" onclick="ai.closeGameZone()" title="Close">x</button>
</div>
<div class="game-area">
<div class="stage-wrap"><canvas id="gameCanvas" width="960" height="600"></canvas></div>
<aside class="game-side">
<div class="section-title" id="gameTitle">Flap Dragon</div>
<div class="score" id="score">0</div>
<p id="gameBest">Best: 0</p>
<p id="gameHelp">Space, click, or tap to flap. Avoid the towers.</p>
<div class="game-stat-grid" id="gameStats"></div>
<div class="game-control-grid" id="gameControls">
<button class="game-control" onpointerdown="games.pressControl('left')" onpointerup="games.releaseControl('left')" onpointerleave="games.releaseControl('left')">Left</button>
<button class="game-control" onpointerdown="games.pressControl('right')" onpointerup="games.releaseControl('right')" onpointerleave="games.releaseControl('right')">Right</button>
<button class="game-control" onpointerdown="games.pressControl('gas')" onpointerup="games.releaseControl('gas')" onpointerleave="games.releaseControl('gas')">Gas</button>
<button class="game-control" onpointerdown="games.pressControl('brake')" onpointerup="games.releaseControl('brake')" onpointerleave="games.releaseControl('brake')">Brake</button>
</div>
<div style="height:12px"></div>
<button class="pill primary" onclick="games.restart()">Restart</button>
</aside>
</div>
</section>
<section class="mode-zone" id="modeZone" aria-label="Advanced modes">
<div class="mode-head">
<div class="brand"><span>BLACK DRAGON MODES</span></div>
<div class="mode-tabs">
<button class="game-btn primary" onclick="ai.openMode('pc')" data-mode-tab="pc">PC Assistant</button>
<button class="game-btn" onclick="ai.openMode('draw')" data-mode-tab="draw">Air Draw 3D</button>
<button class="game-btn" onclick="ai.openMode('notes')" data-mode-tab="notes">Notes</button>
<button class="game-btn" onclick="ai.openMode('memory')" data-mode-tab="memory">Memory</button>
</div>
<button class="round" onclick="ai.closeMode()" title="Close">x</button>
</div>
<div class="mode-body">
<div class="mode-panel active" id="pcPanel">
<div class="split">
<div class="tool-surface">
<div class="section-title">PC Command Center</div>
<input class="field" id="pcCommand" placeholder="Try: system status, open notepad, process list, scan Downloads">
<div class="grid">
<button class="pill primary" onclick="ai.runPcCommand()">Run command</button>
<button class="pill" onclick="ai.pcStatus()">System status</button>
<button class="pill" onclick="ai.runPcCommand('open notepad')">Open Notepad</button>
<button class="pill" onclick="ai.runPcCommand('open calculator')">Open Calculator</button>
<button class="pill" onclick="ai.runPcCommand('process list')">Processes</button>
<button class="pill" onclick="ai.runPcCommand('network info')">Network</button>
<button class="pill" onclick="ai.coolingMode(false)">Cooling check</button>
<button class="pill" onclick="ai.coolingMode(true)">Cool PC</button>
<button class="pill" onclick="ai.scanCleanup()">Scan cleanup</button>
<button class="pill" onclick="ai.deleteCleanup()">Delete temp files</button>
</div>
<div class="hint">Dangerous commands are blocked. Safe app opens, status, network, process list, and scan commands are allowed.</div>
</div>
<div class="tool-surface">
<div class="section-title">Antivirus</div>
<input class="field" id="scanTarget" placeholder="Downloads, Desktop, Documents, or a full folder path">
<div class="grid">
<button class="pill primary" onclick="ai.scanPc()">Scan target</button>
<button class="pill" onclick="ai.scanPc('quick')">Quick scan</button>
<button class="pill" onclick="ai.scanPc('Downloads')">Scan Downloads</button>
<button class="pill" onclick="ai.scanPc('Desktop')">Scan Desktop</button>
</div>
<div class="hint">Uses Microsoft Defender when available, otherwise runs a suspicious-file scan.</div>
</div>
</div>
<div class="output" id="pcOutput">Ready for written or voice PC commands.</div>
</div>
<div class="mode-panel" id="drawPanel">
<div class="draw-grid">
<div class="tool-surface">
<div class="section-title">Draw 2D Shape</div>
<canvas class="draw-canvas" id="drawCanvas" width="720" height="540"></canvas>
<input class="field" id="airPrompt" placeholder="Describe model/environment: shooting range, city, forest, product test room">
<div class="grid">
<button class="pill primary" onclick="airDraw.convert()">Convert to 3D</button>
<button class="pill" onclick="airDraw.makeEnvironment()">Build environment</button>
<button class="pill" onclick="airDraw.pickImage()">Image to 3D</button>
<button class="pill" onclick="airDraw.exportObj()">Export OBJ</button>
<button class="pill" onclick="airDraw.clear()">Clear</button>
</div>
<input id="airImageInput" type="file" accept="image/*" hidden onchange="airDraw.imageTo3d(this.files[0]); this.value=''">
</div>
<div class="tool-surface">
<div class="section-title">3D Preview</div>
<canvas class="view-canvas" id="viewCanvas" width="720" height="540"></canvas>
<div class="hint" id="airOutput">Air Canvas builds an animated hologram-style 3D preview from your 2D stroke, image, or environment prompt.</div>
</div>
</div>
</div>
<div class="mode-panel" id="notesPanel">
<div class="split">
<div class="tool-surface">
<div class="section-title">Typed Note</div>
<input class="field" id="noteTitle" placeholder="Note title">
<textarea class="field" id="noteText" placeholder="Write your note..." rows="8"></textarea>
<div class="section-title">Hand Drawn Note</div>
<canvas class="draw-canvas" id="noteCanvas" width="720" height="360"></canvas>
<div class="grid">
<button class="pill primary" onclick="notes.save()">Save note</button>
<button class="pill" onclick="notes.clearDrawing()">Clear drawing</button>
<button class="pill" onclick="notes.load()">Refresh notes</button>
</div>
</div>
<div class="tool-surface">
<div class="section-title">Saved Notes</div>
<div class="hint">Notes are saved privately on this device. You can type or draw with a mouse/finger.</div>
<div class="secure-list" id="notesList"></div>
</div>
</div>
</div>
<div class="mode-panel" id="memoryPanel">
<div class="split">
<div class="tool-surface">
<div class="section-title">Save Memory</div>
<textarea class="field" id="memoryInput" placeholder="Example: I prefer short answers." rows="5"></textarea>
<div class="grid">
<button class="pill primary" onclick="ai.saveMemoryFromPanel()">Save memory</button>
<button class="pill" onclick="ai.loadMemoryPanel()">Refresh memory</button>
<button class="pill" onclick="ai.clearMemory()">Forget all</button>
</div>
<div class="hint">Black Dragon also learns simple personal facts when you clearly state them, such as your name or preferences.</div>
</div>
<div class="tool-surface">
<div class="section-title">Saved Memory</div>
<div class="secure-list" id="memoryList"></div>
</div>
</div>
</div>
</div>
</section>
<script>
const $ = (id) => document.getElementById(id);
const safeText = (s) => String(s || '').replace(/\s+/g, ' ').trim();
const BLACK_DRAGON_CONFIG = window.BLACK_DRAGON_CONFIG || {};
const HOSTED_BACKEND_FALLBACK = 'https://roky-dev-black-dragon-backend.hf.space';
const configuredBackendUrl = safeText(BLACK_DRAGON_CONFIG.backendUrl || window.BLACK_DRAGON_BACKEND_URL).replace(/\/+$/, '');
const LOCAL_BACKEND_HOST = /^(127\.0\.0\.1|localhost|0\.0\.0\.0|::1)$/i.test(location.hostname)
|| /^192\.168\./.test(location.hostname)
|| /^10\./.test(location.hostname)
|| /^172\.(1[6-9]|2\d|3[01])\./.test(location.hostname);
const LOCAL_FRONTEND = location.protocol === 'file:' || LOCAL_BACKEND_HOST;
const HOSTED_API_PREFIX = '/qk9m7r2v';
const API_PREFIX = (safeText(BLACK_DRAGON_CONFIG.apiPrefix) || (LOCAL_FRONTEND ? '/api' : HOSTED_API_PREFIX)).replace(/\/+$/, '') || '/api';
const SAME_ORIGIN_BACKEND = Boolean(BLACK_DRAGON_CONFIG.sameOriginBackend)
|| LOCAL_FRONTEND
|| /\.hf\.space$/i.test(location.hostname);
// Private assistant configuration is kept outside browser code. Hosted websites use same-origin backend calls.
const BACKEND_BASE = configuredBackendUrl
|| (location.protocol === 'file:' ? 'http://127.0.0.1:8787' : '')
|| (SAME_ORIGIN_BACKEND ? '' : HOSTED_BACKEND_FALLBACK);
const REQUEST_TIMEOUT_MS = Math.max(
15000,
Number(BLACK_DRAGON_CONFIG.requestTimeoutMs || (/\.hf\.space$/i.test(location.hostname) ? 150000 : 55000)) || 55000
);
const apiUrl = (path) => BACKEND_BASE + API_PREFIX + String(path || '').replace(/^\/api(?=\/|$)/, '').replace(/^(?!\/)/, '/');
const TERMS_VERSION = '2026-06-08-v1';
class BlackDragonAI {
constructor() {
this.chats = [];
this.messages = [];
this.attachments = [];
this.currentChatId = '';
this.profileName = this.currentProfileName();
this.userId = this.deviceUserId();
this.state = {
voice: false,
liveTalk: false,
mode: 'balanced',
memory: [],
conversationMemory: '',
learningFacts: [],
lastLearningTurn: 0,
processing: false,
sidebarVisible: true
};
this.identity = {
name: 'Black Dragon'
};
this.coreStatus = { checkedAt: 0, online: false };
this.lastCoreError = '';
this.load();
this.setupInput();
this.setupVoice();
if (!this.chats.length) this.newChat(false);
else this.switchChat(this.chats[0].id);
this.applyModeUi();
this.updateVoiceUi();
this.fitSidebar();
window.addEventListener('resize', () => this.fitSidebar());
this.showProfileGate();
this.updateProfileUi();
}
termsAccepted() {
return true;
}
authToken() {
const token = sessionStorage.getItem('blackDragonSessionToken') || localStorage.getItem('blackDragonSessionToken') || '';
const exp = Number(sessionStorage.getItem('blackDragonSessionExpiresAt') || localStorage.getItem('blackDragonSessionExpiresAt') || 0);
if (!token || (exp && Date.now() / 1000 > exp - 30)) return '';
return token;
}
hasAccess() {
return true;
}
authHeaders(extra = {}) {
const token = this.authToken();
return token ? { ...extra, Authorization: 'Bearer ' + token, 'X-Black-Dragon-Token': token } : { ...extra };
}
currentProfileName() {
return localStorage.getItem('blackDragonProfileName') || '';
}
stableId(text) {
let hash = 2166136261;
for (const ch of String(text || 'local')) {
hash ^= ch.charCodeAt(0);
hash = Math.imul(hash, 16777619);
}
return (hash >>> 0).toString(36);
}
profileId(name) {
return 'profile-' + this.stableId(String(name || 'local').toLowerCase().trim());
}
deviceUserId() {
const profile = this.currentProfileName();
if (profile) return this.profileId(profile);
let id = localStorage.getItem('blackDragonUserId');
if (!id) {
id = 'bd-' + Math.random().toString(36).slice(2) + Date.now().toString(36);
localStorage.setItem('blackDragonUserId', id);
}
return id;
}
showProfileGate(force = false) {
const overlay = $('profileOverlay');
if (!overlay) return;
const locked = force || !this.currentProfileName() || !this.hasAccess();
overlay.classList.toggle('show', locked);
document.body.classList.toggle('profile-locked', locked);
const input = $('profileNameInput');
if (input) input.value = this.currentProfileName();
if (locked) setTimeout(() => input?.focus(), 60);
}
async saveProfile() {
const input = $('profileNameInput');
const button = $('profileLoginButton');
const status = $('profileLoginStatus');
const setStatus = (message, good = false) => {
if (!status) return;
status.textContent = message || '';
status.style.color = good ? 'var(--primary)' : 'var(--warn)';
};
const name = safeText(input?.value || '').slice(0, 80);
if (!name) {
this.toast('Enter a display name first.');
setStatus('Enter a display name first.');
return;
}
if (button) {
button.disabled = true;
button.textContent = 'Opening...';
}
setStatus('Opening profile...');
try {
localStorage.setItem('blackDragonProfileName', name);
this.profileName = name;
this.userId = this.profileId(name);
$('profileOverlay')?.classList.remove('show');
document.body.classList.remove('profile-locked');
setStatus('Profile ready.', true);
this.updateProfileUi();
this.loadMemoryPanel();
$('userInput')?.focus();
} catch (error) {
const message = 'Could not save profile. Refresh and try again.';
this.toast(message);
setStatus(message);
} finally {
if (button) {
button.disabled = false;
button.textContent = 'Continue';
}
}
}
updateProfileUi() {
const label = this.currentProfileName() || 'Profile';
const button = $('profileButton');
if (button) button.textContent = this.hasAccess() && label ? (label.length > 10 ? label.slice(0, 9) + '.' : label) : 'Profile';
}
termsPayload() {
return {
termsAccepted: true,
termsVersion: TERMS_VERSION
};
}
requireTerms() {
if (this.hasAccess()) return true;
this.showProfileGate(true);
this.toast('Enter a display name first.');
return false;
}
setupInput() {
const input = $('userInput');
input.addEventListener('input', () => {
input.style.height = '46px';
input.style.height = Math.min(input.scrollHeight, 140) + 'px';
});
input.addEventListener('keydown', (event) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
this.send();
}
});
}
fitSidebar() {
if (innerWidth <= 860) {
$('sidebar').classList.add('hidden');
this.state.sidebarVisible = false;
} else {
$('sidebar').classList.remove('hidden');
this.state.sidebarVisible = true;
}
$('sidebarToggle').textContent = this.state.sidebarVisible ? 'x' : 'menu';
}
setupVoice() {
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SR) return;
this.recognition = new SR();
this.recognition.lang = 'en-US';
this.recognition.continuous = false;
this.recognition.interimResults = false;
this.recognition.onresult = (event) => {
const text = event.results[0][0].transcript;
$('userInput').value = text;
this.send();
};
this.recognition.onend = () => {
$('micButton').classList.remove('primary');
if (this.state.liveTalk) setTimeout(() => this.startRecording(), 350);
};
this.recognition.onerror = () => this.toast('Microphone could not hear clearly.');
}
newChat(greet = true) {
if (greet && !this.requireTerms()) return;
const chat = { id: String(Date.now()), title: 'New chat', preview: 'Ready', messages: [], timestamp: Date.now() };
this.chats.unshift(chat);
this.currentChatId = chat.id;
this.messages = chat.messages;
this.renderChatList();
$('chat').innerHTML = '';
this.renderWelcome();
if (greet) this.addMessage('Hello. Ask anything, search the web, open apps, attach files, show images, use live talk, or play games.', 'ai');
this.save();
}
renderWelcome() {
if (this.messages.length) return;
$('chat').innerHTML = `
<div class="welcome" id="welcome">
<button class="quick power" onclick="ai.toolPrompt('build')"><strong>Build feature</strong>Plan, code, verify</button>
<button class="quick power" onclick="ai.toolPrompt('review')"><strong>Review code</strong>Find bugs first</button>
<button class="quick" onclick="ai.quick('Search browser for latest AI news')"><strong>Browser search</strong>Find anything online</button>
<button class="quick" onclick="ai.quick('Show image of a black dragon')"><strong>Image lookup</strong>Get related pictures</button>
<button class="quick" onclick="ai.quick('Create a clear step-by-step production launch plan for Black Dragon AI, including frontend, backend, login, privacy, hosting, testing, security, mobile support, and release steps.')"><strong>Project plan</strong>Think like an agent</button>
<button class="quick" onclick="ai.openGameZone()"><strong>Gaming zone</strong>Play dragon games</button>
</div>`;
}
switchChat(id) {
const chat = this.chats.find((c) => c.id === id);
if (!chat) return;
this.currentChatId = id;
this.messages = chat.messages || [];
$('chat').innerHTML = '';
if (!this.messages.length) this.renderWelcome();
this.messages.forEach((m) => this.renderMessage(m));
this.renderChatList();
if (innerWidth <= 860 && this.state.sidebarVisible) this.toggleSidebar();
}
deleteChat(id, event) {
event.stopPropagation();
this.chats = this.chats.filter((c) => c.id !== id);
if (!this.chats.length) return this.newChat();
if (this.currentChatId === id) this.switchChat(this.chats[0].id);
this.renderChatList();
this.save();
}
renderChatList() {
$('chatList').innerHTML = this.chats.map((c) => `
<div class="chat-item ${c.id === this.currentChatId ? 'active' : ''}" onclick="ai.switchChat('${c.id}')">
<strong>${this.escape(c.title || 'Chat')}</strong>
<small>${this.escape(c.preview || '')}</small>
<span class="chat-delete" onclick="ai.deleteChat('${c.id}', event)">x</span>
</div>`).join('');
}
renderMessage(m) {
const row = document.createElement('div');
row.className = 'message ' + (m.sender === 'user' ? 'user' : 'ai');
row.innerHTML = `
<div class="avatar">${m.sender === 'user' ? 'You' : 'BD'}</div>
<div class="bubble"><div class="content"></div><div class="meta">${new Date(m.timestamp).toLocaleTimeString()}</div></div>`;
row.querySelector('.content').textContent = m.content;
$('chat').appendChild(row);
$('chat').scrollTop = $('chat').scrollHeight;
}
addMessage(content, sender) {
const m = { id: Date.now() + Math.random(), content, sender, timestamp: Date.now() };
this.messages.push(m);
const chat = this.chats.find((c) => c.id === this.currentChatId);
if (chat) {
chat.messages = this.messages;
chat.preview = safeText(content).slice(0, 46) || 'Ready';
if (sender === 'user' && chat.title === 'New chat') chat.title = safeText(content).slice(0, 28) || 'Chat';
chat.timestamp = Date.now();
}
this.renderMessage(m);
this.renderChatList();
this.save();
}
showTyping() {
const row = document.createElement('div');
row.className = 'message ai';
row.id = 'typing';
row.innerHTML = '<div class="avatar">BD</div><div class="bubble"><div class="typing"><span></span><span></span><span></span></div></div>';
$('chat').appendChild(row);
$('chat').scrollTop = $('chat').scrollHeight;
}
hideTyping() {
const t = $('typing');
if (t) t.remove();
}
async send() {
if (this.state.processing) return;
if (!this.requireTerms()) return;
const input = $('userInput');
const msg = input.value.trim();
const attachments = this.attachments.slice();
if (!msg && !attachments.length) return;
this.state.processing = true;
$('sendButton').disabled = true;
input.disabled = true;
input.value = '';
input.style.height = '46px';
this.clearAttachments(false);
const shownMessage = msg || 'Analyze the attached file';
const shownAttachments = attachments.length ? '\nAttached: ' + attachments.map((a) => a.name).join(', ') : '';
this.addMessage(shownMessage + shownAttachments, 'user');
this.showTyping();
let reply = '';
try {
reply = await this.answer(shownMessage, attachments);
} catch (error) {
reply = 'I hit a problem, but I am still here. Try again with a shorter command.';
}
this.hideTyping();
this.addMessage(reply, 'ai');
this.learnFromTurn(shownMessage, reply);
if (this.state.voice) this.speak(reply);
this.state.processing = false;
$('sendButton').disabled = false;
input.disabled = false;
input.focus();
}
async answer(raw, attachments = []) {
const msg = raw.trim();
const local = this.localAnswer(msg);
if (local) return local;
const command = await this.tryCommand(msg);
if (command) return command;
const aiText = await this.askCore(msg, attachments);
if (aiText) return this.cleanAnswer(aiText);
const online = await this.backendReady();
if (!online) return 'Black Dragon core is not running. Start it with start-backend.bat or BlackDragon-Chrome-App.bat, then refresh this page.';
if (this.lastCoreError === 'timeout') {
return 'Black Dragon answer engine is taking too long to answer. Ask once more after a few seconds.';
}
return 'Black Dragon did not return an answer. The backend is reachable, but the answer engine may be busy. Try once more or ask a shorter question.';
}
localAnswer(msg) {
const l = msg.toLowerCase();
if (/^(remember|save memory|remember that|memorize)\b/.test(l)) return null;
if (/(what do you remember|show memory|my memory|remember about me|forget memory|clear memory|delete memory|erase memory)/.test(l)) return null;
const memoryReply = this.memoryCommand(msg);
if (memoryReply) return memoryReply;
if (l === '/clear') { this.messages = []; $('chat').innerHTML = ''; this.renderWelcome(); this.save(); return 'Chat cleared.'; }
if (l === '/help') return 'Try: "open YouTube", "search browser for laptops", "show image of dragon city", "attach file", "summarize this", "remember that I like short answers", or "play flap dragon".';
return null;
}
async tryCommand(msg) {
const l = msg.toLowerCase();
if (/^(search|google|browse|browser search|web search|look up|find online)\b/.test(l) || /search (the )?(web|internet|browser) for/.test(l)) {
const q = msg
.replace(/^(search|google|browse|browser search|web search|look up|find online)\s*/i, '')
.replace(/search (the )?(web|internet|browser) for/i, '')
.trim();
return await this.webSearch(q || msg);
}
if (/^(open|launch)\b/.test(l)) {
const target = msg.replace(/^(open|launch)\s*/i, '').replace(/^(the\s+)?(app|website|site)\s+/i, '').trim();
if (/(notepad|calculator|calc|paint|file explorer|explorer|cmd|terminal|powershell|settings)/i.test(target)) {
return await this.runPcCommand('open ' + target, true);
}
return this.openAppOrSite(target);
}
if (/^(install|download)\b/.test(l)) {
const target = msg.replace(/^(install|download)\s*/i, '').trim();
this.openSearch((target || 'app') + ' official download');
return 'Opened a browser search for the official download. Check the publisher before installing.';
}
if (/\b(create|make|new|signup|sign up)\b.*\b(gmail|google account)\b/.test(l)) {
window.open('https://accounts.google.com/signup', '_blank', 'noopener,noreferrer');
return 'Opened the Google account signup page. Complete the personal details yourself for privacy and site compliance.';
}
if (/^(pc|computer)\b/.test(l)) {
return await this.runPcCommand(msg.replace(/^(pc|computer)\s*/i, ''), true);
}
if (/(cool|cooling|overheat|over heat|temperature|thermal).*(pc|computer|laptop)|^(cool|cooling|overheat)/.test(l)) {
return await this.runPcCommand(msg, true);
}
if (/^(scan|antivirus|virus scan)\b/.test(l)) {
return await this.runPcCommand(msg, true);
}
if (/^(start|enable|turn on)\s+live talk/.test(l)) {
if (!this.state.liveTalk) this.toggleLiveTalk();
return 'Live talk is on.';
}
if (/^(stop|disable|turn off)\s+live talk/.test(l)) {
if (this.state.liveTalk) this.toggleLiveTalk();
return 'Live talk is off.';
}
if (/^(stop speaking|stop voice|quiet)\b/.test(l)) {
this.stopSpeech();
return 'Voice stopped.';
}
if (/^(attach|upload|add)\s+(a\s+)?(file|image|photo|picture)/.test(l)) {
this.triggerFilePicker();
return 'Choose a file, then send your question with it.';
}
if (/^(copy|download)\s+(last\s+)?(answer|reply|response)/.test(l)) {
if (l.startsWith('copy')) this.copyLastAnswer();
else this.downloadLastAnswer();
return 'Done.';
}
if (/(generate|create|make|draw).*(image|photo|picture|art)|image generation/.test(l)) {
const q = msg.replace(/(generate|create|make|draw|image|photo|picture|art|of|for|related to)/gi, ' ').replace(/\s+/g, ' ').trim() || msg;
return this.generateImage(q);
}
if (/(show|find|get).*(image|photo|picture)|image of|photo of|picture of/.test(l)) {
const q = msg.replace(/(show|find|get|generate|create|image|photo|picture|of|for|related to)/gi, ' ').replace(/\s+/g, ' ').trim() || msg;
return await this.imageResult(q);
}
if (/(play|game|gaming).*(flap|flappy|race|car|space|shooter|snake|feed|catch|breaker|crystal|pong)|gaming zone/.test(l)) {
this.openGameZone();
if (l.includes('race') || l.includes('car')) games.start('race');
else if (l.includes('space') || l.includes('shooter')) games.start('space');
else if (l.includes('snake') || l.includes('feed')) games.start('snake');
else if (l.includes('catch')) games.start('catch');
else if (l.includes('breaker') || l.includes('crystal')) games.start('breaker');
else if (l.includes('pong')) games.start('pong');
else games.start('flap');
return 'Gaming Zone opened.';
}
return null;
}
async askCore(prompt, attachments = []) {
const backendText = await this.askBackend(prompt, attachments);
if (backendText) return backendText;
return null;
}
async backendReady() {
const now = Date.now();
if (now - this.coreStatus.checkedAt < 30000) return this.coreStatus.online;
try {
const res = await fetch(apiUrl('/health'), { cache: 'no-store', headers: this.authHeaders() });
this.coreStatus = { checkedAt: now, online: res.ok };
return res.ok;
} catch (error) {
this.coreStatus = { checkedAt: now, online: false };
return false;
}
}
async askBackend(prompt, attachments = []) {
if (!BACKEND_BASE && !SAME_ORIGIN_BACKEND) return null;
const controller = new AbortController();
this.lastCoreError = '';
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
try {
$('statusText').textContent = 'Thinking';
const profile = this.state.mode === 'coding' ? 'tech' : this.state.mode === 'research' ? 'research' : 'default';
const payload = {
prompt,
mode: this.state.mode,
profile,
userId: this.userId,
conversationMemory: this.backendConversationContext(),
...this.termsPayload(),
attachments: attachments.map((a) => ({
kind: a.kind,
name: a.name,
mime: a.mime,
text: a.text,
dataUrl: a.dataUrl
}))
};
const res = await fetch(apiUrl('/chat'), {
method: 'POST',
headers: this.authHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify(payload),
signal: controller.signal
});
$('statusText').textContent = 'Ready';
clearTimeout(timer);
const data = await res.json().catch(() => ({}));
if (!res.ok) return data.answer || null;
return data.answer || null;
} catch (error) {
clearTimeout(timer);
$('statusText').textContent = 'Ready';
this.lastCoreError = error?.name === 'AbortError' ? 'timeout' : 'network';
console.warn('Black Dragon core unavailable', error);
return null;
}
}
buildUserContent(prompt, attachments = []) {
const memory = this.memoryContext();
const textFiles = attachments
.filter((a) => a.kind === 'text')
.map((a) => '\n\nAttached file: ' + a.name + '\n' + a.text.slice(0, 18000))
.join('');
const text = [memory, (prompt || 'Analyze the attachment'), textFiles].filter(Boolean).join('\n\n');
const images = attachments.filter((a) => a.kind === 'image');
if (!images.length) return text;
return [
{ type: 'text', text },
...images.map((a) => ({ type: 'image_url', image_url: { url: a.dataUrl } }))
];
}
cleanAnswer(text) {
return text
.replace(/\b(?:gsk_|sk-or-v1-)[A-Za-z0-9_-]{12,}\b/g, '[redacted key]')
.replace(/\bBearer\s+[A-Za-z0-9._-]{12,}\b/gi, 'Bearer [redacted key]')
.replace(/\n{3,}/g, '\n\n')
.trim();
}
temperatureForMode() {
const temps = {
creative: 0.95,
precise: 0.25,
coding: 0.35,
study: 0.55,
research: 0.4,
balanced: 0.65
};
return temps[this.state.mode] ?? temps.balanced;
}
modeInstruction() {
const modes = {
creative: 'Use imaginative options and original phrasing while staying useful.',
precise: 'Prefer exact answers, assumptions, and short structured output.',
coding: 'Act like a senior coding assistant: reason from the task, produce implementation-ready code or edits, name edge cases, include tests or verification, and keep the answer direct.',
study: 'Teach clearly with examples, checks for understanding, and simple explanations.',
research: 'Synthesize carefully, compare possibilities, and say when a live browser search would help.',
balanced: 'Balance helpful detail with brevity.'
};
return modes[this.state.mode] || modes.balanced;
}
memoryContext() {
const memory = Array.isArray(this.state.memory) ? this.state.memory.slice(-20) : [];
const conversation = this.conversationContext();
const parts = [];
if (memory.length) parts.push('Remembered local user facts:\n' + memory.map((item, index) => `${index + 1}. ${item}`).join('\n'));
if (conversation) parts.push(conversation);
return parts.join('\n\n');
}
conversationContext() {
const summary = safeText(this.state.conversationMemory || '');
const facts = Array.isArray(this.state.learningFacts) ? this.state.learningFacts.slice(-12) : [];
const parts = [];
if (summary) parts.push('Conversation memory summary: ' + summary);
if (facts.length) parts.push('Learned from recent conversation:\n' + facts.map((fact) => '- ' + fact).join('\n'));
return parts.join('\n');
}
backendConversationContext() {
const context = this.conversationContext();
return context.length > 900 ? context.slice(-900) : context;
}
learnFromTurn(userText, aiText) {
const user = safeText(userText).slice(0, 260);
const ai = safeText(aiText).slice(0, 260);
if (!user) return;
const facts = Array.isArray(this.state.learningFacts) ? this.state.learningFacts : [];
const lower = user.toLowerCase();
const patterns = [
[/my name is\s+([^.!?]{2,60})/i, 'User name: {0}'],
[/call me\s+([^.!?]{2,60})/i, 'Preferred name: {0}'],
[/i (?:want|need|am building|am making)\s+([^.!?]{4,120})/i, 'User goal/project: {0}'],
[/i (?:like|prefer|love)\s+([^.!?]{3,100})/i, 'User preference: {0}'],
[/i (?:do not like|don't like|hate)\s+([^.!?]{3,100})/i, 'User dislike: {0}']
];
for (const [regex, template] of patterns) {
const match = user.match(regex);
if (match?.[1]) {
const fact = template.replace('{0}', match[1].trim());
if (!facts.some((x) => x.toLowerCase() === fact.toLowerCase())) facts.push(fact.slice(0, 180));
}
}
if (/black dragon|bd|app|backend|apk|website|memory|login|image|pc|game/.test(lower)) {
const note = `Recent topic: ${user}`;
if (!facts.some((x) => x === note)) facts.push(note.slice(0, 180));
}
this.state.learningFacts = facts.slice(-40);
const recent = this.messages.slice(-10).map((m) => `${m.sender === 'user' ? 'User' : 'BD'}: ${safeText(m.content).slice(0, 140)}`);
this.state.conversationMemory = recent.join(' | ').slice(-1400);
this.state.lastLearningTurn = Date.now();
this.save();
}
memoryCommand(msg) {
const l = msg.toLowerCase().trim();
if (/^(remember that|remember|save memory)\b/.test(l)) {
const fact = msg.replace(/^(remember that|remember|save memory)\s*/i, '').trim();
if (!fact) return 'Tell me what to remember.';
this.state.memory = Array.isArray(this.state.memory) ? this.state.memory : [];
this.state.memory.push(fact.slice(0, 240));
this.state.memory = this.state.memory.slice(-40);
this.save();
return 'I will remember that locally.';
}
if (/(what do you remember|show memory|my memory|conversation memory|what are we talking about)/.test(l)) return this.memorySummary();
if (/(forget memory|clear memory|delete memory)/.test(l)) {
this.state.memory = [];
this.state.conversationMemory = '';
this.state.learningFacts = [];
this.save();
return 'Memory cleared.';
}
return null;
}
memorySummary() {
const memory = Array.isArray(this.state.memory) ? this.state.memory : [];
const facts = Array.isArray(this.state.learningFacts) ? this.state.learningFacts : [];
const summary = safeText(this.state.conversationMemory || '');
if (!memory.length && !facts.length && !summary) return 'I do not have any local memory saved yet.';
return [
memory.length ? 'Saved memory:\n' + memory.map((item, index) => `${index + 1}. ${item}`).join('\n') : '',
facts.length ? 'Learned conversation facts:\n' + facts.map((item, index) => `${index + 1}. ${item}`).join('\n') : '',
summary ? 'Conversation summary:\n' + summary : ''
].filter(Boolean).join('\n\n');
}
calculate(input) {
const expr = input
.replace(/^(calculate|calc|solve|what is|what's)\s*/i, '')
.replace(/[×x]/gi, '*')
.replace(/[÷]/g, '/')
.trim();
if (!expr || !/^[\d\s+\-*/().%^]+$/.test(expr)) return null;
try {
const normalized = expr.replace(/\^/g, '**');
const value = Function('"use strict"; return (' + normalized + ')')();
if (!Number.isFinite(value)) return null;
return expr + ' = ' + Number(value.toFixed(10));
} catch (error) {
return null;
}
}
async liveKnowledge(query) {
const wiki = await this.searchWiki(query);
if (wiki) return wiki;
const ddg = await this.searchDdg(query);
if (ddg) return ddg;
return null;
}
async searchWiki(query) {
try {
let res = await fetch('https://en.wikipedia.org/api/rest_v1/page/summary/' + encodeURIComponent(query));
if (res.ok) {
const data = await res.json();
if (data.extract && !/may refer to/i.test(data.extract)) return data.extract.slice(0, 1100);
}
res = await fetch('https://en.wikipedia.org/w/api.php?action=query&list=search&format=json&origin=*&srsearch=' + encodeURIComponent(query));
const list = await res.json();
const title = list.query?.search?.[0]?.title;
if (!title) return null;
res = await fetch('https://en.wikipedia.org/api/rest_v1/page/summary/' + encodeURIComponent(title));
if (!res.ok) return null;
const data = await res.json();
return data.extract ? data.extract.slice(0, 1100) : null;
} catch (error) {
return null;
}
}
async searchDdg(query) {
try {
const res = await fetch('https://api.duckduckgo.com/?format=json&no_html=1&skip_disambig=1&q=' + encodeURIComponent(query));
if (!res.ok) return null;
const data = await res.json();
return data.Abstract || data.RelatedTopics?.find((x) => x.Text)?.Text || null;
} catch (error) {
return null;
}
}
async webSearch(query) {
const q = safeText(query || 'Black Dragon');
try {
const res = await fetch(apiUrl('/web/search') + '?q=' + encodeURIComponent(q), { cache: 'no-store', headers: this.authHeaders() });
const data = await res.json();
this.renderSearchBubble(q, data.results || [], data.searchUrl);
if ((data.results || []).length) return 'I found search results for: ' + q;
} catch (error) {
console.warn('Backend search failed', error);
}
this.openSearch(q);
return 'Opened browser search for: ' + q;
}
renderSearchBubble(query, results, searchUrl) {
const wrap = document.createElement('div');
wrap.className = 'message ai';
const items = (results || []).slice(0, 6).map((item) => `
<a class="search-result" href="${this.escape(item.url || searchUrl || '#')}" target="_blank" rel="noopener noreferrer">
<strong>${this.escape(item.title || 'Result')}</strong>
<span>${this.escape(item.snippet || item.url || '')}</span>
</a>`).join('');
wrap.innerHTML = `
<div class="avatar">BD</div>
<div class="bubble">
<div>Search results for "${this.escape(query)}"</div>
<div class="search-results">${items || '<div style="color:var(--muted);">No direct results loaded.</div>'}</div>
<div class="image-actions"><a href="${this.escape(searchUrl || ('https://duckduckgo.com/?q=' + encodeURIComponent(query)))}" target="_blank" rel="noopener noreferrer">Open full search</a></div>
<div class="meta">${new Date().toLocaleTimeString()}</div>
</div>`;
$('chat').appendChild(wrap);
$('chat').scrollTop = $('chat').scrollHeight;
}
async imageResult(query) {
const images = await this.fetchCommonsImages(query);
this.renderImageBubble(query, images);
if (images.length) return 'I found images for: ' + query;
return 'I opened image search because I could not load direct image results for: ' + query;
}
generateImage(query) {
const prompt = safeText(query || 'black dragon cinematic art');
const seed = Math.floor(Math.random() * 1000000);
const url = 'https://gen.pollinations.ai/image/' + encodeURIComponent(prompt) + '?width=768&height=512&seed=' + seed + '&nologo=true';
const wrap = document.createElement('div');
wrap.className = 'message ai';
wrap.innerHTML = `
<div class="avatar">BD</div>
<div class="bubble">
<div>Generated image: "${this.escape(prompt)}"</div>
<img class="generated-image" src="${this.escape(url)}" alt="${this.escape(prompt)}" loading="lazy" onerror="this.closest('.bubble').querySelector('.image-error').style.display='block'; this.remove();">
<div class="image-error" style="display:none;color:var(--muted);margin-top:8px;">Image generation is busy. Try again or open image search.</div>
<div class="image-actions"><a href="${this.escape(url)}" target="_blank" rel="noopener noreferrer">Open image</a><a href="https://www.bing.com/images/search?q=${encodeURIComponent(prompt)}" target="_blank" rel="noopener noreferrer">Search images</a></div>
<div class="meta">${new Date().toLocaleTimeString()}</div>
</div>`;
$('chat').appendChild(wrap);
$('chat').scrollTop = $('chat').scrollHeight;
return 'Generated an image preview for: ' + prompt;
}
async fetchCommonsImages(query) {
try {
const res = await fetch(apiUrl('/images/search') + '?q=' + encodeURIComponent(query), { cache: 'no-store', headers: this.authHeaders() });
if (res.ok) {
const data = await res.json();
if (Array.isArray(data.images) && data.images.length) return data.images;
}
} catch (error) {
console.warn('Backend image search failed', error);
}
const url = 'https://commons.wikimedia.org/w/api.php?' + new URLSearchParams({
action: 'query',
generator: 'search',
gsrsearch: query,
gsrnamespace: '6',
gsrlimit: '12',
prop: 'imageinfo',
iiprop: 'url|mime',
iiurlwidth: '640',
format: 'json',
origin: '*'
}).toString();
try {
const res = await fetch(url);
if (!res.ok) return [];
const data = await res.json();
return Object.values(data.query?.pages || {})
.map((page) => page.imageinfo?.[0])
.filter(Boolean)
.map((info) => ({
thumb: info.thumburl || info.url,
full: info.url,
mime: info.mime || ''
}))
.filter((img) => img.mime.startsWith('image/') || /\.(png|jpe?g|webp|gif)(\?|$)/i.test(img.full))
.slice(0, 4);
} catch (error) {
console.warn('Image search failed', error);
return [];
}
}
renderImageBubble(query, images) {
const wrap = document.createElement('div');
wrap.className = 'message ai';
const encoded = encodeURIComponent(query);
const imageSearchUrl = 'https://www.bing.com/images/search?q=' + encoded;
const grid = images.length
? `<div class="image-grid">${images.map((img) => `<a href="${img.full}" target="_blank" rel="noopener noreferrer"><img src="${img.thumb}" alt="${this.escape(query)}" loading="lazy" onerror="this.closest('a').remove()"></a>`).join('')}</div>`
: '<div style="color:var(--muted);margin-top:8px;">Direct image results did not load. Use the button below to open browser image search.</div>';
wrap.innerHTML = `
<div class="avatar">BD</div>
<div class="bubble">
<div>Images related to "${this.escape(query)}"</div>
${grid}
<div class="image-actions"><a href="${imageSearchUrl}" target="_blank" rel="noopener noreferrer">Open image search</a></div>
<div class="meta">${new Date().toLocaleTimeString()}</div>
</div>`;
$('chat').appendChild(wrap);
$('chat').scrollTop = $('chat').scrollHeight;
}
openSearch(query) {
const q = encodeURIComponent(query || 'Black Dragon');
window.open('https://duckduckgo.com/?q=' + q, '_blank', 'noopener,noreferrer');
}
openAppOrSite(target) {
const cleanTarget = target.replace(/^(the\s+)?(app|website|site)\s+/i, '').replace(/\s+app$/i, '').trim();
const key = cleanTarget.toLowerCase();
const apps = {
youtube: 'https://www.youtube.com',
google: 'https://www.google.com',
gmail: 'https://mail.google.com',
'gmail signup': 'https://accounts.google.com/signup',
'google account': 'https://accounts.google.com/signup',
maps: 'https://maps.google.com',
drive: 'https://drive.google.com',
docs: 'https://docs.google.com',
sheets: 'https://sheets.google.com',
slides: 'https://slides.google.com',
instagram: 'https://www.instagram.com',
facebook: 'https://www.facebook.com',
whatsapp: 'https://web.whatsapp.com',
x: 'https://x.com',
twitter: 'https://x.com',
linkedin: 'https://www.linkedin.com',
reddit: 'https://www.reddit.com',
netflix: 'https://www.netflix.com',
prime: 'https://www.primevideo.com',
hotstar: 'https://www.hotstar.com',
disney: 'https://www.disneyplus.com',
spotify: 'spotify:',
discord: 'discord:',
telegram: 'tg://resolve',
slack: 'slack:',
teams: 'msteams:',
zoom: 'zoommtg:',
calculator: 'ms-calculator:',
settings: 'ms-settings:',
camera: 'microsoft.windows.camera:',
store: 'ms-windows-store:',
edge: 'microsoft-edge:https://www.bing.com',
word: 'ms-word:',
excel: 'ms-excel:',
powerpoint: 'ms-powerpoint:',
onenote: 'onenote:',
mail: 'mailto:',
calendar: 'https://calendar.google.com'
};
let url = apps[key] || apps[Object.keys(apps).find((k) => key.includes(k))];
if (!url && /^https?:\/\//i.test(cleanTarget)) url = cleanTarget;
if (!url && /^[\w.-]+\.[a-z]{2,}/i.test(cleanTarget)) url = 'https://' + cleanTarget;
if (url) {
window.open(url, '_blank');
return 'Opening ' + cleanTarget + '.';
}
this.openSearch(cleanTarget);
return 'I opened a browser search for "' + cleanTarget + '". If it is a desktop-only app, the browser may ask for permission or show install/open options.';
}
browserSearch() {
const q = prompt('Search for what?');
if (q) this.openSearch(q);
}
async imagePrompt() {
const q = prompt('Show image of what?');
if (q) this.addMessage(await this.imageResult(q), 'ai');
}
openAppPrompt() {
const q = prompt('Open which app or website?');
if (q) this.addMessage(this.openAppOrSite(q), 'ai');
}
toolPrompt(kind) {
const prompts = {
summarize: 'Summarize this clearly with key points and action items: ',
rewrite: 'Rewrite this to be clearer and more polished: ',
translate: 'Translate this to Hindi and keep the meaning natural: ',
code: 'Help me write code for this. Include the best approach and code: ',
debug: 'Debug this problem. Explain the likely cause and fix: ',
plan: 'Create a practical step-by-step plan for: ',
study: 'Teach me this topic simply, then quiz me: ',
compare: 'Compare these options with pros, cons, and the best choice: ',
table: 'Make a clear table for this information: ',
email: 'Write a professional email for this situation: ',
remember: 'Remember that ',
build: 'Act as a senior software builder. Turn this request into a production-ready implementation with: goal, files to change, exact code, edge cases, tests, and verification steps: ',
review: 'Review this like a senior engineer. Put bugs, security risks, regressions, and missing tests first. Then give a short fix plan: ',
fix: 'Find the root cause of this bug and give a minimal safe fix, test cases, and commands or steps to verify it: ',
tests: 'Write focused tests for this feature or bug. Cover success, failure, edge cases, and how to run them: ',
refactor: 'Refactor this safely. Preserve behavior, reduce complexity, explain the tradeoffs, and show the final code: ',
spec: 'Write a clear product spec with user goal, flows, screens, data, edge cases, security, metrics, and launch checklist: '
};
if (['build', 'review', 'fix', 'tests', 'refactor', 'spec', 'code', 'debug'].includes(kind)) {
this.state.mode = 'coding';
this.applyModeUi();
this.save();
}
$('userInput').value = prompts[kind] || '';
$('userInput').focus();
this.toggleMenu(false);
}
showMemory() {
if (!this.requireTerms()) return;
this.addMessage(this.memorySummary(), 'ai');
this.toggleMenu(false);
}
async loadMemoryPanel() {
if (!this.requireTerms()) return;
const list = $('memoryList');
if (!list) return;
const items = Array.isArray(this.state.memory) ? this.state.memory : [];
list.innerHTML = items.length
? items.map((item, index) => `<div class="secure-msg"><strong>Memory ${index + 1}</strong><div>${this.escape(item)}</div></div>`).join('')
: '<div class="hint">No saved memory yet.</div>';
}
async saveMemoryFromPanel() {
if (!this.requireTerms()) return;
const input = $('memoryInput');
const text = input?.value.trim() || '';
if (!text) return this.toast('Write a memory first.');
this.state.memory = Array.isArray(this.state.memory) ? this.state.memory : [];
this.state.memory.push(text.slice(0, 240));
this.state.memory = this.state.memory.slice(-40);
this.save();
this.toast('Memory saved.');
input.value = '';
await this.loadMemoryPanel();
}
async showSystemStatus() {
try {
const res = await fetch(apiUrl('/monitor'), { cache: 'no-store', headers: this.authHeaders() });
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const health = await fetch(apiUrl('/health'), { cache: 'no-store', headers: this.authHeaders() });
const h = await health.json().catch(() => ({}));
this.addMessage(health.ok ? `Black Dragon is ready.\nAuth required: ${h.authRequired ? 'yes' : 'no'}\nBuild: ${h.build || 'current'}` : 'Black Dragon needs a restart.', 'ai');
return;
}
const latency = data.latency ? JSON.stringify(data.latency, null, 2) : 'No latency samples yet.';
const rate = data.rateLimit ? `${data.rateLimit.requests} requests / ${data.rateLimit.windowSeconds}s` : 'active';
this.addMessage(`Black Dragon monitor\nBuild: ${data.build || 'current'}\nRate limit: ${rate}\nChats: ${data.chats || 0}\nMemories: ${data.memories || 0}\nQueue: ${JSON.stringify(data.queue || {})}\nLatency:\n${latency}`, 'ai');
} catch (error) {
this.addMessage('Black Dragon core is not running. Start Black Dragon from the launcher.', 'ai');
}
this.toggleMenu(false);
}
policyUrl(kind) {
const path = kind === 'privacy' ? '/privacy' : '/terms';
if (BACKEND_BASE) return BACKEND_BASE + path;
if (SAME_ORIGIN_BACKEND) return path;
return HOSTED_BACKEND_FALLBACK + path;
}
openPolicy(kind) {
window.open(this.policyUrl(kind), '_blank', 'noopener,noreferrer');
}
clearMemory() {
if (!confirm('Clear Black Dragon local memory?')) return;
this.state.memory = [];
this.state.conversationMemory = '';
this.state.learningFacts = [];
this.save();
this.addMessage('Memory cleared.', 'ai');
this.loadMemoryPanel();
this.toggleMenu(false);
}
triggerFilePicker() {
const picker = $('fileInput');
if (picker) picker.click();
this.toggleMenu(false);
}
async handleFiles(fileList) {
const files = Array.from(fileList || []);
if (!files.length) return;
for (const file of files) {
try {
this.toast('Scanning ' + file.name + '...');
const scanned = await this.scanFileBeforeAccept(file);
if (scanned?.accepted && scanned.file) {
this.attachments.push(scanned.file);
this.toast(file.name + ' scanned and accepted.');
} else {
this.toast(scanned?.message || (file.name + ' was blocked by scan.'));
}
} catch (error) {
this.toast('Could not scan ' + file.name + '.');
}
}
this.renderAttachments();
}
isTextFile(file) {
return file.type.startsWith('text/') || /\.(txt|md|json|csv|html|css|js|ts|jsx|tsx|py|java|c|cpp|cs|go|rs|rb|php|xml|yaml|yml|log)$/i.test(file.name);
}
async scanFileBeforeAccept(file) {
if (!this.requireTerms()) return null;
if (file.size > 25 * 1024 * 1024) {
return { accepted: false, message: file.name + ' is too large. Use files up to 25 MB.' };
}
const dataBase64 = await this.readFile(file, 'base64');
return this.backendPost('/api/file/scan', {
name: file.name,
mime: file.type || 'application/octet-stream',
size: file.size,
dataBase64
});
}
readFile(file, mode) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
if (mode === 'base64') {
const bytes = new Uint8Array(reader.result || new ArrayBuffer(0));
let binary = '';
const chunkSize = 32768;
for (let i = 0; i < bytes.length; i += chunkSize) {
binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
}
resolve(btoa(binary));
return;
}
resolve(String(reader.result || ''));
};
reader.onerror = reject;
if (mode === 'dataURL') reader.readAsDataURL(file);
else if (mode === 'base64') reader.readAsArrayBuffer(file);
else reader.readAsText(file);
});
}
renderAttachments() {
const strip = $('attachmentStrip');
if (!strip) return;
strip.classList.toggle('show', this.attachments.length > 0);
strip.innerHTML = this.attachments.map((a, index) => `
<span class="attachment-chip" title="${this.escape(a.name)}">
<span>${a.kind === 'image' ? 'Image' : a.kind === 'text' ? 'Text' : 'Scanned file'}</span>
<strong>${this.escape(a.name)}</strong>
<button onclick="ai.removeAttachment(${index})" title="Remove attachment">x</button>
</span>`).join('');
}
removeAttachment(index) {
this.attachments.splice(index, 1);
this.renderAttachments();
}
clearAttachments(render = true) {
this.attachments = [];
if (render) this.renderAttachments();
}
lastAiText() {
return [...this.messages].reverse().find((m) => m.sender === 'ai')?.content || '';
}
copyLastAnswer() {
const text = this.lastAiText();
if (!text) return this.toast('No answer to copy yet.');
navigator.clipboard?.writeText(text)
.then(() => this.toast('Answer copied.'))
.catch(() => this.toast('Clipboard permission was blocked.'));
this.toggleMenu(false);
}
downloadLastAnswer() {
const text = this.lastAiText();
if (!text) return this.toast('No answer to download yet.');
const blob = new Blob([text], { type: 'text/plain' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'black-dragon-answer.txt';
a.click();
URL.revokeObjectURL(a.href);
this.toggleMenu(false);
}
quick(text) {
$('userInput').value = text;
this.send();
}
toggleMenu(force) {
if (!this.requireTerms()) return;
const menu = $('toolMenu');
const show = typeof force === 'boolean' ? force : !menu.classList.contains('show');
menu.classList.toggle('show', show);
}
toggleSidebar() {
if (!this.requireTerms()) return;
this.state.sidebarVisible = !$('sidebar').classList.contains('hidden');
this.state.sidebarVisible = !this.state.sidebarVisible;
$('sidebar').classList.toggle('hidden', !this.state.sidebarVisible);
$('sidebarToggle').textContent = this.state.sidebarVisible ? 'x' : 'menu';
}
setMode(mode) {
this.state.mode = mode;
this.applyModeUi();
this.save();
this.toast('Mode: ' + mode);
}
applyModeUi() {
document.querySelectorAll('[data-mode]').forEach((b) => b.classList.toggle('active', b.dataset.mode === this.state.mode));
}
toggleVoice() {
this.state.voice = !this.state.voice;
this.updateVoiceUi();
this.save();
}
updateVoiceUi() {
$('voiceBtn').textContent = 'Voice out: ' + (this.state.voice ? 'On' : 'Off');
$('voiceBtn').classList.toggle('active', this.state.voice);
$('liveBtn').textContent = 'Live talk: ' + (this.state.liveTalk ? 'On' : 'Off');
$('liveBtn').classList.toggle('active', this.state.liveTalk);
}
speak(text) {
if (!window.speechSynthesis) return;
speechSynthesis.cancel();
const u = new SpeechSynthesisUtterance(text.replace(/[^\w\s.,!?'-]/g, ' '));
u.rate = 1;
u.pitch = .92;
speechSynthesis.speak(u);
}
stopSpeech() {
if (window.speechSynthesis) speechSynthesis.cancel();
}
toggleRecording() {
if (!this.requireTerms()) return;
if (!this.recognition) return this.toast('Voice input is not supported in this browser.');
this.startRecording();
}
startRecording() {
try {
this.recognition.start();
$('micButton').classList.add('primary');
} catch (error) {}
}
toggleLiveTalk() {
this.state.liveTalk = !this.state.liveTalk;
this.updateVoiceUi();
this.save();
if (this.state.liveTalk) this.startRecording();
}
testMic() {
if (!navigator.mediaDevices) return this.toast('Microphone test is not supported here.');
navigator.mediaDevices.getUserMedia({ audio: true })
.then((stream) => {
stream.getTracks().forEach((t) => t.stop());
this.toast('Microphone is working.');
})
.catch(() => this.toast('Microphone permission was blocked.'));
}
openGameZone() {
if (!this.requireTerms()) return;
$('gameZone').classList.add('show');
games.start(games.type || 'flap');
}
closeGameZone() {
$('gameZone').classList.remove('show');
}
openMode(mode = 'pc') {
if (!this.requireTerms()) return;
$('modeZone').classList.add('show');
document.querySelectorAll('[data-mode-tab]').forEach((b) => b.classList.toggle('primary', b.dataset.modeTab === mode));
document.querySelectorAll('.mode-panel').forEach((p) => p.classList.remove('active'));
const panel = $(mode + 'Panel');
if (panel) panel.classList.add('active');
if (mode === 'draw') setTimeout(() => airDraw.resize(), 60);
if (mode === 'notes') setTimeout(() => { notes.resize(); notes.load(); }, 60);
if (mode === 'memory') setTimeout(() => this.loadMemoryPanel(), 60);
this.toggleMenu(false);
}
closeMode() {
$('modeZone').classList.remove('show');
}
async backendPost(path, payload = {}, requireAgreement = true) {
if (requireAgreement && !this.requireTerms()) return { ok: false, message: 'Please agree to the Terms and Privacy Policy first.' };
const res = await fetch(apiUrl(path), {
method: 'POST',
headers: this.authHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify({ ...payload, ...this.termsPayload() })
});
return res.json();
}
async pcStatus() {
if (!this.requireTerms()) return;
this.openMode('pc');
try {
const res = await fetch(apiUrl('/pc/status'), { headers: this.authHeaders() });
const data = await res.json();
$('pcOutput').textContent = JSON.stringify(data.status || data, null, 2);
} catch (error) {
$('pcOutput').textContent = 'Backend is not running. Start it with start-backend.bat.';
}
}
async runPcCommand(command, silent = false) {
if (!this.requireTerms()) return 'Please agree to the Terms and Privacy Policy first.';
const value = command || $('pcCommand')?.value || '';
if (!value.trim()) return 'Type a PC command first.';
if (!silent) this.openMode('pc');
try {
const data = await this.backendPost('/api/pc/command', { command: value });
const text = data.message || JSON.stringify(data.data || data, null, 2);
if ($('pcOutput')) $('pcOutput').textContent = text;
return text;
} catch (error) {
const text = 'Backend is not running. Start it with start-backend.bat.';
if ($('pcOutput')) $('pcOutput').textContent = text;
return text;
}
}
async scanPc(target) {
if (!this.requireTerms()) return;
this.openMode('pc');
const value = target || $('scanTarget')?.value || 'quick';
$('pcOutput').textContent = 'Starting scan...';
try {
const data = await this.backendPost('/api/antivirus/scan', { target: value });
const status = data.defenderStatus ? '\n\nDefender status:\n' + JSON.stringify(data.defenderStatus, null, 2) : '';
$('pcOutput').textContent = data.message + status + (data.findings ? '\n\n' + JSON.stringify(data.findings, null, 2) : '');
} catch (error) {
$('pcOutput').textContent = 'Scan failed. Make sure the backend is running.';
}
}
async coolingMode(applyBalanced = false) {
if (!this.requireTerms()) return;
this.openMode('pc');
$('pcOutput').textContent = applyBalanced ? 'Applying safe cooling mode...' : 'Checking PC cooling status...';
try {
const data = await this.backendPost('/api/pc/cooling', { applyBalanced });
const top = (data.topProcesses || []).map((p) => `${p.name} PID:${p.pid} CPU:${p.cpuSeconds}s RAM:${p.memoryMb}MB`).join('\n');
const recs = (data.recommendations || []).map((x) => '- ' + x).join('\n');
$('pcOutput').textContent = `${data.message || 'Cooling check complete.'}\n\nPower plan: ${data.activePowerPlan || 'Unknown'}\nBalanced applied: ${data.appliedBalancedPower ? 'yes' : 'no'}\n\nTop CPU/RAM processes:\n${top || 'No process data available.'}\n\nRecommendations:\n${recs}`;
} catch (error) {
$('pcOutput').textContent = 'Cooling check failed. Make sure the backend is running.';
}
}
async scanCleanup() {
if (!this.requireTerms()) return;
this.openMode('pc');
$('pcOutput').textContent = 'Scanning safe cleanup candidates...';
try {
const data = await this.backendPost('/api/pc/cleanup/scan', {});
const sample = (data.candidates || []).slice(0, 20).map((item) => `${item.sizeMb} MB ${item.path}`).join('\n');
$('pcOutput').textContent = `${data.message}\n\nTop files:\n${sample || 'No cleanup candidates found.'}`;
} catch (error) {
$('pcOutput').textContent = 'Cleanup scan failed. Make sure the backend is running.';
}
}
async deleteCleanup() {
if (!this.requireTerms()) return;
this.openMode('pc');
if (!confirm('Delete only safe temporary/cache cleanup files found by Black Dragon?')) return;
$('pcOutput').textContent = 'Deleting safe temp/cache files...';
try {
const data = await this.backendPost('/api/pc/cleanup/delete', { confirm: true });
$('pcOutput').textContent = data.message + (data.errors?.length ? '\n\nSkipped:\n' + data.errors.join('\n') : '');
} catch (error) {
$('pcOutput').textContent = 'Cleanup delete failed. Make sure the backend is running.';
}
}
exportChats() {
const blob = new Blob([JSON.stringify(this.chats, null, 2)], { type: 'application/json' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'black-dragon-chats.json';
a.click();
URL.revokeObjectURL(a.href);
}
resetAll() {
if (!confirm('Delete chats and settings?')) return;
localStorage.removeItem('blackDragonData');
location.reload();
}
save() {
try {
const savedState = {
voice: this.state.voice,
liveTalk: this.state.liveTalk,
mode: this.state.mode,
memory: Array.isArray(this.state.memory) ? this.state.memory.slice(-40) : [],
conversationMemory: safeText(this.state.conversationMemory || '').slice(-1400),
learningFacts: Array.isArray(this.state.learningFacts) ? this.state.learningFacts.slice(-40) : [],
lastLearningTurn: this.state.lastLearningTurn || 0
};
localStorage.setItem('blackDragonData', JSON.stringify({
chats: this.chats.map((c) => ({ ...c, messages: (c.messages || []).slice(-140) })),
state: savedState
}));
} catch (error) {}
}
load() {
try {
const data = JSON.parse(localStorage.getItem('blackDragonData'));
if (!data) return;
this.chats = data.chats || [];
this.state = { ...this.state, ...(data.state || {}) };
this.state.memory = Array.isArray(this.state.memory) ? this.state.memory : [];
this.state.learningFacts = Array.isArray(this.state.learningFacts) ? this.state.learningFacts : [];
this.state.conversationMemory = safeText(this.state.conversationMemory || '').slice(-1400);
this.state.processing = false;
} catch (error) {}
}
toast(text) {
const n = document.createElement('div');
n.className = 'notice';
n.textContent = text;
document.body.appendChild(n);
setTimeout(() => n.remove(), 2300);
}
escape(s) {
return String(s || '').replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
}
}
class AirDraw3D {
constructor() {
this.drawCanvas = $('drawCanvas');
this.viewCanvas = $('viewCanvas');
this.strokes = [];
this.current = null;
this.lastStrokes = [];
this.model = null;
this.scene = null;
this.angle = 0;
this.raf = 0;
this.convertTimer = 0;
if (!this.drawCanvas || !this.viewCanvas) return;
this.drawCtx = this.drawCanvas.getContext('2d');
this.viewCtx = this.viewCanvas.getContext('2d');
this.bind();
this.clear();
}
bind() {
this.drawCanvas.addEventListener('pointerdown', (event) => {
this.current = [this.point(event)];
this.drawCanvas.setPointerCapture(event.pointerId);
});
this.drawCanvas.addEventListener('pointermove', (event) => {
if (!this.current) return;
this.current.push(this.point(event));
this.render2d();
});
this.drawCanvas.addEventListener('pointerup', () => {
if (this.current?.length) this.strokes.push(this.current);
this.current = null;
this.convert();
});
this.drawCanvas.addEventListener('pointercancel', () => {
if (this.current?.length) this.strokes.push(this.current);
this.current = null;
this.convert();
});
this.drawCanvas.addEventListener('pointerleave', () => {
if (!this.current) return;
this.strokes.push(this.current);
this.current = null;
this.convert();
});
}
point(event) {
const rect = this.drawCanvas.getBoundingClientRect();
return {
x: (event.clientX - rect.left) * (this.drawCanvas.width / rect.width),
y: (event.clientY - rect.top) * (this.drawCanvas.height / rect.height)
};
}
clear() {
cancelAnimationFrame(this.raf);
clearTimeout(this.convertTimer);
this.strokes = [];
this.current = null;
this.lastStrokes = [];
this.model = null;
this.scene = null;
if ($('airOutput')) $('airOutput').textContent = 'Air Canvas ready. Draw, upload an image, or describe an environment.';
this.render2d();
this.render3d([]);
}
resize() {
this.render2d();
this.convert();
}
render2d() {
const c = this.drawCtx;
c.clearRect(0, 0, this.drawCanvas.width, this.drawCanvas.height);
c.fillStyle = '#050b0d';
c.fillRect(0, 0, this.drawCanvas.width, this.drawCanvas.height);
const glow = c.createRadialGradient(this.drawCanvas.width * .5, this.drawCanvas.height * .42, 10, this.drawCanvas.width * .5, this.drawCanvas.height * .42, this.drawCanvas.width * .72);
glow.addColorStop(0, 'rgba(53,246,163,.10)');
glow.addColorStop(1, 'rgba(0,0,0,0)');
c.fillStyle = glow;
c.fillRect(0, 0, this.drawCanvas.width, this.drawCanvas.height);
c.strokeStyle = 'rgba(255,255,255,.06)';
c.lineWidth = 1;
for (let x = 0; x < this.drawCanvas.width; x += 36) {
c.beginPath(); c.moveTo(x, 0); c.lineTo(x, this.drawCanvas.height); c.stroke();
}
for (let y = 0; y < this.drawCanvas.height; y += 36) {
c.beginPath(); c.moveTo(0, y); c.lineTo(this.drawCanvas.width, y); c.stroke();
}
c.shadowColor = 'rgba(53,246,163,.75)';
c.shadowBlur = 10;
c.strokeStyle = '#35f6a3';
c.lineWidth = 6;
c.lineCap = 'round';
c.lineJoin = 'round';
[...this.strokes, this.current].filter(Boolean).forEach((stroke) => this.path(c, stroke));
c.shadowBlur = 0;
}
convert() {
const strokes = [...this.strokes, this.current].filter(Boolean);
this.lastStrokes = strokes;
const prompt = $('airPrompt')?.value || '';
if (strokes.length) {
this.model = null;
this.scene = null;
cancelAnimationFrame(this.raf);
this.animate3d();
}
clearTimeout(this.convertTimer);
if (strokes.length && window.ai?.termsAccepted?.()) {
this.convertTimer = setTimeout(() => this.enhanceStrokes(strokes, prompt), 220);
}
if (!strokes.length) this.render3d([]);
}
async enhanceStrokes(strokes, prompt) {
try {
if ($('airOutput')) $('airOutput').textContent = 'Enhancing 2D drawing into a 3D hologram...';
const data = await ai.backendPost('/api/air/shape-to-3d', { strokes, prompt });
if (data.ok && data.model) {
this.model = data.model;
this.scene = null;
if ($('airOutput')) $('airOutput').textContent = `3D model ready: ${data.model.recognized}. Objects: ${data.model.objects?.length || 0}.`;
}
} catch (error) {
if ($('airOutput')) $('airOutput').textContent = 'Live local extrusion is active. Backend enhancement was skipped.';
}
}
pickImage() {
$('airImageInput')?.click();
}
async imageTo3d(file) {
if (!file) return;
if (!window.ai?.requireTerms?.()) return;
const dataUrl = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(file);
});
try {
if ($('airOutput')) $('airOutput').textContent = 'Creating image relief 3D blockout...';
const data = await ai.backendPost('/api/air/image-to-3d', {
name: file.name,
mime: file.type || 'image/png',
dataUrl,
prompt: $('airPrompt')?.value || file.name
});
if (data.ok && data.model) {
this.model = data.model;
this.scene = null;
this.lastStrokes = [];
cancelAnimationFrame(this.raf);
this.animate3d();
if ($('airOutput')) $('airOutput').textContent = `Image model ready: ${data.model.recognized}.`;
}
} catch (error) {
if ($('airOutput')) $('airOutput').textContent = 'Image to 3D failed. Check backend.';
}
}
async makeEnvironment() {
if (!window.ai?.requireTerms?.()) return;
const description = $('airPrompt')?.value || 'creative test environment';
try {
if ($('airOutput')) $('airOutput').textContent = 'Building 3D environment...';
const data = await ai.backendPost('/api/air/environment', { description, model: this.model });
if (data.ok && data.scene) {
this.scene = data.scene;
cancelAnimationFrame(this.raf);
this.animate3d();
if ($('airOutput')) $('airOutput').textContent = `Environment ready: ${data.scene.type}. Objects: ${data.scene.objects?.length || 0}.`;
}
} catch (error) {
if ($('airOutput')) $('airOutput').textContent = 'Environment build failed. Check backend.';
}
}
exportObj() {
const obj = this.model?.obj;
if (!obj) return ai.toast('Create a 3D model first.');
const blob = new Blob([obj], { type: 'text/plain' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'black-dragon-air-model.obj';
a.click();
URL.revokeObjectURL(a.href);
}
animate3d() {
this.angle += 0.018;
this.render3d(this.lastStrokes);
if (this.lastStrokes.length || this.model || this.scene) this.raf = requestAnimationFrame(() => this.animate3d());
}
render3d(strokes) {
const c = this.viewCtx;
c.clearRect(0, 0, this.viewCanvas.width, this.viewCanvas.height);
const w = this.viewCanvas.width;
const h = this.viewCanvas.height;
const bg = c.createLinearGradient(0, 0, w, h);
bg.addColorStop(0, '#020707');
bg.addColorStop(.45, '#071912');
bg.addColorStop(1, '#05070d');
c.fillStyle = bg;
c.fillRect(0, 0, w, h);
c.fillStyle = 'rgba(53,246,163,.035)';
for (let y = 0; y < h; y += 7) c.fillRect(0, y, w, 1);
c.save();
c.translate(w / 2, h * .62);
c.strokeStyle = 'rgba(53,246,163,.14)';
c.lineWidth = 1;
for (let r = 90; r <= 420; r += 55) {
c.beginPath();
c.ellipse(0, 0, r, r * .28, 0, 0, Math.PI * 2);
c.stroke();
}
for (let a = 0; a < Math.PI * 2; a += Math.PI / 12) {
c.beginPath();
c.moveTo(Math.cos(a) * 55, Math.sin(a) * 18);
c.lineTo(Math.cos(a) * 430, Math.sin(a) * 122);
c.stroke();
}
c.restore();
if (this.scene || this.model) {
this.renderSceneModel(c, w, h);
return;
}
if (!strokes.length) {
this.renderDemoHologram(c, w, h);
c.fillStyle = '#9aabb0';
c.font = '18px system-ui';
c.fillText('Draw, upload image, or build an environment.', 30, 48);
return;
}
const depth = 78;
const pulse = .55 + Math.sin(this.angle * 4) * .18;
const yaw = Math.sin(this.angle) * 32;
const cx = w / 2;
const cy = h / 2;
const project = (p, d) => {
const sx = p.x - this.drawCanvas.width / 2;
const sy = p.y - this.drawCanvas.height / 2;
const z = d / depth;
return {
x: cx + sx * (0.82 + z * .18) + yaw * z,
y: cy + sy * .82 - d * .55 + Math.cos(this.angle + z) * 5,
};
};
c.shadowColor = 'rgba(53,246,163,.9)';
c.shadowBlur = 18;
for (let d = depth; d >= 0; d -= 8) {
c.strokeStyle = d ? `rgba(143,124,255,${0.14 + d / 350})` : `rgba(53,246,163,${pulse})`;
c.lineWidth = d ? 4 : 8;
c.lineCap = 'round';
c.lineJoin = 'round';
strokes.forEach((stroke) => this.path(c, stroke.map((p) => project(p, d))));
}
c.shadowBlur = 0;
c.strokeStyle = 'rgba(255,209,102,.9)';
c.lineWidth = 2;
strokes.forEach((stroke) => {
stroke.filter((_, i) => i % 8 === 0).forEach((p) => {
const a = project(p, 0);
const b = project(p, depth);
c.beginPath();
c.moveTo(a.x, a.y);
c.lineTo(b.x, b.y);
c.stroke();
});
});
c.fillStyle = 'rgba(53,246,163,.92)';
c.font = '12px system-ui';
c.fillText('AIR CANVAS 3D', 28, 28);
}
renderDemoHologram(c, w, h) {
const cx = w / 2;
const cy = h * .56;
const t = this.angle || .8;
const points = [
[-90, -70, -70], [90, -70, -70], [90, 70, -70], [-90, 70, -70],
[-90, -70, 70], [90, -70, 70], [90, 70, 70], [-90, 70, 70]
].map(([x, y, z]) => {
const rx = x * Math.cos(t) - z * Math.sin(t);
const rz = x * Math.sin(t) + z * Math.cos(t);
return { x: cx + rx + rz * .22, y: cy + y - rz * .18 };
});
const edges = [[0,1],[1,2],[2,3],[3,0],[4,5],[5,6],[6,7],[7,4],[0,4],[1,5],[2,6],[3,7]];
c.save();
c.shadowBlur = 18;
edges.forEach(([a, b], i) => {
c.strokeStyle = i % 3 === 0 ? '#8f7cff' : i % 3 === 1 ? '#35f6a3' : '#ffd166';
c.shadowColor = c.strokeStyle;
c.lineWidth = 3;
c.beginPath();
c.moveTo(points[a].x, points[a].y);
c.lineTo(points[b].x, points[b].y);
c.stroke();
});
c.fillStyle = 'rgba(53,246,163,.08)';
c.beginPath();
points.slice(4).forEach((p, i) => i ? c.lineTo(p.x, p.y) : c.moveTo(p.x, p.y));
c.closePath();
c.fill();
c.restore();
}
renderSceneModel(c, w, h) {
const objects = this.scene?.objects || this.model?.objects || [];
const cx = w / 2;
const cy = h * .64;
const yaw = Math.sin(this.angle) * .55;
const project = (x = 0, y = 0, z = 0) => ({
x: cx + x * Math.cos(yaw) + z * Math.sin(yaw) * .42,
y: cy - y - z * .28 + x * Math.sin(yaw) * .08
});
c.fillStyle = 'rgba(53,246,163,.92)';
c.font = '12px system-ui';
c.fillText((this.scene?.type || this.model?.recognized || 'AIR MODEL').toUpperCase(), 28, 28);
objects.forEach((obj, index) => {
if (obj.kind === 'modelRef' && obj.model?.objects) {
obj.model.objects.forEach((child) => this.drawObject(c, child, project, index + 1));
} else {
this.drawObject(c, obj, project, index);
}
});
}
drawObject(c, obj, project, index = 0) {
const color = obj.color || (index % 2 ? '#8f7cff' : '#35f6a3');
c.strokeStyle = color;
c.fillStyle = color + '55';
c.lineWidth = 2;
c.shadowColor = color;
c.shadowBlur = 12;
if (obj.kind === 'extrudedStroke') {
const depth = obj.depth || 72;
const points = obj.points || [];
[0, depth].forEach((z) => {
c.beginPath();
points.forEach((p, i) => {
const q = project((p.x || 0) - 360, 270 - (p.y || 0), z - depth / 2);
if (i) c.lineTo(q.x, q.y); else c.moveTo(q.x, q.y);
});
c.stroke();
});
points.filter((_, i) => i % 18 === 0).forEach((p) => {
const a = project((p.x || 0) - 360, 270 - (p.y || 0), -depth / 2);
const b = project((p.x || 0) - 360, 270 - (p.y || 0), depth / 2);
c.beginPath(); c.moveTo(a.x, a.y); c.lineTo(b.x, b.y); c.stroke();
});
return;
}
const x = obj.x || 0, z = obj.z || 0, ww = obj.w || (obj.r || 40) * 2, dd = obj.d || (obj.r || 40) * 2, hh = obj.h || 80;
if (obj.kind === 'target') {
const p = project(x, hh, z);
c.beginPath(); c.arc(p.x, p.y, obj.r || 40, 0, Math.PI * 2); c.stroke();
c.beginPath(); c.arc(p.x, p.y, (obj.r || 40) * .45, 0, Math.PI * 2); c.stroke();
return;
}
if (obj.kind === 'light') {
const p = project(x, obj.h || 180, z);
const glow = c.createRadialGradient(p.x, p.y, 0, p.x, p.y, 90);
glow.addColorStop(0, color + 'aa');
glow.addColorStop(1, 'rgba(0,0,0,0)');
c.fillStyle = glow;
c.beginPath(); c.arc(p.x, p.y, 90, 0, Math.PI * 2); c.fill();
c.fillStyle = color;
c.beginPath(); c.arc(p.x, p.y, 8, 0, Math.PI * 2); c.fill();
return;
}
if (obj.kind === 'cylinder') {
const top = project(x, hh, z);
const bottom = project(x, 0, z);
const r = obj.r || 32;
c.beginPath(); c.ellipse(top.x, top.y, r, r * .35, 0, 0, Math.PI * 2); c.stroke();
c.beginPath(); c.ellipse(bottom.x, bottom.y, r, r * .35, 0, 0, Math.PI * 2); c.stroke();
c.beginPath(); c.moveTo(top.x - r, top.y); c.lineTo(bottom.x - r, bottom.y); c.moveTo(top.x + r, top.y); c.lineTo(bottom.x + r, bottom.y); c.stroke();
return;
}
const corners = [
project(x - ww / 2, 0, z - dd / 2), project(x + ww / 2, 0, z - dd / 2),
project(x + ww / 2, 0, z + dd / 2), project(x - ww / 2, 0, z + dd / 2),
project(x - ww / 2, hh, z - dd / 2), project(x + ww / 2, hh, z - dd / 2),
project(x + ww / 2, hh, z + dd / 2), project(x - ww / 2, hh, z + dd / 2),
];
const faces = [
[0, 1, 2, 3, '22'],
[4, 5, 6, 7, '55'],
[1, 5, 6, 2, '38'],
[3, 7, 4, 0, '2c'],
];
faces.forEach((face) => {
c.fillStyle = color + face[4];
c.beginPath();
face.slice(0, 4).forEach((idx, i) => i ? c.lineTo(corners[idx].x, corners[idx].y) : c.moveTo(corners[idx].x, corners[idx].y));
c.closePath();
c.fill();
});
const edges = [[0,1],[1,2],[2,3],[3,0],[4,5],[5,6],[6,7],[7,4],[0,4],[1,5],[2,6],[3,7]];
edges.forEach(([a,b]) => { c.beginPath(); c.moveTo(corners[a].x, corners[a].y); c.lineTo(corners[b].x, corners[b].y); c.stroke(); });
c.shadowBlur = 0;
}
path(ctx, stroke) {
if (!stroke.length) return;
ctx.beginPath();
ctx.moveTo(stroke[0].x, stroke[0].y);
stroke.slice(1).forEach((p) => ctx.lineTo(p.x, p.y));
ctx.stroke();
}
}
class NotesPad {
constructor() {
this.canvas = $('noteCanvas');
this.points = [];
this.current = null;
if (!this.canvas) return;
this.ctx = this.canvas.getContext('2d');
this.bind();
this.clearDrawing();
}
bind() {
this.canvas.addEventListener('pointerdown', (event) => {
this.current = [this.point(event)];
this.canvas.setPointerCapture(event.pointerId);
});
this.canvas.addEventListener('pointermove', (event) => {
if (!this.current) return;
this.current.push(this.point(event));
this.draw();
});
this.canvas.addEventListener('pointerup', () => {
if (this.current?.length) this.points.push(this.current);
this.current = null;
this.draw();
});
}
point(event) {
const rect = this.canvas.getBoundingClientRect();
return {
x: (event.clientX - rect.left) * (this.canvas.width / rect.width),
y: (event.clientY - rect.top) * (this.canvas.height / rect.height)
};
}
resize() {
this.draw();
}
clearDrawing() {
this.points = [];
this.current = null;
this.draw();
}
draw() {
const c = this.ctx;
if (!c) return;
c.fillStyle = '#061016';
c.fillRect(0, 0, this.canvas.width, this.canvas.height);
c.strokeStyle = 'rgba(53,246,163,.12)';
c.lineWidth = 1;
for (let x = 0; x < this.canvas.width; x += 32) {
c.beginPath(); c.moveTo(x, 0); c.lineTo(x, this.canvas.height); c.stroke();
}
for (let y = 0; y < this.canvas.height; y += 32) {
c.beginPath(); c.moveTo(0, y); c.lineTo(this.canvas.width, y); c.stroke();
}
c.strokeStyle = '#ffd166';
c.lineWidth = 5;
c.lineCap = 'round';
c.lineJoin = 'round';
[...this.points, this.current].filter(Boolean).forEach((stroke) => {
c.beginPath();
c.moveTo(stroke[0].x, stroke[0].y);
stroke.slice(1).forEach((p) => c.lineTo(p.x, p.y));
c.stroke();
});
}
storageKey() {
return 'blackDragonNotes_' + (ai?.userId || 'local');
}
readNotes() {
try {
return JSON.parse(localStorage.getItem(this.storageKey()) || '[]');
} catch (error) {
return [];
}
}
writeNotes(notes) {
localStorage.setItem(this.storageKey(), JSON.stringify(notes.slice(-200)));
}
async save() {
const title = $('noteTitle')?.value?.trim() || 'Black Dragon Note';
const text = $('noteText')?.value?.trim() || '';
const hasDrawing = this.points.length > 0 || this.current;
const drawing = hasDrawing ? this.canvas.toDataURL('image/png') : '';
if (!text && !drawing) return ai.toast('Write or draw a note first.');
const notes = this.readNotes();
notes.push({ title, text, drawing, created_at: new Date().toISOString() });
this.writeNotes(notes);
ai.toast('Note saved.');
$('noteText').value = '';
await this.load();
}
async load() {
const list = $('notesList');
if (!list) return;
const notes = this.readNotes();
list.innerHTML = notes.map((n) => `
<div class="secure-msg">
<strong>${ai.escape(n.title)}</strong>
<div>${ai.escape(n.text || '')}</div>
${n.drawing ? `<img alt="drawing note" src="${n.drawing}" style="width:100%;max-height:160px;object-fit:contain;margin-top:8px;border:1px solid var(--line);border-radius:8px;">` : ''}
<small>${ai.escape(n.created_at || '')}</small>
</div>`).join('') || '<div class="hint">No notes yet.</div>';
}
}
class DragonGames {
constructor() {
this.canvas = $('gameCanvas');
this.ctx = this.canvas.getContext('2d');
this.type = 'flap';
this.keys = {};
this.controls = {};
this.pointer = false;
this.pointerX = null;
this.pointerY = null;
this.pointerStart = null;
this.running = false;
this.effects = [];
this.palette = { green: '#35f6a3', gold: '#ffd166', violet: '#8f7cff', red: '#ff4f64', ink: '#061016' };
try {
this.bestByType = JSON.parse(localStorage.getItem('blackDragonGameBestByType')) || {};
} catch (error) {
this.bestByType = {};
}
addEventListener('keydown', (e) => {
if ($('gameZone')?.classList.contains('show') && [' ', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) e.preventDefault();
this.keys[e.key.toLowerCase()] = true;
if (e.key === ' ') this.action();
});
addEventListener('keyup', (e) => { this.keys[e.key.toLowerCase()] = false; });
this.canvas.addEventListener('pointerdown', (event) => { const p = this.canvasPoint(event); this.pointer = true; this.pointerX = p.x; this.pointerY = p.y; this.pointerStart = p; this.action(); });
this.canvas.addEventListener('pointermove', (event) => { if (this.pointer) { const p = this.canvasPoint(event); this.pointerX = p.x; this.pointerY = p.y; } });
this.canvas.addEventListener('pointerup', (event) => { this.handlePointerUp(event); });
this.canvas.addEventListener('pointercancel', () => { this.pointer = false; this.pointerX = null; this.pointerY = null; this.pointerStart = null; });
}
canvasPoint(event) {
const rect = this.canvas.getBoundingClientRect();
return {
x: (event.clientX - rect.left) * (this.canvas.width / rect.width),
y: (event.clientY - rect.top) * (this.canvas.height / rect.height)
};
}
pressControl(name) {
this.controls[name] = true;
if (name === 'gas' || name === 'fire') this.action();
}
releaseControl(name) {
this.controls[name] = false;
}
handlePointerUp(event) {
const end = this.canvasPoint(event);
if (this.type === 'snake' && this.pointerStart) {
const dx = end.x - this.pointerStart.x;
const dy = end.y - this.pointerStart.y;
if (Math.max(Math.abs(dx), Math.abs(dy)) > 24) {
const s = this.state;
if (Math.abs(dx) > Math.abs(dy)) s.next = dx > 0 && s.dir !== 'left' ? 'right' : dx < 0 && s.dir !== 'right' ? 'left' : s.next;
else s.next = dy > 0 && s.dir !== 'up' ? 'down' : dy < 0 && s.dir !== 'down' ? 'up' : s.next;
}
}
this.pointer = false;
this.pointerX = null;
this.pointerY = null;
this.pointerStart = null;
}
start(type) {
this.type = type;
$('gameZone')?.setAttribute('data-game', type);
document.querySelectorAll('.game-btn').forEach((b) => b.classList.remove('primary'));
const labels = { flap: 'Flap Dragon', race: 'Dragon Race', space: 'Space Shooter', snake: 'Feed Dragon', catch: 'Dragon Catch', breaker: 'Crystal Breaker', pong: 'Dragon Pong' };
const helps = {
flap: 'Space, click, or tap to flap. Dodge bamboo towers and collect gold orbs.',
race: 'Use Left/Right or A/D to steer. Hold Gas to go faster, Brake to slow down. Dodge traffic and collect boosts.',
space: 'Use Arrow Left/Right, A/D, or drag. Space/click/tap fires plasma.',
snake: 'Use Arrow keys or W/A/S/D. Eat the fire fruit and protect the dragon head.',
catch: 'Use Arrow Left/Right, A/D, or drag. Catch gold gems and avoid red fire.',
breaker: 'Move with A/D, Arrow keys, or drag. Break all crystals with the energy ball.',
pong: 'Use W/S, Arrow Up/Down, or drag. Beat the auto paddle.'
};
[...document.querySelectorAll('.game-btn')].find((b) => b.textContent === labels[type])?.classList.add('primary');
$('gameTitle').textContent = labels[type];
$('gameHelp').textContent = helps[type] || helps.flap;
this.updateBestLabel();
this.restart();
}
restart() {
cancelAnimationFrame(this.raf);
this.score = 0;
this.frame = 0;
this.effects = [];
this.running = true;
if (this.type === 'flap') this.state = { x: 135, r: 22, y: 260, vy: 0, pipes: [{ x: 930, gap: 228, top: 150, orb: true }], clouds: [] };
if (this.type === 'race') this.state = { x: 480, steer: 0, speed: 6.5, maxSpeed: 15, road: 0, traffic: [], boosts: [], distance: 0, gear: 1, damage: 0, invincible: 45 };
if (this.type === 'space') this.state = { x: 480, bullets: [], rocks: [], cool: 0, stars: [] };
if (this.type === 'snake') {
const snake = [{ x: 8, y: 8 }, { x: 7, y: 8 }, { x: 6, y: 8 }];
this.state = { dir: 'right', next: 'right', snake, food: this.placeSnakeFood(snake), tick: 0 };
}
if (this.type === 'catch') this.state = { x: 480, gems: [], fire: [], shield: 0 };
if (this.type === 'breaker') this.state = { paddleX: 480, ball: { x: 480, y: 486, vx: 0, vy: 0, stuck: true }, bricks: this.makeBricks(), level: 1, lives: 3 };
if (this.type === 'pong') this.state = { playerY: 300, aiY: 300, ball: { x: 480, y: 300, vx: 6, vy: 3.5 }, player: 0, ai: 0 };
this.updateBestLabel();
this.loop();
}
action() {
if (!this.running) return this.restart();
if (this.type === 'flap') {
this.state.vy = -8.7;
this.pop(this.state.x - 18, this.state.y + 16, this.palette.green, 4);
}
if (this.type === 'space' && this.state.cool <= 0) {
this.state.bullets.push({ x: this.state.x, y: 500 });
this.state.bullets.push({ x: this.state.x - 16, y: 512 });
this.state.bullets.push({ x: this.state.x + 16, y: 512 });
this.state.cool = 10;
}
if (this.type === 'breaker' && this.state.ball.stuck) {
this.state.ball.stuck = false;
this.state.ball.vx = 4.8;
this.state.ball.vy = -7.2;
}
}
loop() {
this.update();
this.draw();
if (this.running) this.raf = requestAnimationFrame(() => this.loop());
}
update() {
this.frame++;
if (this.type === 'flap') this.updateFlap();
if (this.type === 'race') this.updateRace();
if (this.type === 'space') this.updateSpace();
if (this.type === 'snake') this.updateSnake();
if (this.type === 'catch') this.updateCatch();
if (this.type === 'breaker') this.updateBreaker();
if (this.type === 'pong') this.updatePong();
this.effects.forEach((e) => { e.x += e.vx; e.y += e.vy; e.vy += .08; e.life--; });
this.effects = this.effects.filter((e) => e.life > 0);
$('score').textContent = this.score;
this.updateBestLabel();
this.updateGameStats();
}
updateFlap() {
const s = this.state;
s.vy += .43;
s.y += s.vy;
const speed = 4.1 + Math.min(2.4, this.score * .06);
if (this.frame % 84 === 0) s.pipes.push({ x: 960, gap: Math.max(180, 232 - this.score * 1.2), top: 76 + Math.random() * 270, orb: true });
if (this.frame % 110 === 0) s.clouds.push({ x: 980, y: 45 + Math.random() * 180, w: 70 + Math.random() * 80 });
s.clouds.forEach((cloud) => cloud.x -= speed * .28);
s.clouds = s.clouds.filter((cloud) => cloud.x > -180);
s.pipes.forEach((p) => p.x -= speed);
s.pipes = s.pipes.filter((p) => p.x > -90);
s.pipes.forEach((p) => {
if (!p.done && p.x + 70 < s.x - s.r) { p.done = true; this.score++; this.pop(s.x, s.y, '#ffcf5a', 10); }
const hitTop = this.circleRect(s.x, s.y, s.r, p.x, 0, 70, p.top);
const hitBottom = this.circleRect(s.x, s.y, s.r, p.x, p.top + p.gap, 70, 600 - (p.top + p.gap));
if (hitTop || hitBottom) this.gameOver();
if (p.orb && Math.hypot(s.x - (p.x + 35), s.y - (p.top + p.gap / 2)) < s.r + 14) {
p.orb = false;
this.score += 3;
this.pop(p.x + 35, p.top + p.gap / 2, this.palette.gold, 18);
}
});
if (s.y - s.r < 0 || s.y + s.r > 600) this.gameOver();
}
updateRace() {
const s = this.state;
const left = this.keys.arrowleft || this.keys.a || this.controls.left;
const right = this.keys.arrowright || this.keys.d || this.controls.right;
const throttle = this.keys.arrowup || this.keys.w || this.controls.gas;
const brake = this.keys.arrowdown || this.keys.s || this.controls.brake;
if (throttle) s.speed += .18;
else s.speed += (8.0 - s.speed) * .025;
if (brake) s.speed -= .38;
s.speed = Math.max(2.5, Math.min(s.maxSpeed + Math.min(4, this.score * .025), s.speed));
s.gear = Math.max(1, Math.min(6, Math.floor(s.speed / 3) + 1));
if (this.pointerX !== null) {
s.x += (this.pointerX - s.x) * .2;
s.steer = Math.max(-1, Math.min(1, (this.pointerX - 480) / 420));
} else {
s.steer = (left ? -1 : 0) + (right ? 1 : 0);
s.x += s.steer * (5.8 + s.speed * .16);
}
s.x = Math.max(276, Math.min(684, s.x));
s.road += s.speed;
s.distance += s.speed * .018;
this.score = Math.floor(s.distance);
s.invincible = Math.max(0, (s.invincible || 0) - 1);
if (this.frame % Math.max(34, 76 - Math.floor(s.speed * 2.4)) === 0) {
const lanes = [315, 420, 540, 645];
const x = lanes[Math.floor(Math.random() * lanes.length)];
if (!s.traffic.some((car) => car.y < 90 && Math.abs(car.x - x) < 70)) {
s.traffic.push({ x, y: -90, w: 54, h: 90, color: Math.random() > .55 ? this.palette.red : this.palette.violet });
}
}
if (this.frame % 135 === 0) {
const lanes = [315, 420, 540, 645];
s.boosts.push({ x: lanes[Math.floor(Math.random() * lanes.length)], y: -40, r: 15 });
}
s.traffic.forEach((car) => car.y += s.speed * .92 + 2.4);
s.boosts.forEach((boost) => boost.y += s.speed * .95 + 2.8);
s.traffic = s.traffic.filter((car) => {
if (car.y > 690) return false;
if (s.invincible <= 0 && this.rectsOverlap(s.x - 31, 478, 62, 96, car.x - car.w / 2, car.y - car.h / 2, car.w, car.h)) {
s.damage++;
s.speed = Math.max(3, s.speed * .45);
s.invincible = 80;
this.pop(s.x, 520, this.palette.red, 28);
if (s.damage >= 4) this.gameOver();
return false;
}
return true;
});
s.boosts = s.boosts.filter((boost) => {
if (Math.hypot(boost.x - s.x, boost.y - 520) < boost.r + 34) {
s.speed = Math.min(s.maxSpeed + 3, s.speed + 3.8);
this.score += 5;
s.distance += 5;
this.pop(boost.x, boost.y, this.palette.gold, 22);
return false;
}
return boost.y < 690;
});
}
updateSpace() {
const s = this.state;
if (this.keys.arrowleft || this.keys.a || this.controls.left) s.x -= 7;
if (this.keys.arrowright || this.keys.d || this.controls.right) s.x += 7;
if (this.pointerX !== null) s.x += (this.pointerX - s.x) * .22;
if (this.keys[' '] || this.pointer || this.controls.gas) this.action();
s.x = Math.max(40, Math.min(920, s.x));
s.cool--;
if (this.frame % 36 === 0) s.rocks.push({ x: 30 + Math.random() * 900, y: -30, r: 18 + Math.random() * 18 });
s.bullets.forEach((b) => b.y -= 12);
s.rocks.forEach((r) => r.y += 4.4);
s.bullets = s.bullets.filter((b) => b.y > -20);
s.rocks = s.rocks.filter((r) => {
for (const b of s.bullets) {
if (Math.hypot(b.x - r.x, b.y - r.y) < r.r + 7) { r.hit = true; b.y = -99; this.score++; this.pop(r.x, r.y, this.palette.violet, 16); }
}
if (Math.hypot(s.x - r.x, 520 - r.y) < r.r + 24) this.gameOver();
return !r.hit && r.y < 650;
});
}
updateSnake() {
const s = this.state;
const opposite = { up: 'down', down: 'up', left: 'right', right: 'left' };
if ((this.keys.arrowup || this.keys.w) && s.dir !== 'down') s.next = 'up';
if ((this.keys.arrowdown || this.keys.s) && s.dir !== 'up') s.next = 'down';
if ((this.keys.arrowleft || this.keys.a) && s.dir !== 'right') s.next = 'left';
if ((this.keys.arrowright || this.keys.d) && s.dir !== 'left') s.next = 'right';
if (opposite[s.next] === s.dir) s.next = s.dir;
if (++s.tick % 8) return;
s.dir = s.next;
const head = { ...s.snake[0] };
if (s.dir === 'up') head.y--;
if (s.dir === 'down') head.y++;
if (s.dir === 'left') head.x--;
if (s.dir === 'right') head.x++;
const eatsFood = head.x === s.food.x && head.y === s.food.y;
const bodyToCheck = eatsFood ? s.snake : s.snake.slice(0, -1);
if (head.x < 0 || head.x >= 30 || head.y < 0 || head.y >= 18 || bodyToCheck.some((p) => p.x === head.x && p.y === head.y)) return this.gameOver();
s.snake.unshift(head);
if (eatsFood) {
this.score++;
this.pop(head.x * 32 + 16, head.y * 32 + 16, '#ffcf5a', 12);
s.food = this.placeSnakeFood(s.snake);
} else s.snake.pop();
}
placeSnakeFood(snake) {
for (let attempt = 0; attempt < 300; attempt++) {
const food = { x: Math.floor(Math.random() * 30), y: Math.floor(Math.random() * 18) };
if (!snake.some((p) => p.x === food.x && p.y === food.y)) return food;
}
return { x: 17, y: 10 };
}
circleRect(cx, cy, cr, rx, ry, rw, rh) {
const closestX = Math.max(rx, Math.min(cx, rx + rw));
const closestY = Math.max(ry, Math.min(cy, ry + rh));
return Math.hypot(cx - closestX, cy - closestY) <= cr;
}
rectsOverlap(ax, ay, aw, ah, bx, by, bw, bh) {
return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
}
updateCatch() {
const s = this.state;
if (this.keys.arrowleft || this.keys.a || this.controls.left) s.x -= 8;
if (this.keys.arrowright || this.keys.d || this.controls.right) s.x += 8;
if (this.pointerX !== null) s.x += (this.pointerX - s.x) * .2;
s.x = Math.max(55, Math.min(905, s.x));
if (this.frame % 34 === 0) s.gems.push({ x: 35 + Math.random() * 890, y: -28, r: 14 });
if (this.frame % 90 === 0) s.fire.push({ x: 35 + Math.random() * 890, y: -28, r: 18 });
s.gems.forEach((g) => g.y += 5.2);
s.fire.forEach((f) => f.y += 6.1);
s.gems = s.gems.filter((g) => {
if (Math.abs(g.x - s.x) < 48 && Math.abs(g.y - 522) < 34) { this.score++; this.pop(g.x, g.y, '#ffcf5a', 12); return false; }
return g.y < 640;
});
s.fire = s.fire.filter((f) => {
if (Math.abs(f.x - s.x) < 50 && Math.abs(f.y - 522) < 38) this.gameOver();
return f.y < 640;
});
}
makeBricks() {
const bricks = [];
const colors = [this.palette.green, this.palette.gold, this.palette.violet, this.palette.red];
for (let row = 0; row < 5; row++) {
for (let col = 0; col < 10; col++) {
bricks.push({ x: 86 + col * 82, y: 64 + row * 34, w: 68, h: 22, color: colors[(row + col) % colors.length], alive: true });
}
}
return bricks;
}
updateBreaker() {
const s = this.state;
if (this.keys.arrowleft || this.keys.a || this.controls.left) s.paddleX -= 9;
if (this.keys.arrowright || this.keys.d || this.controls.right) s.paddleX += 9;
if (this.controls.gas && s.ball.stuck) this.action();
if (this.pointerX !== null) s.paddleX += (this.pointerX - s.paddleX) * .28;
s.paddleX = Math.max(70, Math.min(890, s.paddleX));
const b = s.ball;
if (b.stuck) {
b.x = s.paddleX;
b.y = 486;
return;
}
b.x += b.vx;
b.y += b.vy;
if (b.x < 16 || b.x > 944) b.vx *= -1;
if (b.y < 24) b.vy *= -1;
if (b.y > 620) {
s.lives--;
if (s.lives <= 0) return this.gameOver();
Object.assign(b, { x: s.paddleX, y: 486, vx: 0, vy: 0, stuck: true });
}
if (b.y > 468 && b.y < 506 && Math.abs(b.x - s.paddleX) < 74 && b.vy > 0) {
b.vy = -Math.abs(b.vy) - .12;
b.vx += (b.x - s.paddleX) * .055;
this.pop(b.x, b.y, this.palette.green, 8);
}
s.bricks.forEach((brick) => {
if (!brick.alive) return;
if (this.circleRect(b.x, b.y, 10, brick.x, brick.y, brick.w, brick.h)) {
brick.alive = false;
b.vy *= -1;
this.score += 2;
this.pop(brick.x + brick.w / 2, brick.y + brick.h / 2, brick.color, 10);
}
});
if (s.bricks.every((brick) => !brick.alive)) {
s.level++;
s.bricks = this.makeBricks();
Object.assign(b, { x: s.paddleX, y: 486, vx: 0, vy: 0, stuck: true });
this.score += 10;
}
}
updatePong() {
const s = this.state;
if (this.keys.arrowup || this.keys.w || this.controls.gas) s.playerY -= 8.5;
if (this.keys.arrowdown || this.keys.s || this.controls.brake) s.playerY += 8.5;
if (this.pointerY !== null) s.playerY += (this.pointerY - s.playerY) * .25;
s.playerY = Math.max(70, Math.min(530, s.playerY));
s.aiY += (s.ball.y - s.aiY) * .055;
s.aiY = Math.max(70, Math.min(530, s.aiY));
const b = s.ball;
b.x += b.vx;
b.y += b.vy;
if (b.y < 18 || b.y > 582) b.vy *= -1;
if (b.x < 62 && Math.abs(b.y - s.playerY) < 62 && b.vx < 0) {
b.vx = Math.abs(b.vx) + .35;
b.vy += (b.y - s.playerY) * .045;
this.score++;
this.pop(72, b.y, this.palette.green, 8);
}
if (b.x > 898 && Math.abs(b.y - s.aiY) < 62 && b.vx > 0) {
b.vx = -Math.abs(b.vx) - .18;
b.vy += (b.y - s.aiY) * .035;
}
if (b.x < -30) {
s.ai++;
if (s.ai >= 3) return this.gameOver();
s.ball = { x: 480, y: 300, vx: 6, vy: 3.5 };
}
if (b.x > 990) {
s.player++;
this.score += 5;
s.ball = { x: 480, y: 300, vx: -6, vy: -3.5 };
}
}
draw() {
const c = this.ctx;
c.clearRect(0, 0, 960, 600);
c.fillStyle = '#071018';
c.fillRect(0, 0, 960, 600);
if (this.type === 'flap') this.drawFlap(c);
if (this.type === 'race') this.drawRace(c);
if (this.type === 'space') this.drawSpace(c);
if (this.type === 'snake') this.drawSnake(c);
if (this.type === 'catch') this.drawCatch(c);
if (this.type === 'breaker') this.drawBreaker(c);
if (this.type === 'pong') this.drawPong(c);
this.drawEffects(c);
this.hud(c);
}
drawFlap(c) {
const s = this.state;
this.sky(c);
c.fillStyle = 'rgba(255,255,255,.13)';
(s.clouds || []).forEach((cloud) => {
c.beginPath();
c.ellipse(cloud.x, cloud.y, cloud.w * .42, 18, 0, 0, Math.PI * 2);
c.ellipse(cloud.x + cloud.w * .25, cloud.y + 7, cloud.w * .34, 15, 0, 0, Math.PI * 2);
c.fill();
});
s.pipes.forEach((p) => {
const grad = c.createLinearGradient(p.x, 0, p.x + 70, 0);
grad.addColorStop(0, '#0c4a34');
grad.addColorStop(.5, '#35f6a3');
grad.addColorStop(1, '#073122');
c.fillStyle = grad;
c.fillRect(p.x, 0, 70, p.top);
c.fillRect(p.x, p.top + p.gap, 70, 600);
c.fillStyle = 'rgba(255,207,90,.55)';
for (let y = 18; y < 600; y += 46) c.fillRect(p.x, y, 70, 3);
c.fillStyle = '#ffd166';
c.fillRect(p.x - 6, p.top - 12, 82, 12);
c.fillRect(p.x - 6, p.top + p.gap, 82, 12);
if (p.orb) {
const ox = p.x + 35;
const oy = p.top + p.gap / 2;
c.shadowColor = '#ffd166';
c.shadowBlur = 18;
c.beginPath(); c.arc(ox, oy, 12 + Math.sin(this.frame * .12) * 2, 0, Math.PI * 2); c.fill();
c.shadowBlur = 0;
}
});
this.dragon(c, s.x, s.y, s.r, s.vy);
}
drawRace(c) {
const s = this.state;
const bg = c.createLinearGradient(0, 0, 0, 600);
bg.addColorStop(0, '#0b1620');
bg.addColorStop(.38, '#11261a');
bg.addColorStop(1, '#07100c');
c.fillStyle = bg;
c.fillRect(0, 0, 960, 600);
c.fillStyle = '#15191d';
c.fillRect(240, 0, 480, 600);
c.fillStyle = '#263039';
c.fillRect(258, 0, 10, 600);
c.fillRect(692, 0, 10, 600);
c.strokeStyle = 'rgba(255,255,255,.82)';
c.lineWidth = 5;
c.setLineDash([36, 30]);
[360, 480, 600].forEach((x) => {
c.beginPath();
c.moveTo(x, -80 + (s.road % 66));
c.lineTo(x, 680);
c.stroke();
});
c.setLineDash([]);
c.fillStyle = 'rgba(53,246,163,.14)';
for (let y = -80 + (s.road * .45 % 96); y < 680; y += 96) {
c.fillRect(82, y, 62, 28);
c.fillRect(808, y + 35, 62, 28);
}
s.boosts.forEach((boost) => {
c.save();
c.shadowColor = this.palette.gold;
c.shadowBlur = 16;
c.fillStyle = this.palette.gold;
c.beginPath();
c.moveTo(boost.x, boost.y - boost.r);
c.lineTo(boost.x + boost.r, boost.y);
c.lineTo(boost.x, boost.y + boost.r);
c.lineTo(boost.x - boost.r, boost.y);
c.closePath();
c.fill();
c.restore();
});
s.traffic.forEach((car) => this.car(c, car.x, car.y, car.color, car.w, car.h));
const blink = s.invincible > 0 && Math.floor(this.frame / 5) % 2 === 0;
if (!blink) this.car(c, s.x, 528, this.palette.green, 64, 100);
c.fillStyle = 'rgba(0,0,0,.66)';
c.fillRect(0, 0, 220, 94);
c.fillStyle = this.palette.green;
c.font = '800 18px system-ui';
c.fillText(`${Math.round(s.speed * 16)} km/h`, 18, 31);
c.fillText(`Gear ${s.gear}`, 18, 58);
c.fillStyle = s.damage >= 3 ? this.palette.red : this.palette.gold;
c.fillText(`Damage ${s.damage}/4`, 18, 85);
}
drawSpace(c) {
const s = this.state;
this.sky(c);
c.fillStyle = '#35f6a3';
c.shadowColor = '#35f6a3';
c.shadowBlur = 12;
c.beginPath(); c.moveTo(s.x, 488); c.lineTo(s.x - 28, 540); c.lineTo(s.x + 28, 540); c.closePath(); c.fill();
c.shadowBlur = 0;
c.fillStyle = '#ffcf5a';
s.bullets.forEach((b) => c.fillRect(b.x - 3, b.y - 14, 6, 18));
s.rocks.forEach((r) => {
c.fillStyle = '#8d99ae';
c.beginPath(); c.arc(r.x, r.y, r.r, 0, Math.PI * 2); c.fill();
c.strokeStyle = '#8f7cff'; c.stroke();
});
}
drawSnake(c) {
const s = this.state;
const cell = 32;
c.fillStyle = '#08120d'; c.fillRect(0, 0, 960, 600);
const glow = c.createRadialGradient(480, 300, 80, 480, 300, 520);
glow.addColorStop(0, 'rgba(53,246,163,.08)');
glow.addColorStop(1, 'rgba(0,0,0,0)');
c.fillStyle = glow; c.fillRect(0, 0, 960, 600);
c.strokeStyle = 'rgba(255,255,255,.05)';
for (let x = 0; x < 30; x++) for (let y = 0; y < 18; y++) c.strokeRect(x * cell, y * cell, cell, cell);
const fx = s.food.x * cell + 16;
const fy = s.food.y * cell + 16;
c.fillStyle = '#ffd166';
c.shadowColor = '#ffd166';
c.shadowBlur = 16;
c.beginPath(); c.arc(fx, fy, 10 + Math.sin(this.frame * .12) * 2, 0, Math.PI * 2); c.fill();
c.shadowBlur = 0;
s.snake.forEach((p, i) => {
const x = p.x * cell + 3;
const y = p.y * cell + 3;
c.fillStyle = i === 0 ? '#8f7cff' : (i % 2 ? '#35f6a3' : '#19c987');
c.shadowColor = i === 0 ? '#8f7cff' : '#35f6a3';
c.shadowBlur = i === 0 ? 16 : 8;
c.beginPath();
c.roundRect ? c.roundRect(x, y, 26, 26, 7) : c.rect(x, y, 26, 26);
c.fill();
c.shadowBlur = 0;
if (i === 0) {
c.fillStyle = '#00120b';
c.fillRect(p.x * cell + 10, p.y * cell + 10, 4, 4);
c.fillRect(p.x * cell + 19, p.y * cell + 10, 4, 4);
c.strokeStyle = '#ffd166';
c.lineWidth = 2;
c.strokeRect(p.x * cell + 2, p.y * cell + 2, 28, 28);
c.strokeStyle = '#ffd166';
c.beginPath();
c.moveTo(p.x * cell + 8, p.y * cell + 2);
c.lineTo(p.x * cell + 5, p.y * cell - 5);
c.moveTo(p.x * cell + 24, p.y * cell + 2);
c.lineTo(p.x * cell + 27, p.y * cell - 5);
c.stroke();
}
});
}
drawCatch(c) {
const s = this.state;
this.sky(c);
c.fillStyle = 'rgba(53,246,163,.16)';
c.fillRect(0, 548, 960, 52);
c.fillStyle = '#35f6a3';
c.beginPath();
c.moveTo(s.x - 46, 542);
c.lineTo(s.x + 46, 542);
c.lineTo(s.x + 28, 576);
c.lineTo(s.x - 28, 576);
c.closePath();
c.fill();
c.fillStyle = '#ffcf5a';
s.gems.forEach((g) => {
c.beginPath();
c.moveTo(g.x, g.y - g.r);
c.lineTo(g.x + g.r, g.y);
c.lineTo(g.x, g.y + g.r);
c.lineTo(g.x - g.r, g.y);
c.closePath();
c.fill();
});
c.fillStyle = '#ff4f64';
s.fire.forEach((f) => {
c.beginPath();
c.arc(f.x, f.y, f.r, 0, Math.PI * 2);
c.fill();
});
}
drawBreaker(c) {
const s = this.state;
const bg = c.createLinearGradient(0, 0, 960, 600);
bg.addColorStop(0, '#090822');
bg.addColorStop(.55, '#10151a');
bg.addColorStop(1, '#06120d');
c.fillStyle = bg;
c.fillRect(0, 0, 960, 600);
c.strokeStyle = 'rgba(255,255,255,.06)';
for (let x = 0; x < 960; x += 36) { c.beginPath(); c.moveTo(x, 0); c.lineTo(x, 600); c.stroke(); }
s.bricks.forEach((brick) => {
if (!brick.alive) return;
c.shadowColor = brick.color;
c.shadowBlur = 12;
c.fillStyle = brick.color;
c.beginPath();
c.roundRect ? c.roundRect(brick.x, brick.y, brick.w, brick.h, 6) : c.rect(brick.x, brick.y, brick.w, brick.h);
c.fill();
c.shadowBlur = 0;
c.fillStyle = 'rgba(255,255,255,.18)';
c.fillRect(brick.x + 5, brick.y + 4, brick.w - 10, 3);
});
c.fillStyle = this.palette.green;
c.shadowColor = this.palette.green;
c.shadowBlur = 16;
c.beginPath();
c.roundRect ? c.roundRect(s.paddleX - 72, 504, 144, 18, 9) : c.rect(s.paddleX - 72, 504, 144, 18);
c.fill();
c.fillStyle = this.palette.gold;
c.beginPath(); c.arc(s.ball.x, s.ball.y, 10, 0, Math.PI * 2); c.fill();
c.shadowBlur = 0;
if (s.ball.stuck) {
c.fillStyle = 'rgba(238,247,242,.76)';
c.font = '18px system-ui';
c.textAlign = 'center';
c.fillText('Tap or press Space to launch', 480, 560);
c.textAlign = 'left';
}
}
drawPong(c) {
const s = this.state;
c.fillStyle = '#050b0d';
c.fillRect(0, 0, 960, 600);
c.strokeStyle = 'rgba(53,246,163,.35)';
c.lineWidth = 4;
c.setLineDash([18, 18]);
c.beginPath(); c.moveTo(480, 0); c.lineTo(480, 600); c.stroke();
c.setLineDash([]);
c.fillStyle = 'rgba(143,124,255,.12)';
c.fillRect(0, 0, 480, 600);
c.fillStyle = this.palette.green;
c.fillRect(46, s.playerY - 58, 18, 116);
c.fillStyle = this.palette.violet;
c.fillRect(896, s.aiY - 58, 18, 116);
c.shadowColor = this.palette.gold;
c.shadowBlur = 18;
c.fillStyle = this.palette.gold;
c.beginPath(); c.arc(s.ball.x, s.ball.y, 13, 0, Math.PI * 2); c.fill();
c.shadowBlur = 0;
c.fillStyle = '#eef7f2';
c.font = '800 44px system-ui';
c.textAlign = 'center';
c.fillText(`${s.player} : ${s.ai}`, 480, 78);
c.textAlign = 'left';
}
sky(c) {
const bg = c.createLinearGradient(0, 0, 960, 600);
bg.addColorStop(0, '#06151f');
bg.addColorStop(.5, '#11152b');
bg.addColorStop(1, '#06140d');
c.fillStyle = bg; c.fillRect(0, 0, 960, 600);
c.fillStyle = 'rgba(255,255,255,.55)';
for (let i = 0; i < 54; i++) c.fillRect((i * 83 + this.frame * .7) % 960, (i * 47) % 560, 2 + (i % 3 === 0 ? 1 : 0), 2);
}
dragon(c, x, y, r = 24, tilt = 0) {
c.save();
c.translate(x, y);
c.rotate(Math.max(-.45, Math.min(.45, tilt * .035)));
c.shadowColor = this.palette.green;
c.shadowBlur = 18;
c.fillStyle = this.palette.gold;
c.beginPath();
c.ellipse(-25, 4, 25, 10 + Math.sin(this.frame * .32) * 5, -.45, 0, Math.PI * 2);
c.ellipse(18, 5, 24, 10 + Math.cos(this.frame * .32) * 5, .45, 0, Math.PI * 2);
c.fill();
c.fillStyle = this.palette.green;
c.beginPath(); c.ellipse(0, 0, r + 8, r, 0, 0, Math.PI * 2); c.fill();
c.fillStyle = '#061016';
c.beginPath(); c.arc(9, -6, 4, 0, Math.PI * 2); c.fill();
c.strokeStyle = this.palette.violet;
c.lineWidth = 4;
c.beginPath(); c.moveTo(-18, -16); c.lineTo(-30, -29); c.moveTo(8, -17); c.lineTo(15, -31); c.stroke();
c.strokeStyle = this.palette.gold;
c.lineWidth = 3;
c.beginPath(); c.moveTo(-28, 2); c.quadraticCurveTo(-45, 8, -54, 0); c.stroke();
c.shadowBlur = 0;
c.restore();
}
car(c, x, y, color, w = 56, h = 86) {
c.shadowColor = color;
c.shadowBlur = 12;
c.fillStyle = color;
c.beginPath();
c.roundRect ? c.roundRect(x - w / 2, y - h / 2, w, h, 9) : c.rect(x - w / 2, y - h / 2, w, h);
c.fill();
c.shadowBlur = 0;
c.fillStyle = 'rgba(0,0,0,.32)';
c.fillRect(x - w * .34, y - h * .32, w * .68, h * .20);
c.fillRect(x - w * .34, y + h * .10, w * .68, h * .25);
c.fillStyle = '#ffd166';
c.fillRect(x - w * .42, y - h * .48, w * .22, h * .06);
c.fillRect(x + w * .20, y - h * .48, w * .22, h * .06);
}
pop(x, y, color = '#35f6a3', count = 10) {
for (let i = 0; i < count; i++) {
const a = Math.random() * Math.PI * 2;
const speed = 1.5 + Math.random() * 4;
this.effects.push({ x, y, vx: Math.cos(a) * speed, vy: Math.sin(a) * speed, life: 22 + Math.random() * 18, color });
}
}
drawEffects(c) {
this.effects.forEach((e) => {
c.globalAlpha = Math.max(0, e.life / 36);
c.fillStyle = e.color;
c.beginPath();
c.arc(e.x, e.y, 3 + e.life / 14, 0, Math.PI * 2);
c.fill();
});
c.globalAlpha = 1;
}
hud(c) {
c.fillStyle = 'rgba(0,0,0,.32)';
c.fillRect(16, 14, 230, 42);
c.fillStyle = '#35f6a3';
c.font = '800 18px system-ui';
c.fillText(`${this.type.toUpperCase()} ${this.score} BEST ${this.bestScore()}`, 28, 41);
}
gameOver() {
this.running = false;
this.bestByType[this.type] = Math.max(this.bestScore(), this.score);
localStorage.setItem('blackDragonGameBestByType', JSON.stringify(this.bestByType));
this.updateBestLabel();
const c = this.ctx;
c.fillStyle = 'rgba(0,0,0,.55)';
c.fillRect(0, 0, 960, 600);
c.fillStyle = '#35f6a3';
c.font = '700 52px system-ui';
c.textAlign = 'center';
c.fillText('Game Over', 480, 280);
c.font = '24px system-ui';
c.fillStyle = '#ffcf5a';
c.fillText(`Score ${this.score} | Best ${this.bestScore()}`, 480, 325);
c.fillStyle = '#eef7f2';
c.fillText('Click restart or press a game button', 480, 362);
c.textAlign = 'left';
}
bestScore() {
return Number(this.bestByType?.[this.type] || 0);
}
updateBestLabel() {
if ($('gameBest')) $('gameBest').textContent = 'Best: ' + this.bestScore();
}
updateGameStats() {
const box = $('gameStats');
if (!box || !this.state) return;
const s = this.state;
const stats = [];
if (this.type === 'race') {
stats.push(['Speed', `${Math.round((s.speed || 0) * 16)} km/h`], ['Gear', `G${s.gear || 1}`], ['Damage', `${s.damage || 0}/4`], ['Traffic', s.traffic?.length || 0]);
} else if (this.type === 'breaker') {
stats.push(['Lives', s.lives], ['Level', s.level], ['Crystals', (s.bricks || []).filter((b) => b.alive).length], ['Ball', s.ball?.stuck ? 'Ready' : 'Live']);
} else if (this.type === 'pong') {
stats.push(['You', s.player], ['Auto', s.ai], ['Ball', `${Math.abs(Math.round(s.ball?.vx || 0))}`], ['Goal', '3 points']);
} else if (this.type === 'snake') {
stats.push(['Length', s.snake?.length || 0], ['Direction', s.dir], ['Food', `${s.food?.x || 0},${s.food?.y || 0}`], ['Grid', '30 x 18']);
} else if (this.type === 'space') {
stats.push(['Bullets', s.bullets?.length || 0], ['Rocks', s.rocks?.length || 0], ['Charge', Math.max(0, s.cool || 0)], ['Mode', 'Shooter']);
} else if (this.type === 'catch') {
stats.push(['Gems', s.gems?.length || 0], ['Fire', s.fire?.length || 0], ['Shield', s.shield || 0], ['Mode', 'Catch']);
} else {
stats.push(['Pipes', s.pipes?.length || 0], ['Height', Math.round(s.y || 0)], ['Velocity', Math.round((s.vy || 0) * 10) / 10], ['Mode', 'Flap']);
}
box.innerHTML = stats.map(([label, value]) => `<div class="game-stat"><span>${label}</span><strong>${value}</strong></div>`).join('');
}
}
const ai = new BlackDragonAI();
const games = new DragonGames();
const airDraw = new AirDraw3D();
const notes = new NotesPad();
window.ai = ai;
window.games = games;
window.airDraw = airDraw;
window.notes = notes;
</script>
</body>
</html>