RA

[PokéTibia / OTClient] Fly com sombra e botões de subir/descer — source + Lua

Por raulcdj, há 10h em Mods, funções e outros

1
14
raulcdjR
01 de outubro de 2026 às 17:00

Fly com sombra e botoes — PokéTibia / OTClient

Compartilhando os trechos que uso na minha base TFS 0.3.6 / PokexGeneration e no meu fork de OTClient. O jogador montado no Fly recebe uma sombra preta deslocada, e o painel oferece Subir/Descer. A criatura voando e desenhada apos os objetos do andar; a sombra fica no passe do chao. Nao e shader nem sistema completo de montar Fly.

Precisa da source do CLIENTE com g_drawQueue, g_adaptiveRenderer, g_sprites.getOffsetFactor e a assinatura de Outfit::draw mostrada no patch. Nao e compativel automaticamente com OTClient classico, v8, Redemption ou qualquer fork. O TFS precisa de extended opcodes e de um sistema Fly existente com storage 17000 e outfits/tiles de voo. Nao substitua order.lua nem login.lua completos.

Instalacao: salve o patch como fly_client.patch na raiz da source do cliente. Use git apply --check --ignore-space-change fly_client.patch antes de git apply --ignore-space-change fly_client.patch. Se houver contexto diferente, adapte manualmente os trechos. Nao aplique duas vezes se setFlying/drawShadow ja existem. Recompile o cliente. Copie os tres arquivos de game_fly para modules/game_fly e carregue o modulo depois de game_interface (no bootstrap ou em load-later de interface.otmod). Esta edicao publica usa botoes com texto para nao depender das imagens das setas.

Servidor: copie flyup.lua e flydown.lua para data/talkactions/scripts e registre dentro de <talkactions>:

<talkaction words="h1" case-sensitive="no" event="script" value="flyup.lua"/>
<talkaction words="h2" case-sensitive="no" event="script" value="flydown.lua"/>

Integre show/hide nos pontos de montar, desmontar e login conforme o bloco de integracao abaixo. O modulo envia h1/h2 pelo chat; nao envia up/down pelo opcode. O opcode 115 apenas informa show/hide do servidor ao cliente. Se 115 estiver ocupado, troque em todos os locais.

Os scripts h1/h2 usam tiles 11675/11676/11677, storage 17000, markPosEff e canWalkOnPos desta familia de bases. Confira seus IDs e helpers; nao sao recursos do TFS puro. Eles mantem as restricoes de teto, obstaculos/PZ e altura desta base (z de 1 a 7). O item de voo deve ser removido pelo movement/sistema existente; nao copie esses IDs para uma base sem os tiles e a limpeza correspondentes.

Limites: esta implementacao marca somente o jogador local como flying; outros jogadores nao recebem a sombra automaticamente. Nao modifica pathfinding nem cria voo para qualquer Pokemon. O deslocamento da sombra e 60 * offsetFactor nos dois eixos; ajuste em Creature::drawShadow. O patch foi extraido dos arquivos locais, nao de uma release universal. Revisao de codigo e testes Lua com simulacao realizados; nao foi feita nova compilacao nem teste visual nesta publicacao. Teste montando/desmontando, andando em todas as direcoes, mudando andar, reconectando e com GameMapDrawGroundFirst ligado/desligado antes de usar em producao.

Creditos: OTClient team e autores do fork, mantendo a licenca MIT do OTClient. Modulo game_fly identificado como autor raul. Organizacao da edicao publica com auxilio do Codex. Nao reivindico autoria dos sistemas de Fly preexistentes da base. Nao distribuo executavel, sprites, banco, mapa ou configuracoes particulares.

client\modules\game_fly\game_fly.lua

flyWindow = nil
FLY_OPCODE = 115 -- Certifique-se de que nenhum outro modulo use este mesmo numero

function init()
connect(g_game, { onGameStart = online, onGameEnd = offline })

-- Registra a escuta do Opcode vindo do servidor
ProtocolGame.registerExtendedOpcode(FLY_OPCODE, onExtendedOpcode)

