🎮ArcadeLab

Remix of Spedfish — Auto Fish Play 3D

by MegaFlare51
2759 lines42.5 KB
▶ Play
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<title>Spedfish — Auto Fish Play 3D</title>

<style>
html,body{
  margin:0;width:100%;height:100%;overflow:hidden;
  font-family:Arial;background:#061522;color:#fff
}
*{box-sizing:border-box}
button{
  border:1px solid #38c8ff88;background:#092c43ef;color:#fff;
  border-radius:14px;padding:13px 18px;font-weight:800;
  touch-action:none
}
.screen{position:fixed;inset:0}
.hide{display:none!important}

#loading{
  z-index:50;
  background:linear-gradient(135deg,#090909,#252525 45%,#0b0b0b);
  display:flex;flex-direction:column;align-items:center;
  justify-content:center;transition:opacity 1.5s
}
#loading h1{
  font-size:42px;margin:0 0 25px;color:#eee;
  text-shadow:2px 2px #111,0 0 18px #ff1730;
  letter-spacing:5px
}
.bar{
  width:min(78vw,480px);height:18px;
  border:3px solid #777;border-radius:8px;
  overflow:visible;position:relative;
  padding:3px;background:linear-gradient(180deg,#666,#292929);
  box-shadow:inset 0 3px 8px #111,0 0 15px #ff1730
}
.fill{
  height:100%;width:0;border-radius:4px;
  background:linear-gradient(90deg,#ff1028,#ff4a55,#ff1028);
  box-shadow:0 0 16px #ff1530
}
.loadtxt{
  margin-top:12px;color:#ff9aa4;
  text-shadow:0 0 8px #ff1f36
}
.loadparticles{
  position:absolute;inset:0;pointer-events:none;overflow:hidden
}
.loadparticles:before,
.loadparticles:after{
  content:"✦ • ◆ · ✦ • ◆ · ✦ • ◆";
  position:absolute;left:-20%;width:140%;
  font-size:18px;letter-spacing:35px;color:#ff253b;
  text-shadow:0 0 10px #ff001f;
  animation:particles 3s linear infinite;opacity:.75
}
.loadparticles:before{top:42%;animation-duration:2.4s}
.loadparticles:after{
  top:54%;animation-duration:3.2s;
  animation-direction:reverse;font-size:13px
}
@keyframes particles{
  from{transform:translateX(-10%) translateY(0);opacity:.1}
  50%{opacity:1}
  to{transform:translateX(10%) translateY(-22px);opacity:.05}
}

#fade{
  z-index:49;background:#000;opacity:0;
  pointer-events:none;transition:opacity 1.5s
}

#lobby{
  background:linear-gradient(135deg,#171717,#3a3a3a 45%,#111);
  overflow:hidden
}
#lobby:before{
  content:"⌁ SYSTEM ONLINE ⌁ // AUTO FISH PLAY // ⌁ SYSTEM ONLINE ⌁";
  position:absolute;top:17px;left:0;width:100%;
  text-align:center;font-size:11px;color:#ff5963;
  letter-spacing:3px;z-index:11
}
#lobby:after{
  content:"";position:absolute;inset:10px;
  border:4px solid #777;border-radius:16px;
  box-shadow:inset 0 0 50px #000,0 0 18px #ff1830;
  pointer-events:none;z-index:10
}
#lobby .lobby-bg{
  position:absolute;inset:0;
  background:
    repeating-linear-gradient(0deg,#0000 0 38px,#ffffff08 39px 40px),
    linear-gradient(135deg,#4a4a4a,#161616);
  border:6px solid #777;
  box-shadow:inset 0 0 70px #000
}
#lobby .lobby-bg:after{
  content:'◈ ◇ ◈ ◇ ◈ ◇ ◈';
  position:absolute;bottom:8px;left:0;width:100%;
  text-align:center;font-size:28px;color:#ff2633;
  opacity:.28;letter-spacing:12px
}

.logo{
  position:absolute;top:42px;width:100%;
  text-align:center;font-size:42px;font-weight:1000;
  color:#fff;text-shadow:0 0 8px #ff2738,0 0 28px #ff001f;
  letter-spacing:4px
}
.logo:before{
  content:'◉ CORE // FISHING NETWORK';
  display:block;font-size:10px;letter-spacing:5px;
  color:#ff5963;margin-bottom:7px
}
.logo:after{
  content:'ADVENTURE SYSTEM • v1.0 OFICIAL • ONLINE';
  display:block;font-size:11px;letter-spacing:4px;
  color:#ff7b82;margin-top:7px
}

