<!
DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>FanTube: Chaos in YouTube City</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<canvas id="gameCanvas"></canvas>
<div id="ui">
<div class="dir">
<button id="btn-left">◀</button>
<button id="btn-right">▶</button>
</div>
<div class="actions">
<button id="btn-jump">Pular</button>
<button id="btn-attack">Socar</button>
<button id="btn-dodge">Esquiva</button>
</div>
<div class="skills">
<button id="btn-skill1">Dash</button>
<button id="btn-skill2">Hand Likes</button>
<button id="btn-skill3">Dash Frenético</button>
<button id="btn-inventory">Inventário</button>
</div>
</div>
<script src="game.js"></script>
</body>
</html># arquivo: ai_api.py
from fastapi import FastAPI, Request
from pydantic import BaseModel
import random
app = FastAPI(title="MiniIA API 💡")
# Modelo de requisição
class Query(BaseModel):
pergunta: str
# Função "inteligente" simples
def gerar_resposta(texto: str) -> str:
respostas = [
"Hmmm, interessante 😏",
"Não tenho certeza, mas posso tentar! 🔥",
"Isso parece divertido! 🌟",
f"Você disse '{texto}', né? Que legal! 😜"
]
# Retorna uma resposta aleatória
return random.choice(respostas)
@app.post("/perguntar")
async def perguntar(query: Query):
resposta = gerar_resposta(query.pergunta)
return {"resposta": resposta}
# Roda com: uvicorn ai_api:app --reload/* Layout básico */
* { box-sizing: border-box; }
html,body { height:100%; margin:0; font-family: Inter, Arial, sans-serif; -webkit-
user-select:none; -ms-user-select:none; user-select:none; }
body { background: linear-gradient(180deg,#111020 0%, #1b1b2f 100%);
overflow:hidden; }
/* Canvas ocupa a tela */
#gameCanvas { display:block; width:100vw; height:100vh; background: linear-
gradient(180deg,#0b0b12 0%, #111 70%); }
/* UI mobile */
#ui {
position: absolute;
bottom: 12px;
left: 12px;
right: 12px;
display:flex;
justify-content:space-between;
align-items:center;
gap:8px;
pointer-events:none; /* para evitar bloquear o canvas; habilitamos nos buttons */
}
/* grupos */
#ui .dir, #ui .actions, #ui .skills {
display:flex; gap:8px;
pointer-events:auto;
}
/* botões */
#ui button {
min-width:64px;
padding:10px 12px;
border-radius:12px;
border:none;
background: linear-gradient(180deg,#ff66aa,#ff3388);
color:white;
font-weight:700;
font-size:14px;
box-shadow:0 6px 14px rgba(0,0,0,0.4);
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
/* mais responsivo em telas pequenas */
@media (max-width:420px) {
#ui button { min-width:54px; padding:8px 10px; font-size:13px; }
}/* FanTube: Chaos in YouTube City
Versão web inicial (HTML5 Canvas)
Arquivo: game.js
- Modular, expansível, mobile-friendly
*/
/* ----- Setup canvas ----- */
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
window.addEventListener('resize', resize);
resize();
/* ----- Utilidades ----- */
function clamp(v,a,b){ return Math.max(a,Math.min(b,v)); }
/* ----- Game state ----- */
const GAME = {
dt: 1/60,
gravity: 0.6,
speedModes: [1, 1.3, 1.9], // médio, rápido, hyper
speedIndex: 0,
slow: false,
map: null,
enemies: [],
bosses: [],
checkpoints: []
};
/* ----- Simple Tilemap / Rooms basic (metroidvania skeleton) ----- */
const MAP = {
width: 3000,
height: 900,
areas: ['YouTube City', 'Cavernas', 'Cyberspace', 'Espaço'],
camera: {x:0,y:0,w:canvas.width, h:canvas.height}
};
GAME.map = MAP;
/* ----- Player (FanTube) ----- */
const player = {
x: 150, y: 0,
w: 48, h: 64,
vx: 0, vy: 0,
speed: 4,
onGround: false,
facing: 1, // 1 right, -1 left
hp: 10,
maxHp: 10,
combo: {step:0, timer:0, cooldown:0.35},
dash: {cool:0, active:false},
abilities: {
dashDirected: true,
handLikes: false,
dashFrenzy: false,
stopTime: false,
hyperPunch: false,
speedClone: false
},
invincible: 0
};
/* Spawn example enemies */
function spawnEnemy(x,y,type='runner'){
const e = {
id: 'e'+Math.random().toString(36).slice(2,7),
x,y,w:40,h:40,
vx: (Math.random()>0.5?1:-1)*1,
type, hp:3, alive:true,
aiTimer:0
};
GAME.enemies.push(e);
}
/* Spawn some enemies across the map */
for(let i=0;i<8;i++){
spawnEnemy(400 + i*200, canvas.height-110, i%2? 'flyer':'runner');
}
/* ----- Controls ----- */
const keys =
{left:false,right:false,jump:false,attack:false,dodge:false,skill1:false,skill2:fal
se,skill3:false};
function bindBtn(id, onStart, onEnd){
const el = document.getElementById(id);
if(!el) return;
el.addEventListener('touchstart', e=>{ e.preventDefault(); onStart(); },
{passive:false});
el.addEventListener('mousedown', e=>{ e.preventDefault(); onStart(); });
el.addEventListener('touchend', e=>{ e.preventDefault(); onEnd(); },
{passive:false});
el.addEventListener('mouseup', e=>{ e.preventDefault(); onEnd(); });
}
/* movement left/right */
bindBtn('btn-left', ()=>keys.left=true, ()=>keys.left=false);
bindBtn('btn-right', ()=>keys.right=true, ()=>keys.right=false);
bindBtn('btn-jump', ()=>{ if(player.onGround) player.vy=-14; });
bindBtn('btn-attack', ()=>attackCombo());
bindBtn('btn-dodge', ()=>dodge());
bindBtn('btn-skill1', ()=>useDash());
bindBtn('btn-skill2', ()=>useHandLikes());
bindBtn('btn-skill3', ()=>useDashFrenzy());
bindBtn('btn-inventory', ()=>openInventory());
/* Keyboard support for desktop testing */
window.addEventListener('keydown', e=>{
if(e.key==='ArrowLeft') keys.left=true;
if(e.key==='ArrowRight') keys.right=true;
if(e.key==='ArrowUp') if(player.onGround) player.vy=-14;
if(e.key==='z') attackCombo();
if(e.key==='x') dodge();
if(e.key==='c') useDash();
if(e.key==='v') useHandLikes();
if(e.key==='b') useDashFrenzy();
});
window.addEventListener('keyup', e=>{
if(e.key==='ArrowLeft') keys.left=false;
if(e.key==='ArrowRight') keys.right=false;
});
/* ----- Game mechanics ----- */
/* Attack combo */
function attackCombo(){
if(player.combo.timer > 0) {
// continue combo
player.combo.step = (player.combo.step + 1) % 4;
} else {
player.combo.step = 0;
}
player.combo.timer = player.combo.cooldown;
// hitbox check on enemies
const range = 60;
GAME.enemies.forEach(e=>{
if(!e.alive) return;
const dx = (e.x + e.w/2) - (player.x + player.w/2);
const dy = Math.abs((e.y + e.h/2) - (player.y + player.h/2));
if(Math.abs(dx) < range && dy < 50){
e.hp -= 1 + (player.abilities.handLikes? 2:0); // handlikes stronger
e.vx = (dx>0)?2:-2;
if(e.hp <= 0) e.alive = false;
}
});
}
/* Dodge - timing can counter */
function dodge(){
player.invincible = 18; // frames
// if timed exactly, would trigger counter (not fully implemented)
// simple visual feedback:
playEffect('dodge', player.x, player.y);
}
/* Dash Directed */
function useDash(){
if(player.dash.cool > 0) return;
player.dash.cool = 90; // frames until reuse
player.dash.active = true;
GAME.slow = true;
// directional aiming: if user presses left/right quickly while dash active,
we'll use that
setTimeout(()=>{ /* placeholder for ending slow */ }, 200);
// implement immediate burst in update()
}
/* Hand Likes */
function useHandLikes(){
if(!player.abilities.handLikes) { /* locked: unlock later */
playEffect('locked'); return; }
// special heavy combo
player.combo.step = 3;
playEffect('handlikes', player.x, player.y);
}
/* Dash Frenzy */
function useDashFrenzy(){
if(!player.abilities.dashFrenzy) { playEffect('locked'); return; }
// set small loop of dashes that circle nearest enemy
playEffect('dashfrenzy', player.x, player.y);
}
/* Parar o tempo placeholder (unlock later) */
function stopTime(){
if(!player.abilities.stopTime) { playEffect('locked'); return; }
// freeze enemies for short time
GAME.enemies.forEach(e=> e.frozen = 45 );
GAME.slow = true;
setTimeout(()=>{ GAME.slow = false; }, 800);
}
/* Effects (simple) */
const effects = [];
function playEffect(name,x=0,y=0){
effects.push({name,x,y,t:30});
}
/* ----- Physics + update ----- */
function update(dt){
// dt fixed small step
// movement
const speedMul = GAME.speedModes[GAME.speedIndex] * (GAME.slow ? 0.45 : 1);
let vx = 0;
if(keys.left) { vx = -player.speed * speedMul; player.facing = -1; }
if(keys.right) { vx = player.speed * speedMul; player.facing = 1; }
player.vx = vx;
// dash active quick burst
if(player.dash.active){
player.vx = 18 * player.facing;
player.dash.active = false;
// short invulnerability
player.invincible = 20;
setTimeout(()=>{ GAME.slow = false; }, 150);
}
// apply vx/vy
player.x += player.vx;
player.vy += GAME.gravity;
player.y += player.vy;
// ground collision (simple)
const groundY = canvas.height - 80;
if(player.y + player.h >= groundY){
player.y = groundY - player.h;
player.vy = 0;
player.onGround = true;
} else player.onGround = false;
// combo timer tick
if(player.combo.timer > 0) player.combo.timer -= dt;
// dash cooldown tick
if(player.dash.cool > 0) player.dash.cool -= 1;
// invincible tick
if(player.invincible > 0) player.invincible -= 1;
// update enemies AI
for(let e of GAME.enemies){
if(!e.alive) continue;
if(e.frozen && e.frozen>0){ e.frozen -= 1; continue; }
e.aiTimer += dt;
// simple horizontal patrol
e.x += e.vx * (1 + (Math.sin(e.aiTimer*2)*0.2));
// gravity
if(e.y + e.h < groundY){ e.y += 2; } else e.y = groundY - e.h;
// collision with player
if(rectsOverlap(e, player) && player.invincible <= 0){
player.hp -= 1;
player.invincible = 40;
// knockback
player.vx = (player.x < e.x) ? -6 : 6;
}
}
// remove dead enemies (simple)
GAME.enemies = GAME.enemies.filter(e=> e.alive);
// keep camera following player
MAP.camera.x = clamp(player.x - canvas.width/2 + player.w/2, 0, MAP.width -
canvas.width);
MAP.camera.y = 0;
}
/* rectangle overlap helper */
function rectsOverlap(a,b){
return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
}
/* ----- Render ----- */
function render(){
// clear
ctx.clearRect(0,0,canvas.width,canvas.height);
// background (parallax simple)
ctx.fillStyle = '#071226';
ctx.fillRect(0,0,canvas.width,canvas.height);
// ground
ctx.fillStyle = '#222';
ctx.fillRect(0, canvas.height-80, canvas.width, 80);
// draw map bounds indicator
ctx.fillStyle = 'rgba(255,255,255,0.03)';
ctx.fillRect(-MAP.camera.x, 0, MAP.width, MAP.height);
// draw enemies
for(let e of GAME.enemies){
const ex = e.x - MAP.camera.x;
ctx.fillStyle = e.type==='flyer' ? '#ffdd55' : '#ff6666';
ctx.fillRect(ex, e.y, e.w, e.h);
// HP bar
ctx.fillStyle = '#222';
ctx.fillRect(ex, e.y-8, e.w, 6);
ctx.fillStyle = '#0f0';
ctx.fillRect(ex, e.y-8, e.w * clamp(e.hp/3,0,1), 6);
}
// player
const px = player.x - MAP.camera.x;
ctx.save();
// flash when invincible
if(player.invincible > 0 && Math.floor(player.invincible/6)%2===0)
ctx.globalAlpha = 0.4;
ctx.fillStyle = '#00ffcc';
ctx.fillRect(px, player.y, player.w, player.h);
ctx.restore();
// effects
for(let i=effects.length-1;i>=0;i--){
const ef = effects[i];
ef.t--;
if(ef.t<=0) effects.splice(i,1);
else{
const ex = ef.x - MAP.camera.x;
ctx.fillStyle = 'rgba(255,255,255,0.8)';
ctx.fillRect(ex-8, ef.y-8, 16, 16);
}
}
// HUD: HP
ctx.fillStyle = 'white';
ctx.font = '18px Arial';
ctx.fillText(`HP: ${player.hp}/${player.maxHp}`, 18, 28);
ctx.fillText(`Area: ${MAP.areas[0]}`, 18, 52);
}
/* ----- Inventory and UI features ----- */
let inventoryOpen = false;
function openInventory(){
inventoryOpen = !inventoryOpen;
if(inventoryOpen) {
GAME.slow = true;
alert('Inventário aberto (exemplo). Aqui você poderia equipar habilidades.
Fecha pra continuar.');
GAME.slow = false;
inventoryOpen = false;
}
}
/* ----- Game loop ----- */
let last = performance.now();
function loop(ts){
const delta = (ts - last) / 1000;
last = ts;
// we step fixed dt for stability
const step = 1/60;
let accumulator = delta;
while(accumulator > 0){
update(step);
accumulator -= step;
}
render();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
/* ----- Helper: spawn more enemies dynamically for demo ----- */
setInterval(()=>{
const x = clamp(player.x + canvas.width * (0.6 + Math.random()*0.6), 200,
MAP.width-200);
spawnEnemy(x, canvas.height - 120, Math.random()>0.6? 'flyer':'runner');
}, 4000);
/* ----- End of game.js ----- */