if g_game.isOnline() then
online()
end
end

function terminate()
disconnect(g_game, { onGameStart = online, onGameEnd = offline })
ProtocolGame.unregisterExtendedOpcode(FLY_OPCODE)
offline()
end

function online()
if not flyWindow then
local gameMap = modules.game_interface.getMapPanel()
flyWindow = g_ui.displayUI('game_fly.otui', gameMap)
end
-- Inicia escondido. O servidor vai mandar o comando para exibir ao subir no Fly
flyWindow:hide()
end

function offline()
local player = g_game.getLocalPlayer()
if player then player:setFlying(false) end
if flyWindow then
flyWindow:destroy()
flyWindow = nil
end
end

-- Funcao que recebe o comando do servidor para ocultar/mostrar
function onExtendedOpcode(protocol, opcode, buffer)
if not flyWindow then return end

if buffer == "show" then
local player = g_game.getLocalPlayer()
if player then player:setFlying(true) end
flyWindow:show()
flyWindow:raise()
elseif buffer == "hide" then
local player = g_game.getLocalPlayer()
if player then player:setFlying(false) end
flyWindow:hide()
end
end

function flyUp()
if g_game.isOnline() then
g_game.talk("h1")
end
end

function flyDown()
if g_game.isOnline() then
g_game.talk("h2")
end
end

client\modules\game_fly\game_fly.otmod

Module
name: game_fly
description: Painel de Fly com botoes de seta estilo OtPokemon
author: raul
sandboxed: true
scripts: [ game_fly ]
@onLoad: init()
@onUnload: terminate()

client\modules\game_fly\game_fly.otui

UIWindow
id: flyWindow
size: 64 128
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
focusable: false
&draggable: false

UIButton
id: buttonUp
size: 58 32
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
margin-top: -20
text: 'Subir'
focusable: false
&phantom: false
@onClick: modules.game_fly.flyUp()

UIButton
id: buttonDown
size: 58 32
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
text: 'Descer'
focusable: false
&phantom: false
@onClick: modules.game_fly.flyDown()

fly_client.patch

--- a/src/client/creature.h
+++ b/src/client/creature.h
@@ -47,6 +47,7 @@
virtual ~Creature();

virtual void draw(const Point& dest, bool animate = true, LightView* lightView = nullptr);
+ void drawShadow(const Point& dest, bool animate = true);
virtual void drawOutfit(const Rect& destRect, Otc::Direction direction = Otc::InvalidDirection, const Color& color = Color::white, bool animate = false, bool ui = false, bool oldScaling = false);

void drawInformation(const Point& point, bool useGray, const Rect& parentRect, int drawFlags);
@@ -74,6 +75,8 @@
void setTypeTexture(const std::string& filename);
void setIconTexture(const std::string& filename);
void setPassable(bool passable) { m_passable = passable; }
+ void setFlying(bool flying) { m_flying = flying; }
+ bool isFlying() { return m_flying; }
void setSpeedFormula(double speedA, double speedB, double speedC);

void addTimedSquare(uint8 color);
@@ -232,6 +235,7 @@
stdext::boolean<true> m_showShieldTexture;
stdext::boolean<false> m_shieldBlink;
stdext::boolean<false> m_passable;
+ bool m_flying = false;
Color m_timedSquareColor;
Color m_staticSquareColor;
Color m_nameColor;
--- a/src/client/creature.cpp
+++ b/src/client/creature.cpp
@@ -135,6 +135,23 @@
lightView->addLight(creatureCenter, light);
}

+void Creature::drawShadow(const Point& dest, bool animate)
+{
+ if (!m_flying || !canBeSeen())
+ return;
+
+ Point animationOffset = animate ? m_walkOffset : Point(0, 0);
+ if (m_outfit.getCategory() != ThingCategoryCreature)
+ animationOffset -= getDisplacement();
+
+ const Point jumpOffset(m_jumpOffset.x, m_jumpOffset.y);
+ const Point shadowOffset(60 * g_sprites.getOffsetFactor(), 60 * g_sprites.getOffsetFactor());
+ const size_t shadowQueueStart = g_drawQueue->size();
+ m_outfit.draw(dest - jumpOffset + animationOffset + shadowOffset,
+ m_walking ? m_walkDirection : m_direction,
+ m_walkAnimationPhase, true, nullptr);
+ g_drawQueue->setMark(shadowQueueStart, Color(0, 0, 0, 255));
+}