.level{
  position:absolute;top:75px;left:50%;
  transform:translateX(-50%);
  padding:9px 20px;border-radius:30px;
  background:#09090cf2;border:1px solid #ff304455;
  box-shadow:0 0 18px #ff001f22
}
.level b{color:#ff5963}

.coins{
  position:absolute;right:20px;top:20px;
  background:#09090cf2;border:1px solid #ff304455;
  padding:10px 14px;border-radius:14px
}
.coins b{color:#ff5963}

.menu{
  position:absolute;top:50%;
  transform:translateY(-50%);
  display:flex;flex-direction:column;gap:12px;
  z-index:12
}
.left{left:20px}
.right{right:20px}
.menu button{
  background:linear-gradient(135deg,#21070b,#100b0d);
  border:1px solid #ff344866;
  box-shadow:0 0 18px #ff001f22,inset 0 0 20px #ff001f0d;
  border-radius:12px;position:relative;overflow:hidden
}
.menu button:before{
  content:'//';color:#ff3948;margin-right:9px
}
.play{
  font-size:28px;padding:24px 35px;
  background:linear-gradient(135deg,#ff2838,#8f0613)!important;
  border-color:#ff7a82!important;
  box-shadow:0 0 30px #ff203366,0 8px 0 #4b050b!important
}

.lobby-scene{
  position:absolute;left:50%;top:50%;
  transform:translate(-50%,-50%) scale(.82);
  width:360px;height:260px;
  z-index:3;filter:drop-shadow(0 0 24px #ff1d3044)
}
.lobby-island{
  position:absolute;left:50%;top:60%;
  transform:translate(-50%,-50%);
  width:330px;height:120px;border-radius:50%;
  background:radial-gradient(circle,#4c1117,#16080a);
  border:2px solid #ff354455;
  box-shadow:0 0 35px #ff102522,inset 0 0 30px #ff00151a
}

.lobby-char{
  position:absolute;left:50%;top:40%;
  transform:translate(-50%,-50%);
  width:60px;height:130px
}
.lc-head{
  position:absolute;left:10px;top:0;
  width:40px;height:40px;background:#e2a071;
  border-radius:9px
}
.lc-hair{
  position:absolute;left:8px;top:-5px;
  width:44px;height:15px;background:#20252c;
  border-radius:8px 8px 2px 2px;z-index:2
}
.lc-body{
  position:absolute;left:7px;top:40px;
  width:46px;height:50px;
  background:#168ce0;border-radius:7px
}
.lc-arm{
  position:absolute;top:43px;
  width:12px;height:43px;
  background:#e2a071;border-radius:7px
}
.lca{left:-7px;transform:rotate(12deg)}
.lcb{right:-7px;transform:rotate(-12deg)}
.lc-leg{
  position:absolute;top:87px;
  width:15px;height:43px;background:#26394a;
  border-radius:5px
}
.lcl{left:9px}
.lcr{right:9px}
.lc-rod{
  position:absolute;right:-15px;top:35px;
  width:4px;height:90px;
  background:#9b6b38;transform:rotate(-20deg)
}

.modal{
  z-index:40;background:#000b;
  display:flex;align-items:center;
  justify-content:center;padding:14px
}
.panel{
  width:min(950px,96vw);max-height:90vh;
  overflow:auto;background:#09263a;
  border:1px solid #42cfff55;border-radius:20px;
  padding:20px
}
.close{float:right;padding:7px 11px}

.skins{
  display:grid;grid-template-columns:repeat(5,1fr);gap:10px
}
.skin{
  background:#0d3850;border-radius:14px;
  padding:8px;text-align:center
}
.avatar{height:110px;position:relative}
.ah{
  position:absolute;left:50%;top:5px;
  transform:translateX(-50%);
  width:42px;height:42px;background:#e2a071;
  border-radius:9px
}
.ab{
  position:absolute;left:50%;top:47px;
  transform:translateX(-50%);
  width:48px;height:49px;border-radius:7px
}
.al{
  position:absolute;left:calc(50% - 23px);
  top:95px;width:46px;height:13px;
  background:#243646
}
.skin button{width:100%;padding:8px;margin-top:7px}
.owned{background:#149553!important}

.setting{margin:25px 0}
.setting label{
  display:flex;justify-content:space-between;
  margin-bottom:8px
}
input[type=range]{
  width:100%;accent-color:#10a9ef
}

#game{background:#000}
#game canvas{
  position:absolute;inset:0
}

/* NÍVEL PEQUENO NO JOGO */
.gameLevel{
  position:absolute;z-index:7;top:8px;left:50%;
  transform:translateX(-50%);
  background:#09090cf5;
  border:1px solid #ff304455;
  box-shadow:0 0 12px #ff001f22;
  color:#fff;padding:4px 10px;
  border-radius:10px;font-size:12px;
  font-weight:900;text-align:center;
  min-width:110px
}
.gameLevel b{color:#ff5963}
.xpbar{
  width:120px;height:6px;margin-top:3px;
  background:#16070a;border:1px solid #ff304455;
  border-radius:99px;overflow:hidden
}
.xpfill{
  height:100%;width:0;
  background:linear-gradient(90deg,#ff1028,#ff5963);
  box-shadow:0 0 7px #ff2038;
  transition:width .25s
}
.xptext{font-size:7px;opacity:.75}

.hud{
  position:absolute;z-index:5;top:10px;left:10px;
  background:#03111dcc;padding:10px 13px;
  border-radius:13px
}
.hud b{color:#63dcff}

.gamebtn{
  position:absolute;z-index:6;
  top:10px;right:10px
}

.controls{
  position:absolute;z-index:6;inset:0;
  pointer-events:none
}
.move{
  position:absolute;bottom:16px;left:16px;
  display:grid;
  grid-template-columns:58px 58px 58px;
  grid-template-rows:58px 58px;
  gap:5px;pointer-events:auto
}
.move button{padding:0;font-size:22px}
.up{grid-column:2}
.leftb{grid-column:1;grid-row:2}
.down{grid-column:2;grid-row:2}
.rightb{grid-column:3;grid-row:2}

.actions{
  position:absolute;right:15px;bottom:20px;
  display:flex;gap:8px;pointer-events:auto
}
.cast{
  width:82px;height:82px;
  border-radius:50%;background:#079fe5
}

.hint{
  position:absolute;bottom:5px;left:50%;
  transform:translateX(-50%);
  font-size:11px;white-space:nowrap
}

.catchMsg{
  position:absolute;z-index:20;
  left:50%;top:22%;
  transform:translateX(-50%);
  font-size:34px;font-weight:1000;
  background:#09263aee;padding:16px 28px;
  border:2px solid #55dcff;border-radius:20px;
  box-shadow:0 0 35px #00bfff66
}
.clickCatch{
  position:absolute;z-index:21;
  left:50%;bottom:18%;
  transform:translateX(-50%);
  font-size:24px;font-weight:1000;
  background:#08a8ee;
  border:2px solid #b9f2ff;
  border-radius:18px;
  padding:16px 30px;
  box-shadow:0 8px 25px #0008;
  cursor:pointer
}

.shopHint{
  pointer-events:auto;
  position:absolute;left:50%;top:18%;
  transform:translateX(-50%);
  background:#0b3650ef;
  border:2px solid #42cfff;
  border-radius:18px;
  padding:12px 22px;
  text-align:center;font-weight:900;
  box-shadow:0 8px 30px #0008;
  cursor:pointer
}
.shopHint small,.sellHint small{
  font-weight:600;opacity:.8
}

.sellHint{
  pointer-events:auto;
  position:absolute;left:50%;top:32%;
  transform:translateX(-50%);
  background:#174a35ee;
  border:2px solid #55ff9b;
  border-radius:18px;
  padding:12px 22px;
  text-align:center;font-weight:900;
  box-shadow:0 8px 30px #0008
}

.rods{
  display:grid;grid-template-columns:repeat(4,1fr);
  gap:12px
}
.rodcard{
  background:#0d3850;
  border:1px solid #42cfff33;
  border-radius:16px;padding:14px;
  text-align:center
}
.rodicon{font-size:42px}
.rodcard button{
  width:100%;margin-top:10px
}
.rodcard.owned{background:#146d4b}

@media(max-width:700px){
  .rods{grid-template-columns:repeat(2,1fr)}
}
@media(max-width:650px){
  .logo{font-size:25px;top:40px}
  .logo:before{font-size:8px}
  .logo:after{font-size:8px}
  .skins{grid-template-columns:repeat(2,1fr)}
  .left{left:8px}
  .right{right:8px}
  .menu button{padding:11px}
  .coins{top:65px}
}
@media(max-width:850px) and (orientation:landscape){
  .logo{top:34px}
  .menu{top:55%}
  .lobby-scene{transform:translate(-50%,-50%) scale(.82)}
}
@media(orientation:portrait){
  #loading:after,#lobby:after{
    content:"GIRE O CELULAR DE LADO ↔";
    display:flex;align-items:flex-end;
    justify-content:center;padding-bottom:18px;
    color:#ff5963;font-size:14px;
    letter-spacing:2px;text-shadow:0 0 8px #ff1730
  }
}
</style>
</head>

<body>

<div id="loading" class="screen">
  <div class="loadparticles"></div>
  <h1>Spedfish</h1>
  <div class="bar">
    <div id="fill" class="fill"></div>
  </div>
  <div id="loadtxt" class="loadtxt">Carregando 0%</div>
</div>

<div id="fade" class="screen"></div>

<div id="lobby" class="screen hide">
  <div class="lobby-bg">

    <div class="lobby-title">🎣 SPEDFISH</div>
    <div class="lobby-sub">UMA AVENTURA COMEÇA AQUI</div>

    <div class="lobby-scene">
      <div class="lobby-island"></div>

      <div class="lobby-char">
        <div class="lc-hair"></div>
        <div class="lc-head"></div>
        <div class="lc-body"></div>
        <div class="lc-arm lca"></div>
        <div class="lc-arm lcb"></div>
        <div class="lc-leg lcl"></div>
        <div class="lc-leg lcr"></div>
        <div class="lc-rod"></div>
      </div>
    </div>

  </div>

  <div class="logo">🎣 SPEDFISH</div>

  <div class="level">
    Nível <b id="level">1</b>
  </div>

  <div class="coins">
    💰 <b id="lobbyCoins">0</b>
  </div>

  <div class="menu left">
    <button onclick="openSkins()">👕 SKINS</button>
    <button onclick="openSettings()">⚙️ CONFIGURAÇÕES</button>
    <button onclick="openEvents()">🎉 EVENTOS</button>
  </div>

  <div class="menu right">
    <button class="play" onclick="play()">▶ JOGAR</button>
  </div>
</div>

<div id="modal" class="screen modal hide">
  <div id="panel" class="panel"></div>
</div>

<div id="game" class="screen hide">

  <div id="catchMsg" class="catchMsg hide">
    🎣 PESCOUUU!
  </div>

  <div id="clickCatch" class="clickCatch hide">
    CLICAR <b id="clickCount">0</b>/10
  </div>

  <div class="gameLevel">
    ⭐ NÍVEL <b id="gameLevel">1</b>
    <div class="xpbar">
      <div id="xpFill" class="xpfill"></div>
    </div>
    <div class="xptext" id="xpText">0 / 500 XP</div>
  </div>

  <div class="hud">
    🐟 Peixes: <b id="fish">0</b>
    · ⭐ XP: <b id="xpHud">0</b>
    · 💰 <b id="coins">0</b>
    · 🎣 Isca: <b id="bait">3</b>
  </div>

  <button class="gamebtn" onclick="backLobby()">
    ☰ MENU
  </button>

  <div class="controls">

    <div class="move">
      <button class="up" data-key="w">▲</button>
      <button class="leftb" data-key="a">◀</button>
      <button class="down" data-key="s">▼</button>
      <button class="rightb" data-key="d">▶</button>
    </div>

    <div class="actions">
      <button onclick="
        if(nearShop())openShop('peixes');
        else if(nearBoatShop())openBoatShop();
        else alert('Chegue perto de uma loja.')
      ">🏪 LOJA</button>

      <button class="cast" onclick="cast()">🎣</button>
    </div>

    <div id="shopHint" class="shopHint hide"
         onclick="
         if(nearShop())openShop('peixes');
         else if(nearBoatShop())openBoatShop()
         ">
      🏪 LOJA<br>
      <small>Toque para abrir</small>
    </div>

    <div class="hint">
      Arraste o dedo na tela para girar a câmera
    </div>

  </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>

<script>

const $=id=>document.getElementById(id);

let coins=0,
fish=0,
bait=3,
level=1,
xp=0;

let fishInv={
  kling:0,
  park:0,
  nickgroud:0
};

let enchInv={
  kling:0,
  park:0,
  nickgroud:0
};

let owned=[0];
let equipped=0;
let quality=75;
let sensitivity=50;

let keys={},
scene,
camera,
renderer,
player,
parts=[],
yaw=0,
pitch=.22,
fishObjs=[],
started=false,
rod=0,
ownedRods=[0],
shopOpen=false,
catching=false,
clicks=0,
fishHeld=null;

function sync(){

  level=Math.min(2500,1+Math.floor(xp/500));

  ['coins','lobbyCoins'].forEach(id=>{
    $(id).textContent=coins;
  });

  $('fish').textContent=fish;
  $('bait').textContent=bait;

  if($('level'))
    $('level').textContent=level;

  if($('gameLevel'))
    $('gameLevel').textContent=level;

  if($('xpHud'))
    $('xpHud').textContent=xp;

  let cur=xp%500;

  let pct=level>=2500
    ?100
    :cur/5;

  if($('xpFill'))
    $('xpFill').style.width=pct+'%';

  if($('xpText'))
    $('xpText').textContent=
      level>=2500
      ?'MÁXIMO — 2500'
      :cur+' / 500 XP';
}

let startTime=performance.now();

function loading(){

  let t=Math.min(
    7000,
    performance.now()-startTime
  );

  let v=t/7000*100;

  $('fill').style.width=v+'%';
  $('loadtxt').textContent=
    'Carregando '+Math.floor(v)+'%';

  if(t<7000){

    requestAnimationFrame(loading);

  }else{

    setTimeout(()=>{

      $('fade').style.opacity=1;

      setTimeout(()=>{

        $('loading').classList.add('hide');
        $('fade').style.opacity=0;
        $('lobby').classList.remove('hide');

      },3000);

    },150);

  }
}

requestAnimationFrame(loading);

function show(html){

  $('panel').innerHTML=html;
  $('modal').classList.remove('hide');

}

function closeModal(){

  $('modal').classList.add('hide');
  shopOpen=false;

}

function openEvents(){

  show(`
    <button class="close" onclick="closeModal()">✕</button>
    <h2>🎉 Eventos</h2>
    <h3>Sem eventos disponíveis</h3>
  `);

}

function nearShop(){

  return player &&
    player.position.distanceTo(
      new THREE.Vector3(0,1,-82)
    )<11;

}

function nearBoatShop(){

  return player &&
    player.position.distanceTo(
      new THREE.Vector3(78,1,-82)
    )<11;

}

function openShop(tab='peixes'){

  shopOpen=true;

  let h=`
    <button class="close"
      onclick="closeModal()">✕</button>

    <h2>🏪 LOJA</h2>

    <p>Escolha uma categoria:</p>

    <div style="
      display:flex;
      gap:10px;
      margin:14px 0;
      flex-wrap:wrap">

      <button onclick="openShop('peixes')" style="flex:1">
        🐟 PEIXES
      </button>

      <button onclick="openShop('varas')" style="flex:1">
        🎣 VARAS
      </button>

      <button onclick="openShop('iscas')" style="flex:1">
        🪱 ISCAS
      </button>

    </div>
  `;

  if(tab==='iscas'){

    let packs=[
      [1,1],
      [10,2],
      [50,10],
      [100,30],
      [200,40],
      [1000,100],
      [10000,1000]
    ];

    h+=`
      <h3>🪱 Iscas</h3>

      <p>
        Você começa com <b>3 iscas</b>.
        Atualmente: <b>${bait}</b>
      </p>

      <div class="rods">
    `;

    packs.forEach(p=>{

      h+=`
        <div class="rodcard">

          <div class="rodicon">🪱</div>

          <h3>
            ${p[0].toLocaleString('pt-BR')}
            isca${p[0]===1?'':'s'}
          </h3>

          <b>
            R$ ${p[1].toLocaleString('pt-BR')}
          </b>

          <button onclick="buyBait(${p[0]},${p[1]})">
            COMPRAR
          </button>

        </div>
      `;

    });

    h+='</div>';

  }

  else if(tab==='peixes'){

    let n=fishInv.kling;
    let c=fishInv.park;
    let k=fishInv.nickgroud;

    let totalMoney=
      n*50+
      c*100+
      k*200+
      enchInv.kling*200+
      enchInv.park*300+
      enchInv.nickgroud*500;

    h+=`
      <h3>🐟 Peixes</h3>

      <div class="rodcard">

        <div>
          🐟 Kling:
          <b>${n}</b>
          · R$50 · ✨ R$200
        </div>

        <div>
          🐠 Park:
          <b>${c}</b>
          · R$100 · ✨ R$300
        </div>

        <div>
          🐡 Nickgroud:
          <b>${k}</b>
          · R$200 · ✨ R$500
        </div>

        <hr>

        <h3>
          ✨ Encantados:
          ${enchInv.kling+
            enchInv.park+
            enchInv.nickgroud}

          · Total:
          ${fish} peixes
        </h3>

        <p>
          Valor total:
          <b>
            R$ ${totalMoney.toLocaleString('pt-BR')}
          </b>
        </p>

        <button onclick="sellFish()">
          VENDER TUDO
        </button>

      </div>
    `;

  }

  else{

    let rods=[
      ['Madeira',0,'🎣'],
      ['Plástico',300,'🪄'],
      ['Ferro',600,'⚙️'],
      ['Nikricom',1000,'✨']
    ];

    h+=`
      <h3>🎣 Varas</h3>
      <div class="rods">
    `;

    rods.forEach((r,i)=>{

      let own=ownedRods.includes(i);

      h+=`
        <div class="rodcard">

          <div class="rodicon">${r[2]}</div>

          <h3>${r[0]}</h3>

          <b>
            ${r[1]===0
              ?'GRÁTIS'
              :'R$ '+r[1].toLocaleString('pt-BR')}
          </b>

          <button
            class="${own?'owned':''}"
            onclick="buyRod(${i})">

            ${
              own
              ?(rod===i?'EQUIPADA':'EQUIPAR')
              :(r[1]===0?'PEGAR GRÁTIS':'COMPRAR')
            }

          </button>

        </div>
      `;

    });

    h+='</div>';

  }

  show(h);

}

function buyBait(qty,price){

  if(coins<price){

    alert('Dinheiro insuficiente.');
    return;

  }

  coins-=price;
  bait+=qty;

  sync();
  openShop('iscas');

}

function buyRod(i){

  let prices=[
    0,
    300,
    600,
    1000
  ];

  let names=[
    'Madeira',
    'Plástico',
    'Ferro',
    'Nikricom'
  ];

  if(ownedRods.includes(i)){

    rod=i;
    sync();
    openShop('varas');
    return;

  }

  if(coins<prices[i]){

    alert(
      'Você precisa de R$ '+
      prices[i].toLocaleString('pt-BR')+'.'
    );

    return;

  }

  coins-=prices[i];
  ownedRods.push(i);
  rod=i;

  sync();
  openShop('varas');

}

function sellFish(){

  let money=
    fishInv.kling*50+
    fishInv.park*100+
    fishInv.nickgroud*200+
    enchInv.kling*200+
    enchInv.park*300+
    enchInv.nickgroud*500;

  if(!fish){

    alert('Você não tem peixes para vender.');
    return;

  }

  coins+=money;

  fish=0;

  fishInv={
    kling:0,
    park:0,
    nickgroud:0
  };

  enchInv={
    kling:0,
    park:0,
    nickgroud:0
  };

  sync();

  alert(
    'Peixes vendidos por R$ '+
    money.toLocaleString('pt-BR')+'!'
  );

  openShop('peixes');

}

function openBoatShop(){

  shopOpen=true;

  let boats=[
    ['Canoa',7000,'🛶'],
    ['Barquinho',10000,'🚤'],
    ['Navio',14000,'🚢']
  ];

  let h=`
    <button class="close"
      onclick="closeModal()">✕</button>

    <h2>🚤 LOJA DE BARCOS</h2>

    <p>Escolha seu barco:</p>

    <div class="rods">
  `;

  boats.forEach((b,i)=>{

    h+=`
      <div class="rodcard">

        <div class="rodicon">${b[2]}</div>

        <h3>${b[0]}</h3>

        <b>
          R$ ${b[1].toLocaleString('pt-BR')}
        </b>

        <button onclick="buyBoat(${i})">
          COMPRAR
        </button>

      </div>
    `;

  });

  h+='</div>';

  show(h);

}

function buyBoat(i){

  let prices=[
    7000,
    10000,
    14000
  ];

  let names=[
    'Canoa',
    'Barquinho',
    'Navio'
  ];

  if(coins<prices[i]){

    alert(
      'Você precisa de R$ '+
      prices[i].toLocaleString('pt-BR')+
      ' para comprar '+names[i]+'.'
    );

    return;

  }

  coins-=prices[i];

  sync();

  alert(names[i]+' comprado!');

  openBoatShop();

}

function openSettings(){

  show(`
    <button class="close"
      onclick="closeModal()">✕</button>

    <h2>⚙️ Configurações</h2>

    <div class="setting">

      <label>
        Qualidade
        <b id="qv">${quality}%</b>
      </label>

      <input
        type="range"
        min="0"
        max="100"
        value="${quality}"
        oninput="
          quality=+this.value;
          qv.textContent=this.value+'%';
          applyQuality()
        "
      >

    </div>

    <div class="setting">

      <label>
        Sensibilidade
        <b id="sv">${sensitivity}%</b>
      </label>

      <input
        type="range"
        min="0"
        max="100"
        value="${sensitivity}"
        oninput="
          sensitivity=+this.value;
          sv.textContent=this.value+'%'
        "
      >

    </div>

    <button onclick="closeModal()">
      SALVAR
    </button>
  `);

}

const colors=[
  '#168ce0',
  '#e44d65',
  '#814de0',
  '#16a36a',
  '#e09b2d',
  '#f04aa0',
  '#14a6a6',
  '#73452b',
  '#e7e7e7',
  '#222'
];

function avatar(i){

  return `
    <div class="avatar">
      <div class="ah"></div>
      <div
        class="ab"
        style="background:${colors[i%colors.length]}">
      </div>
      <div class="al"></div>
    </div>
  `;

}

function openSkins(){

  let s=`
    <button class="close"
      onclick="closeModal()">✕</button>

    <h2>👕 Skins</h2>

    <p>
      20 personagens 3D · preço:
      2.000 moedas cada
    </p>

    <div class="skins">
  `;

  for(let i=0;i<20;i++){

    let own=owned.includes(i);

    s+=`
      <div class="skin">

        ${avatar(i)}

        <b>Skin ${i+1}</b>

        <button
          class="${own?'owned':''}"
          onclick="chooseSkin(${i})">

          ${
            own
            ?(equipped===i?'EQUIPADO':'EQUIPAR')
            :'2.000 💰'
          }

        </button>

      </div>
    `;

  }

  show(s+'</div>');

}

function chooseSkin(i){

  if(!owned.includes(i)){

    if(coins<2000){

      alert(
        'Você precisa de 2.000 moedas.'
      );

      return;

    }

    coins-=2000;
    owned.push(i);

  }

  equipped=i;

  sync();

  openSkins();

  if(player)
    changeSkin();

}

function play(){

  closeModal();

  $('lobby').classList.add('hide');
  $('game').classList.remove('hide');

  if(!started){

    started=true;
    init();

  }else{

    sync();

  }

}

function backLobby(){

  $('game').classList.add('hide');
  $('lobby').classList.remove('hide');

}

function mat(c){

  return new THREE.MeshStandardMaterial({
    color:c
  });

}

function cube(s,c){

  let m=new THREE.Mesh(
    new THREE.BoxGeometry(...s),
    mat(c)
  );

  return m;

}

function makeTree(x,z){

  let trunk=cube(
    [.8,5,.8],
    0x79502e
  );

  trunk.position.set(
    x,2.5,z
  );

  scene.add(trunk);

  for(let i=0;i<4;i++){

    let leaf=new THREE.Mesh(
      new THREE.ConeGeometry(
        2.1,3.5,5
      ),
      mat(0x159b47)
    );

    leaf.position.set(
      x+(i%2?1:-1)*.8,
      5.2+(i*.35),
      z+(i<2?-.5:.5)
    );

    leaf.rotation.z=
      (i%2?-.35:.35);

    scene.add(leaf);

  }

}

function makePlayer(){

  player=new THREE.Group();
  scene.add(player);

  let body=cube(
    [1.15,1.5,.72],
    colors[equipped%colors.length]
  );

  body.position.y=2;
  body.userData.body=true;
  player.add(body);

  let head=cube(
    [.9,.9,.9],
    0xe2a071
  );

  head.position.y=3.2;
  player.add(head);

  let hair=cube(
    [.94,.25,.94],
    0x20252c
  );

  hair.position.y=3.7;
  player.add(hair);

  for(let side of [-1,1]){

    let arm=cube(
      [.35,1.25,.4],
      0xe2a071
    );

    arm.position.set(
      side*.78,2,0
    );

    arm.userData.arm=side;
    player.add(arm);
    parts.push(arm);

    let leg=cube(
      [.38,1.25,.45],
      0x26394a
    );

    leg.position.set(
      side*.3,.72,0
    );

    leg.userData.leg=side;
    player.add(leg);
    parts.push(leg);

  }

  let handRod=cube(
    [.12,2.5,.12],
    0x9b6b38
  );

  handRod.position.set(
    .95,2.25,.15
  );

  handRod.rotation.z=-.35;
  handRod.userData.handRod=true;

  player.add(handRod);
  parts.push(handRod);

}

function nearBridge(){

  if(!player)
    return false;

  let x=player.position.x;
  let z=player.position.z;

  return(
    (z<-82&&Math.abs(x)<12)||
    (z>82&&Math.abs(x)<12)||
    (x>82&&Math.abs(z)<12)||
    (x<-82&&Math.abs(z)<12)
  );

}

function makeRealFish(
  color=0xff3030,
  species='Lmabreia',
  size=1
){

  const g=new THREE.Group();

  const bodyScale=
    species==='nickgroud'
    ?1.3
    :species==='park'
    ?1.1
    :.98;

  const bodyMat=
    new THREE.MeshStandardMaterial({
      color,
      roughness:.25,
      metalness:.04
    });

  const bellyColor=
    species==='kling'
    ?0xffb8b8
    :species==='park'
    ?0xb9d7ff
    :0xffd0a0;

  const bellyMat=
    new THREE.MeshStandardMaterial({
      color:bellyColor,
      roughness:.32
    });

  const finMat=
    new THREE.MeshStandardMaterial({
      color:0xf8f3e8,
      transparent:true,
      opacity:.82,
      roughness:.35,
      side:THREE.DoubleSide
    });

  const darkFinMat=
    new THREE.MeshStandardMaterial({
      color:0x7c2630,
      roughness:.3,
      side:THREE.DoubleSide
    });

  const eyeWhite=
    new THREE.MeshStandardMaterial({
      color:0xffffff,
      roughness:.18
    });

  const eyeBlack=
    new THREE.MeshStandardMaterial({
      color:0x090b10,
      roughness:.08
    });

  const body=new THREE.Mesh(
    new THREE.SphereGeometry(.62,32,20),
    bodyMat
  );

  body.scale.set(
    1.55*bodyScale,
    .74*bodyScale,
    .52*bodyScale
  );

  g.add(body);
  g.userData.body=body;

  const belly=new THREE.Mesh(
    new THREE.SphereGeometry(.625,28,16),
    bellyMat
  );

  belly.scale.set(
    1.25*bodyScale,
    .48*bodyScale,
    .525*bodyScale
  );

  belly.position.set(
    .12,
    -.12*bodyScale,
    0
  );

  g.add(belly);

  const stripeMat=
    new THREE.MeshStandardMaterial({
      color:0xffffff,
      transparent:true,
      opacity:.24,
      roughness:.45
    });

  const stripe=new THREE.Mesh(
    new THREE.BoxGeometry(
      1.65*bodyScale,
      .055*bodyScale,
      .025*bodyScale
    ),
    stripeMat
  );

  stripe.position.set(
    -.02,
    .03*bodyScale,
    .505*bodyScale
  );

  g.add(stripe);

  for(let i=0;i<7;i++){

    const scale=new THREE.Mesh(
      new THREE.SphereGeometry(
        .045*bodyScale,
        8,6
      ),
      stripeMat
    );

    scale.scale.z=.25;

    scale.position.set(
      (i-3)*.32*bodyScale,
      .04*bodyScale,
      .515*bodyScale
    );

    g.add(scale);

  }

  const tail=new THREE.Group();

  tail.position.x=
    -1.05*bodyScale;

  g.add(tail);
  g.userData.tail=tail;

  const tailTop=new THREE.Mesh(
    new THREE.ConeGeometry(
      .48*bodyScale,
      .82*bodyScale,
      5
    ),
    bodyMat
  );

  tailTop.rotation.z=Math.PI/2;
  tailTop.position.x=
    -.36*bodyScale;

  tailTop.scale.y=1.18;
  tail.add(tailTop);

  const tailBottom=tailTop.clone();

  tailBottom.rotation.z=-Math.PI/2;
  tailBottom.position.x=
    -.36*bodyScale;

  tail.add(tailBottom);

  const tailCenter=new THREE.Mesh(
    new THREE.SphereGeometry(
      .18*bodyScale,12,8
    ),
    bodyMat
  );

  tailCenter.position.x=
    -.1*bodyScale;

  tail.add(tailCenter);

  const dorsal=new THREE.Mesh(
    new THREE.ConeGeometry(
      .28*bodyScale,
      .7*bodyScale,
      4
    ),
    finMat
  );

  dorsal.position.set(
    .02,
    .53*bodyScale,
    0
  );

  dorsal.rotation.z=Math.PI;

  g.add(dorsal);
  g.userData.dorsal=dorsal;

  const lower=new THREE.Mesh(
    new THREE.ConeGeometry(
      .22*bodyScale,
      .52*bodyScale,
      4
    ),
    finMat
  );

  lower.position.set(
    -.12,
    -.48*bodyScale,
    0
  );

  g.add(lower);
  g.userData.lower=lower;

  for(const z of [-.34,.34]){

    const fin=new THREE.Mesh(
      new THREE.ConeGeometry(
        .2*bodyScale,
        .62*bodyScale,
        4
      ),
      finMat
    );

    fin.position.set(
      .18*bodyScale,
      -.08*bodyScale,
      z
    );

    fin.rotation.x=
      z>0?.62:-.62;

    fin.rotation.z=Math.PI;

    g.add(fin);

  }

  const gillMat=
    new THREE.MeshStandardMaterial({
      color:0x7d2028,
      roughness:.38
    });

  const gill=new THREE.Mesh(
    new THREE.TorusGeometry(
      .18*bodyScale,
      .025*bodyScale,
      8,12,
      Math.PI*.9
    ),
    gillMat
  );

  gill.position.set(
    .48*bodyScale,
    .02*bodyScale,
    .47*bodyScale
  );

  gill.rotation.y=Math.PI/2;

  g.add(gill);

  for(const z of [-.22,.22]){

    const eye=new THREE.Group();

    eye.position.set(
      .79*bodyScale,
      .18*bodyScale,
      z*1.7
    );

    g.add(eye);

    const white=new THREE.Mesh(
      new THREE.SphereGeometry(
        .12*bodyScale,16,10
      ),
      eyeWhite
    );

    eye.add(white);

    const pupil=new THREE.Mesh(
      new THREE.SphereGeometry(
        .07*bodyScale,14,10
      ),
      eyeBlack
    );

    pupil.position.set(
      .095*bodyScale,
      0,
      z>0
        ?.045*bodyScale
        :-.045*bodyScale
    );

    eye.add(pupil);

    const shine=new THREE.Mesh(
      new THREE.SphereGeometry(
        .022*bodyScale,8,6
      ),
      new THREE.MeshBasicMaterial({
        color:0xffffff
      })
    );

    shine.position.set(
      .13*bodyScale,
      .035*bodyScale,
      z>0
        ?.065*bodyScale
        :-.065*bodyScale
    );

    eye.add(shine);

  }

  const mouth=new THREE.Mesh(
    new THREE.TorusGeometry(
      .14*bodyScale,
      .022*bodyScale,
      8,16,
      Math.PI*.9
    ),
    darkFinMat
  );

  mouth.position.set(
    .93*bodyScale,
    -.035*bodyScale,
    0
  );

  mouth.rotation.y=Math.PI/2;

  g.add(mouth);

  g.scale.setScalar(size);

  return g;

}

function fishType(){

  let r=Math.random();
  let f;

  if(r<.60)
    f=['kling',50,2,100];

  else if(r<.90)
    f=['park',100,1,200];

  else
    f=['nickgroud',120,.6,300];

  let enchanted=
    Math.random()*100<f[2];

  return[
    f[0],
    enchanted?f[3]:f[1],
    enchanted
  ];

}

function finishCatch(){

  player.userData.fishing=false;
  player.position.y=1;
  player.rotation.x=0;

  let f=fishType();

  fish++;
  xp+=f[1];

  fishInv[f[0]]++;

  if(f[2])
    enchInv[f[0]]++;

  $('clickCatch').classList.add('hide');
  $('catchMsg').classList.remove('hide');

  setTimeout(()=>{
    $('catchMsg').classList.add('hide');
  },1400);

  let col=
    f[0]==='kling'
    ?0x2ed3ff
    :f[0]==='park'
    ?0x7b7cff
    :0xffb52e;

  let held=makeRealFish(
    col,
    f[0],
    .72
  );

  held.position.set(
    .95,2.25,-.55
  );

  held.rotation.set(
    .05,-.2,-.2
  );

  held.userData.fishHeld=true;

  player.add(held);

  fishHeld=held;

  let heldRef=held;

  setTimeout(()=>{

    if(fishHeld===heldRef){

      player.remove(heldRef);
      fishHeld=null;

    }

  },3500);

  sync();

}

function doCatchClick(){

  if(!catching)
    return;

  clicks++;

  $("clickCount").textContent=
    clicks;

  if(clicks>=10){

    catching=false;
    finishCatch();

  }

}

function changeSkin(){

  let body=
    player?.children.find(
      x=>x.geometry?.parameters?.width===1.15
    );

  if(body)
    body.material.color.set(
      colors[equipped%colors.length]
    );

}

function init(){

  scene=new THREE.Scene();

  scene.background=
    new THREE.Color(0x75d3e8);

  scene.fog=
    new THREE.Fog(
      0x75d3e8,
      55,
      145
    );

  camera=
    new THREE.PerspectiveCamera(
      65,
      innerWidth/innerHeight,
      .1,
      300
    );

  renderer=
    new THREE.WebGLRenderer({
      antialias:true,
      powerPreference:'high-performance'
    });

  renderer.setSize(
    innerWidth,
    innerHeight
  );

  game.prepend(
    renderer.domElement
  );

  scene.add(
    new THREE.HemisphereLight(
      0xbbeeff,
      0x19364a,
      2
    )
  );

  let sun=
    new THREE.DirectionalLight(
      0xffffff,
      1.8
    );

  sun.position.set(
    25,45,20
  );

  scene.add(sun);

  let island=new THREE.Mesh(
    new THREE.CylinderGeometry(
      105,117,4,64
    ),
    mat(0xc9aa65)
  );

  island.position.y=-2;
  scene.add(island);

  let water=new THREE.Mesh(
    new THREE.PlaneGeometry(
      400,400
    ),
    new THREE.MeshPhongMaterial({
      color:0x0785ad,
      transparent:true,
      opacity:.85
    })
  );

  water.rotation.x=-Math.PI/2;
  water.position.y=-4;

  scene.add(water);

  [
    [-70,-55],
    [70,-55],
    [-65,55],
    [65,55],
    [-20,75],
    [20,75],
    [-82,0],
    [82,0],
    [-45,-5],
    [45,-5],
    [-15,-75],
    [15,-75]
  ].forEach(p=>makeTree(...p));

  let shop=cube(
    [11,5,6],
    0xd9c39d
  );

  shop.position.set(
    0,2,-82
  );

  scene.add(shop);

  let boatShop=cube(
    [11,5,6],
    0x486477
  );

  boatShop.position.set(
    78,2,-82
  );

  scene.add(boatShop);

  let boatRoof=cube(
    [12,1,7],
    0x273a4a
  );

  boatRoof.position.set(
    78,5,-82
  );

  scene.add(boatRoof);

  let boatDoor=cube(
    [2.5,3,.2],
    0x10232d
  );

  boatDoor.position.set(
    78,1.5,-78.9
  );

  scene.add(boatDoor);

  let roof=cube(
    [12,1,7],
    0xd74a42
  );

  roof.position.set(
    0,5,-82
  );

  scene.add(roof);

  let door=cube(
    [2.5,3,.2],
    0x36596a
  );

  door.position.set(
    0,1.5,-78.9
  );

  scene.add(door);

  let bridge=cube(
    [16,1.2,18],
    0x70472c
  );

  bridge.position.set(
    0,-.1,-92
  );

  scene.add(bridge);

  let bridge2=bridge.clone();

  bridge2.position.set(
    92,-.1,0
  );

  bridge2.rotation.y=Math.PI/2;

  scene.add(bridge2);

  let bridge3=bridge.clone();

  bridge3.position.set(
    0,-.1,92
  );

  scene.add(bridge3);

  let bridge4=bridge.clone();

  bridge4.position.set(
    -92,-.1,0
  );

  bridge4.rotation.y=Math.PI/2;

  scene.add(bridge4);

  for(let x of [-7,-3,3,7]){

    let post=cube(
      [.35,2,.35],
      0x4c301e
    );

    post.position.set(
      x,1,-28
    );

    scene.add(post);

    let rail=cube(
      [.35,1.4,22],
      0x4c301e
    );

    rail.position.set(
      x,1,-28
    );

    scene.add(rail);

  }

  let sign=cube(
    [5,2,.25],
    0x4b2d1b
  );

  sign.position.set(
    0,3,-24
  );

  scene.add(sign);

  makePlayer();

  player.position.set(
    0,1,7
  );

  for(let i=0;i<30;i++){

    let species=
      ['kling','park','nickgroud']
      [i%3];

    let col=
      species==='kling'
      ?0x2ed3ff
      :species==='park'
      ?0x7b7cff
      :0xffb52e;

    let f=makeRealFish(
      col,
      species,
      .58+Math.random()*.18
    );

    let a=Math.random()*Math.PI*2;

    let r=
      108+Math.random()*30;

    f.position.set(
      Math.cos(a)*r,
      -3.45-Math.random()*1.2,
      Math.sin(a)*r
    );

    f.rotation.y=
      a+Math.PI;

    f.userData.t=
      Math.random()*10;

    f.userData.baseY=
      f.position.y;

    f.userData.speed=
      .012+Math.random()*.012;

    f.userData.phase=
      Math.random()*6.28;

    f.userData.baseScaleY=
      f.scale.y;

    scene.add(f);
    fishObjs.push(f);

  }

  setupControls();
  applyQuality();

  window.onresize=resize;

  animate();

}

function applyQuality(){

  if(!renderer)
    return;

  let q=Number(quality);

  let tier=
    q<=20
    ?'PÉSSIMA'
    :q<=40
    ?'BOA'
    :q<=80
    ?'MUITO BOA'
    :'MUITOOOO BOA';

  if($('qv'))
    $('qv').textContent=
      q+'% — '+tier;

  let ratio=
    q<=20?.5
    :q<=40?.75
    :q<=80?1.15
    :1.6;

  renderer.setPixelRatio(ratio);

  if(scene){

    if(q<=20){

      scene.fog.near=28;
      scene.fog.far=80;

    }else if(q<=40){

      scene.fog.near=40;
      scene.fog.far=105;

    }else if(q<=80){

      scene.fog.near=55;
      scene.fog.far=145;

    }else{

      scene.fog.near=70;
      scene.fog.far=190;

    }

  }

}

function resize(){

  camera.aspect=
    innerWidth/innerHeight;

  camera.updateProjectionMatrix();

  renderer.setSize(
    innerWidth,
    innerHeight
  );

}

$("clickCatch").onclick=
  doCatchClick;

function setupControls(){

  document
    .querySelectorAll('[data-key]')
    .forEach(b=>{

      let k=b.dataset.key;

      b.onpointerdown=e=>{
        e.preventDefault();
        keys[k]=1;
      };

      b.onpointerup=
      b.onpointercancel=()=>{
        keys[k]=0;
      };

    });

  window.onkeydown=e=>
    keys[e.key.toLowerCase()]=1;

  window.onkeyup=e=>
    keys[e.key.toLowerCase()]=0;

  renderer.domElement.style.touchAction=
    'none';

  let camPointers=
    new Map();

  renderer.domElement.onpointerdown=e=>{

    if(
      e.pointerType==='mouse' &&
      e.button!==0
    )
      return;

    camPointers.set(
      e.pointerId,
      {
        x:e.clientX,
        y:e.clientY
      }
    );

    renderer.domElement
      .setPointerCapture?.(
        e.pointerId
      );

  };

  renderer.domElement.onpointermove=e=>{

    let p=
      camPointers.get(
        e.pointerId
      );

    if(!p)
      return;

    let dx=
      e.clientX-p.x;

    let dy=
      e.clientY-p.y;

    p.x=e.clientX;
    p.y=e.clientY;

    let sens=
      .003*
      (
        .35+
        .65*
        (sensitivity/100)
      );

    yaw-=dx*sens;

    pitch=Math.max(
      -.45,
      Math.min(
        .9,
        pitch-dy*sens
      )
    );

  };

  renderer.domElement.onpointerup=e=>
    camPointers.delete(
      e.pointerId
    );

  renderer.domElement.onpointercancel=e=>
    camPointers.delete(
      e.pointerId
    );

}

function animate(){

  requestAnimationFrame(
    animate
  );

  if(!renderer)
    return;

  const sp=.13*1.7;

  let dx2=0,
      dz2=0,
      moving=false;

  if(keys.w){

    dx2-=Math.sin(yaw)*sp;
    dz2-=Math.cos(yaw)*sp;
    moving=true;

  }

  if(keys.s){

    dx2+=Math.sin(yaw)*sp;
    dz2+=Math.cos(yaw)*sp;
    moving=true;

  }

  if(keys.a){

    dx2-=Math.cos(yaw)*sp;
    dz2+=Math.sin(yaw)*sp;
    moving=true;

  }

  if(keys.d){

    dx2+=Math.cos(yaw)*sp;
    dz2-=Math.sin(yaw)*sp;
    moving=true;

  }

  if(
    moving &&
    !player.userData.fishing
  ){

    player.position.x=
      Math.max(
        -98,
        Math.min(
          98,
          player.position.x+dx2
        )
      );

    player.position.z=
      Math.max(
        -98,
        Math.min(
          98,
          player.position.z+dz2
        )
      );

    player.rotation.y=
      Math.atan2(dx2,dz2);

    let t=
      performance.now()*.014;

    parts.forEach(p=>{

      if(p.userData.arm)
        p.rotation.x=
          Math.sin(t)*
          .65*
          p.userData.arm;

      if(p.userData.leg)
        p.rotation.x=
          -Math.sin(t)*
          .7*
          p.userData.leg;

    });

  }else if(
    !player.userData.fishing
  ){

    parts.forEach(p=>
      p.rotation.x*=.85
    );

  }

  if(player.userData.fishing){

    let ft=
      performance.now()-
      player.userData.fishingStart;

    player.position.y=
      ft<3000?.65:.85;

    parts.forEach(p=>{

      if(p.userData.leg)
        p.rotation.x=-.15;

      if(p.userData.arm)
        p.rotation.x=-1.15;

    });

    if(ft>3000)
      player.rotation.x=-.08;

  }

  let dist=8;

  let target=
    new THREE.Vector3(
      player.position.x+
        Math.sin(yaw)*dist,

      player.position.y+
        4+
        pitch*4,

      player.position.z+
        Math.cos(yaw)*dist
    );

  camera.position.lerp(
    target,
    .22
  );

  camera.lookAt(
    player.position.x,
    player.position.y+1.7,
    player.position.z
  );

  if($('shopHint')){

    $('shopHint')
      .classList.toggle(
        'hide',
        (!nearShop()&&!nearBoatShop())||
        shopOpen
      );

  }

  fishObjs.forEach(f=>{

    let tm=
      performance.now()*.001;

    let ph=
      f.userData.phase||0;

    f.position.y=
      f.userData.baseY+
      Math.sin(
        tm*2.2+
        f.userData.t
      )*.2;

    f.rotation.y+=
      f.userData.speed||.005;

    f.rotation.z=
      Math.sin(
        tm*3.2+ph
      )*.045;

    f.position.x+=
      Math.cos(
        f.rotation.y
      )*.018;

    f.position.z+=
      Math.sin(
        f.rotation.y
      )*.018;

    let wave=
      Math.sin(
        tm*10+ph
      )*.48;

    if(f.userData.tail)
      f.userData.tail.rotation.y=
        wave;

    if(f.userData.dorsal)
      f.userData.dorsal.rotation.z=
        Math.PI+
        Math.sin(
          tm*7+ph
        )*.08;

    if(f.userData.lower)
      f.userData.lower.rotation.z=
        Math.sin(
          tm*7+ph
        )*.07;

    f.scale.y=
      f.userData.baseScaleY*
      (
        1+
        Math.sin(
          tm*10+ph
        )*.012
      );

  });

  if(fishHeld){

    let ht=
      performance.now()*.006;

    fishHeld.rotation.y=
      Math.sin(ht)*.08;

    if(fishHeld.userData.tail)
      fishHeld.userData.tail.rotation.y=
        Math.sin(ht*8)*.3;

  }

  renderer.render(
    scene,
    camera
  );

}

function cast(){

  if(catching)
    return;

  if(bait<=0){

    alert(
      'Sem iscas. Vá à loja para comprar.'
    );

    return;

  }

  if(!nearBridge()){

    alert(
      'Chegue perto de uma ponte para pescar.'
    );

    return;

  }

  bait--;

  sync();

  catching=true;
  clicks=0;

  $('clickCount').textContent=0;

  player.userData.fishing=true;

  player.userData.fishingStart=
    performance.now();

  player.rotation.y=0;

  setTimeout(()=>{

    if(!catching)
      return;

    $('catchMsg')
      .classList.remove('hide');

    setTimeout(()=>{

      if(catching){

        $('catchMsg')
          .classList.add('hide');

        $('clickCatch')
          .classList.remove('hide');

      }

    },900);

  },3000);

}

sync();

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

Game Source: Remix of Spedfish — Auto Fish Play 3D

Creator: MegaFlare51

Libraries: none

Complexity: complex (2759 lines, 42.5 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: spedfish-auto-fish-play-3d-megaflare51" to link back to the original. Then publish at arcadelab.ai/publish.