void Creature::drawOutfit(const Rect& destRect, Otc::Direction direction, const Color& color, bool animate, bool ui, bool oldScaling)
{
--- a/src/client/tile.h
+++ b/src/client/tile.h
@@ -67,7 +67,9 @@

void drawGround(const Point& dest, LightView* lightView = nullptr);
void drawBottom(const Point& dest, LightView* lightView = nullptr);
+ void drawCreatureShadows(const Point& dest);
void drawCreatures(const Point& dest, LightView* lightView = nullptr);
+ void drawFlyingCreatures(const Point& dest, LightView* lightView = nullptr);
void drawTop(const Point& dest, LightView* lightView = nullptr);
void drawTexts(Point dest);
void drawWidget(Point dest);
--- a/src/client/tile.cpp
+++ b/src/client/tile.cpp
@@ -120,6 +120,31 @@
}
}

+void Tile::drawCreatureShadows(const Point& dest)
+{
+ if (m_fill != Color::alpha || m_topDraws < m_topCorrection)
+ return;
+
+ for (const CreaturePtr& creature : m_walkingCreatures) {
+ if (creature->isHidden())
+ continue;
+ Point creatureDest(dest.x + ((creature->getPrewalkingPosition().x - m_position.x) * g_sprites.spriteSize() - m_drawElevation * g_sprites.getOffsetFactor()),
+ dest.y + ((creature->getPrewalkingPosition().y - m_position.y) * g_sprites.spriteSize() - m_drawElevation * g_sprites.getOffsetFactor()));
+ creature->drawShadow(creatureDest, true);
+ }
+
+ int limit = g_adaptiveRenderer.creaturesLimit();
+ for (auto& thing : m_things) {
+ if (!thing->isCreature() || thing->isHidden())
+ continue;
+ if (limit-- <= 0)
+ break;
+ CreaturePtr creature = thing->static_self_cast<Creature>();
+ if (!creature || creature->isWalking())
+ continue;
+ creature->drawShadow(dest - m_drawElevation * g_sprites.getOffsetFactor(), true);
+ }
+}

void Tile::drawCreatures(const Point& dest, LightView* lightView)
{
@@ -130,7 +155,7 @@

// walking creatures
for (const CreaturePtr& creature : m_walkingCreatures) {
- if (creature->isHidden())
+ if (creature->isHidden() || creature->isFlying())
continue;
Point creatureDest(dest.x + ((creature->getPrewalkingPosition().x - m_position.x) * g_sprites.spriteSize() - m_drawElevation * g_sprites.getOffsetFactor()),
dest.y + ((creature->getPrewalkingPosition().y - m_position.y) * g_sprites.spriteSize() - m_drawElevation * g_sprites.getOffsetFactor()));
@@ -146,12 +171,37 @@
if (limit-- <= 0)
break;
CreaturePtr creature = thing->static_self_cast<Creature>();
- if (!creature || creature->isWalking())
+ if (!creature || creature->isWalking() || creature->isFlying())
continue;
creature->draw(dest - m_drawElevation * g_sprites.getOffsetFactor(), true, lightView);
}
}

+void Tile::drawFlyingCreatures(const Point& dest, LightView* lightView)
+{
+ if (m_fill != Color::alpha || m_topDraws < m_topCorrection)
+ return;
+
+ for (const CreaturePtr& creature : m_walkingCreatures) {
+ if (creature->isHidden() || !creature->isFlying() || creature->getPrewalkingPosition() != m_position)
+ continue;
+ Point creatureDest(dest.x + ((creature->getPrewalkingPosition().x - m_position.x) * g_sprites.spriteSize() - m_drawElevation * g_sprites.getOffsetFactor()),
+ dest.y + ((creature->getPrewalkingPosition().y - m_position.y) * g_sprites.spriteSize() - m_drawElevation * g_sprites.getOffsetFactor()));
+ creature->draw(creatureDest, true, lightView);
+ }
+
+ int limit = g_adaptiveRenderer.creaturesLimit();
+ for (auto& thing : m_things) {
+ if (!thing->isCreature() || thing->isHidden())
+ continue;
+ if (limit-- <= 0)
+ break;
+ CreaturePtr creature = thing->static_self_cast<Creature>();
+ if (!creature || creature->isWalking() || !creature->isFlying() || creature->getPosition() != m_position)
+ continue;
+ creature->draw(dest - m_drawElevation * g_sprites.getOffsetFactor(), true, lightView);
+ }
+}

void Tile::drawTop(const Point& dest, LightView* lightView)
{
--- a/src/client/mapview.cpp
+++ b/src/client/mapview.cpp
@@ -177,6 +177,11 @@
Point tileDrawPos = transformPositionTo2D(tile->getPosition(), cameraPosition);
tile->drawGround(tileDrawPos, m_lightView.get());
}
+ // Flying shadows stay on the ground and are covered by walls and top items.
+ for (auto& tile : tiles) {
+ Point tileDrawPos = transformPositionTo2D(tile->getPosition(), cameraPosition);
+ tile->drawCreatureShadows(tileDrawPos);
+ }
// bottom, creatures, top
for (auto& tile : tiles) {
Point tileDrawPos = transformPositionTo2D(tile->getPosition(), cameraPosition);
@@ -205,6 +210,7 @@
}

tile->drawGround(tileDrawPos, m_lightView.get());
+ tile->drawCreatureShadows(tileDrawPos);

tile->drawBottom(tileDrawPos, m_lightView.get());

@@ -218,6 +224,11 @@
}
}

+ // Flying creatures remain visible above mountains and other top objects.
+ for (auto& tile : tiles) {
+ Point tileDrawPos = transformPositionTo2D(tile->getPosition(), cameraPosition);
+ tile->drawFlyingCreatures(tileDrawPos, m_lightView.get());
+ }

for (const MissilePtr& missile : g_map.getFloorMissiles(floor)) {
missile->draw(transformPositionTo2D(missile->getPosition(), cameraPosition), true, m_lightView.get());
--- a/src/client/luafunctions_client.cpp
+++ b/src/client/luafunctions_client.cpp
@@ -509,6 +509,8 @@
g_lua.bindClassMemberFunction<Creature>("setSpeed", &Creature::setSpeed);
g_lua.bindClassMemberFunction<Creature>("getBaseSpeed", &Creature::getBaseSpeed);
g_lua.bindClassMemberFunction<Creature>("setBaseSpeed", &Creature::setBaseSpeed);
+ g_lua.bindClassMemberFunction<Creature>("setFlying", &Creature::setFlying);
+ g_lua.bindClassMemberFunction<Creature>("isFlying", &Creature::isFlying);
g_lua.bindClassMemberFunction<Creature>("getSkull", &Creature::getSkull);
g_lua.bindClassMemberFunction<Creature>("getShield", &Creature::getShield);
g_lua.bindClassMemberFunction<Creature>("getEmblem", &Creature::getEmblem);

server\data\talkactions\scripts\flydown.lua

local lower = {460, 11675, 11676, 11677} -- Corrigido para números para evitar falhas de leitura

local houses = {'919', '1015', '1590', '1591', '1592', '1593', '1582', '1584', '1586', '1588', '5248', '5189'}

local function refreshFlyOutfit(cid, flyOutfit)
addEvent(function()
if not isCreature(cid) then return end
doCreatureChangeOutfit(cid, flyOutfit)
end, 80)
end

function onSay(cid, words, param)

if param ~= "" then
return false
end

if getPlayerStorageValue(cid, 17000) <= 0 then
return true
end

-- Impede de descer abaixo do nível do chão (Z=7)
if getThingPos(cid).z >= 7 then
doPlayerSendCancel(cid, "Voce nao pode descer mais!")
return true
end

local currentTile = getTileInfo(getThingPos(cid)).itemid

if currentTile == 11677 then
doPlayerSendCancel(cid, "Desculpe, nao e possivel descer mais por aqui.")
return true
end

if currentTile >= 2 and not isInArray(lower, currentTile) then
doPlayerSendCancel(cid, "Voce nao pode descer aqui.")
return true
end

local pos = getThingPos(cid)
pos.z = pos.z + 1
pos.stackpos = 0

if getTileThingByPos(pos).itemid >= 1 then

-- Impede o jogador de descer voando dentro de uma Zone de Proteção (PZ), como um CP
if getTilePzInfo(pos) == true then
doPlayerSendCancel(cid, "Voce nao pode descer aqui (Zona de Protecao).")
return true
end

-- Impede de descer se houver um obstáculo sólido embaixo (paredes, montanhas, etc.)
if not canWalkOnPos(pos, true, true, false, false, true) then
doPlayerSendCancel(cid, "Voce nao pode descer aqui, o caminho esta bloqueado.")
return true
end

local flyOutfit = getCreatureOutfit(cid)
doTeleportThing(cid, pos)
refreshFlyOutfit(cid, flyOutfit)
if getCreatureOutfit(cid).lookType == 667 or getCreatureOutfit(cid).lookType == 999 then
markPosEff(cid, getThingPos(cid))
end

else
local flyOutfit = getCreatureOutfit(cid)
doCombatAreaHealth(cid, 0, pos, 0, 0, 0, CONST_ME_NONE)
doCreateItem(11675, 1, pos)
doTeleportThing(cid, pos)
refreshFlyOutfit(cid, flyOutfit)
if getCreatureOutfit(cid).lookType == 667 or getCreatureOutfit(cid).lookType == 999 then
markPosEff(cid, getThingPos(cid)) --edited porygon fly sistem
end
end

return true
end

server\data\talkactions\scripts\flyup.lua

local function refreshFlyOutfit(cid, flyOutfit)
addEvent(function()
if not isCreature(cid) then return end
doCreatureChangeOutfit(cid, flyOutfit)
end, 80)
end

function onSay(cid, words, param)

if param ~= "" then
return false
end

if getPlayerStorageValue(cid, 17000) <= 0 then
return true
end

-- Altere para 0 se o seu mapa suportar o andar 0 sem travar o movimento do Pokémon.
local andar_maximo_permitido = 1

if getThingPos(cid).z <= andar_maximo_permitido then
doPlayerSendCancel(cid, "Voce ja esta na altura maxima permitida de Fly!")
return true
end

local pos = getThingPos(cid)
pos.z = pos.z - 1
pos.stackpos = 0

-- Impede de subir se houver um teto, telhado ou construção sólida logo acima
if getTileThingByPos(pos).itemid >= 1 or getTileItemById(getThingPos(cid), 1386).itemid >= 1 then
doPlayerSendCancel(cid, "Voce nao pode voar atraves de construcoes.")
return true
end

local flyOutfit = getCreatureOutfit(cid)
doCombatAreaHealth(cid, 0, pos, 0, 0, 0, CONST_ME_NONE)
doCreateItem(11676, 1, pos)
doTeleportThing(cid, pos)
refreshFlyOutfit(cid, flyOutfit)

if getCreatureOutfit(cid).lookType == 667 or getCreatureOutfit(cid).lookType == 999 then
markPosEff(cid, getThingPos(cid)) --edited porygon fly sistem
end

return true
end

server\integracao.lua

-- Inserir apenas nos pontos correspondentes do sistema de Fly existente.
-- Depois de montar e ativar storage 17000:
doSendPlayerExtendedOpcode(cid, 115, "show")
-- Depois de desmontar e desativar storage 17000:
doSendPlayerExtendedOpcode(cid, 115, "hide")
-- No login, depois de restaurar o estado de Fly:
if getPlayerStorageValue(cid, 17000) > 0 then
doSendPlayerExtendedOpcode(cid, 115, "show")
else
doSendPlayerExtendedOpcode(cid, 115, "hide")
end