tibia-rme/source/about_window.cpp
Eduardo Dantas 6d501a5ad2
feat: support reading assets from client 11 or higher (#70)
feat: migrate Remeres to 12.x assets system and modernized item handling

• Migrated old Remeres .dat/.spr system to new 12.x client /assets/ appearance & sprite format
• Removed OTB dependency and outdated client version structures
• Integrated Protobuf-based sprite reader (credit: @nekiro)
• Major refactor on graphics.cpp structure and brush icon handling
• Updated brushes and tags system
• Fixed top order drawing bugs and missing brushes icons
• Fixed outfit color paint and correct offset handling for non-32x32 sprites
• Re-enabled map saving and loading
• Improved creature sprite rendering and brush previews
• Added new item properties window with toggle for legacy/new layout

Co-authored-by: Marcos <66353315+marcosvf132@users.noreply.github.com>
Co-authored-by: Pedro Henrique Alves Cruz <phac@cin.ufpe.br>
2025-05-10 16:38:45 -03:00

892 lines
19 KiB
C++

//////////////////////////////////////////////////////////////////////
// This file is part of Remere's Map Editor
//////////////////////////////////////////////////////////////////////
// Remere's Map Editor is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Remere's Map Editor is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//////////////////////////////////////////////////////////////////////
#include "main.h"
#include "gui.h"
#include "about_window.h"
class GamePanel : public wxPanel {
public:
GamePanel(wxWindow* parent, int width, int height);
virtual ~GamePanel();
void OnPaint(wxPaintEvent &);
void OnKeyDown(wxKeyEvent &);
void OnKeyUp(wxKeyEvent &);
void OnIdle(wxIdleEvent &);
void pause() {
paused_val = true;
}
void unpause() {
paused_val = false;
}
bool paused() const {
return paused_val || dead;
}
protected:
virtual void Render(wxDC &pdc) = 0;
virtual void GameLoop(int time) = 0;
virtual void OnKey(wxKeyEvent &event, bool down) = 0;
virtual int getFPS() const = 0;
protected:
wxStopWatch game_timer;
private:
bool paused_val;
DECLARE_EVENT_TABLE()
protected:
bool dead;
};
const int TETRIS_MAPHEIGHT = 20;
const int TETRIS_MAPWIDTH = 10;
class TetrisPanel : public GamePanel {
public:
TetrisPanel(wxWindow* parent);
~TetrisPanel();
protected:
virtual void Render(wxDC &pdc);
virtual void GameLoop(int time);
virtual void OnKey(wxKeyEvent &event, bool down);
virtual int getFPS() const {
return lines / 10 + 3;
}
enum Color {
NO_COLOR,
RED,
BLUE,
GREEN,
STEEL,
YELLOW,
PURPLE,
WHITE,
};
enum BlockType {
FIRST_BLOCK,
BLOCK_TOWER = FIRST_BLOCK,
BLOCK_SQUARE,
BLOCK_TRIANGLE,
BLOCK_L,
BLOCK_J,
BLOCK_Z,
BLOCK_S,
LAST_BLOCK = BLOCK_S
};
struct Block {
Color structure[4][4];
int x, y;
} block;
const wxBrush &GetBrush(Color color) const;
bool BlockCollisionTest(int mx, int my) const;
void RemoveRow(int row);
void NewBlock();
void MoveBlock(int x, int y);
void RotateBlock();
void NewGame();
void EndGame();
void AddScore(int lines);
int score;
int lines;
Color map[TETRIS_MAPWIDTH][TETRIS_MAPHEIGHT];
};
const int SNAKE_MAPHEIGHT = 20;
const int SNAKE_MAPWIDTH = 20;
class SnakePanel : public GamePanel {
public:
SnakePanel(wxWindow* parent);
~SnakePanel();
protected:
virtual void Render(wxDC &pdc);
virtual void GameLoop(int time);
virtual void OnKey(wxKeyEvent &event, bool down);
virtual int getFPS() const {
return 7;
}
enum {
NORTH,
SOUTH,
WEST,
EAST,
};
void NewApple();
void Move(int dir);
void NewGame();
void EndGame();
void UpdateTitle();
// -1 is apple, 0 is nothing, >0 is snake (will decay in n rounds)
int length;
int last_dir;
int map[SNAKE_MAPWIDTH][SNAKE_MAPHEIGHT];
};
//=============================================================================
// About Window - Information window about the application
BEGIN_EVENT_TABLE(AboutWindow, wxDialog)
EVT_BUTTON(wxID_OK, AboutWindow::OnClickOK)
EVT_BUTTON(ABOUT_VIEW_LICENSE, AboutWindow::OnClickLicense)
EVT_MENU(ABOUT_RUN_TETRIS, AboutWindow::OnTetris)
EVT_MENU(ABOUT_RUN_SNAKE, AboutWindow::OnSnake)
EVT_MENU(wxID_CANCEL, AboutWindow::OnClickOK)
END_EVENT_TABLE()
AboutWindow::AboutWindow(wxWindow* parent) :
wxDialog(parent, wxID_ANY, "About", wxDefaultPosition, wxSize(300, 320), wxRESIZE_BORDER | wxCAPTION | wxCLOSE_BOX),
game_panel(nullptr) {
std::string compiler;
#if defined(__clang__)
compiler = fmt::format("Clang++ {}.{}.{}", __clang_major__, __clang_minor__, __clang_patchlevel__);
#elif defined(_MSC_VER)
compiler = fmt::format("Microsoft Visual Studio {}", _MSC_VER);
#elif defined(__GNUC__)
compiler = fmt::format("G++ {}.{}.{}", __GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__);
#else
compiler = "unknown";
#endif
wxString about;
about << "This is an OpenTibia Map Editor created by Remere and edited by Canary.\n";
about << "Version " << __W_RME_VERSION__ << " for ";
about <<
#ifdef __WINDOWS__
"Windows";
#elif __LINUX__
"Linux";
#elif __APPLE__
"macOS";
#else
"Unsupported OS";
#endif
about << "\n\n";
about << "Using " << wxVERSION_STRING << " interface\n";
about << "OpenGL version " << wxString((char*)glGetString(GL_VERSION), wxConvUTF8) << "\n";
about << "\n";
about << "This program comes with ABSOLUTELY NO WARRANTY;\n";
about << "for details see the LICENSE file.\n";
about << "This is free software, and you are welcome to redistribute it\n";
about << "under certain conditions.\n";
about << "\n";
about << "Compiled on: " << __TDATE__ << " : " << __TTIME__ << "\n";
about << "Compiled with: " << compiler << "\n";
topsizer = newd wxBoxSizer(wxVERTICAL);
topsizer->Add(newd wxStaticText(this, wxID_ANY, about), 1, wxALL, 20);
wxSizer* choicesizer = newd wxBoxSizer(wxHORIZONTAL);
choicesizer->Add(newd wxButton(this, wxID_OK, "OK"), wxSizerFlags(1).Center());
topsizer->Add(choicesizer, 0, wxALIGN_CENTER | wxLEFT | wxRIGHT | wxBOTTOM, 20);
wxAcceleratorEntry entries[3];
entries[0].Set(wxACCEL_NORMAL, WXK_ESCAPE, wxID_CANCEL);
entries[1].Set(wxACCEL_NORMAL, 't', ABOUT_RUN_TETRIS);
entries[2].Set(wxACCEL_NORMAL, 's', ABOUT_RUN_SNAKE);
wxAcceleratorTable accel(3, entries);
SetAcceleratorTable(accel);
SetSizerAndFit(topsizer);
Centre(wxBOTH);
}
AboutWindow::~AboutWindow() {
////
}
void AboutWindow::OnClickOK(wxCommandEvent &WXUNUSED(event)) {
EndModal(0);
}
void AboutWindow::OnClickLicense(wxCommandEvent &WXUNUSED(event)) {
FileName path;
try {
path = wxStandardPaths::Get().GetExecutablePath();
} catch (std::bad_cast &) {
return;
}
path.SetFullName("COPYING.txt");
std::ifstream gpl(path.GetFullPath().mb_str());
std::string gpl_str;
char ch;
while (gpl.get(ch)) {
gpl_str += ch;
}
g_gui.ShowTextBox(this, "License", wxstr(gpl_str.size() ? gpl_str : "The COPYING.txt file is not available."));
}
void AboutWindow::OnTetris(wxCommandEvent &) {
if (!game_panel) {
DestroyChildren();
game_panel = newd TetrisPanel(this);
topsizer->Add(game_panel, 1, wxALIGN_CENTER | wxALL, 7);
Fit();
game_panel->SetFocus();
SetWindowStyleFlag(wxRESIZE_BORDER | wxCAPTION | wxCLOSE_BOX);
Refresh();
}
}
void AboutWindow::OnSnake(wxCommandEvent &) {
if (!game_panel) {
DestroyChildren();
game_panel = newd SnakePanel(this);
topsizer->Add(game_panel, 1, wxALIGN_CENTER | wxALL, 7);
Fit();
game_panel->SetFocus();
SetWindowStyleFlag(wxRESIZE_BORDER | wxCAPTION | wxCLOSE_BOX);
Refresh();
}
}
//=============================================================================
// GamePanel - Abstract class for games
BEGIN_EVENT_TABLE(GamePanel, wxPanel)
EVT_KEY_DOWN(GamePanel::OnKeyDown)
EVT_KEY_UP(GamePanel::OnKeyUp)
EVT_PAINT(GamePanel::OnPaint)
EVT_IDLE(GamePanel::OnIdle)
END_EVENT_TABLE()
GamePanel::GamePanel(wxWindow* parent, int width, int height) :
wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(width, height), wxWANTS_CHARS),
paused_val(false),
dead(false) {
// Receive idle events
SetExtraStyle(wxWS_EX_PROCESS_IDLE);
// Complete redraw
SetBackgroundStyle(wxBG_STYLE_CUSTOM);
}
GamePanel::~GamePanel() {
////
}
void GamePanel::OnPaint(wxPaintEvent &) {
wxBufferedPaintDC pdc(this);
Render(pdc);
}
void GamePanel::OnKeyDown(wxKeyEvent &event) {
switch (event.GetKeyCode()) {
case WXK_ESCAPE: {
if (dead) {
return;
}
wxDialog* dlg = (wxDialog*)GetParent();
dlg->EndModal(0);
break;
}
default: {
OnKey(event, true);
break;
}
}
}
void GamePanel::OnKeyUp(wxKeyEvent &event) {
OnKey(event, false);
}
void GamePanel::OnIdle(wxIdleEvent &event) {
int time = game_timer.Time();
if (time > 1000 / getFPS()) {
game_timer.Start();
if (!paused()) {
GameLoop(time);
}
}
if (!paused()) {
event.RequestMore(true);
}
}
//=============================================================================
// TetrisPanel - A window with a Tetris game!
TetrisPanel::TetrisPanel(wxWindow* parent) :
GamePanel(parent, 16 * TETRIS_MAPWIDTH, 16 * TETRIS_MAPHEIGHT) {
NewGame();
}
TetrisPanel::~TetrisPanel() {
////
}
const wxBrush &TetrisPanel::GetBrush(Color color) const {
static std::unique_ptr<wxBrush> yellow_brush;
static std::unique_ptr<wxBrush> purple_brush;
if (yellow_brush.get() == nullptr) {
yellow_brush.reset(newd wxBrush(wxColor(255, 255, 0)));
}
if (purple_brush.get() == nullptr) {
purple_brush.reset(newd wxBrush(wxColor(128, 0, 255)));
}
const wxBrush* brush = nullptr;
switch (color) {
case RED:
brush = wxRED_BRUSH;
break;
case BLUE:
brush = wxCYAN_BRUSH;
break;
case GREEN:
brush = wxGREEN_BRUSH;
break;
case PURPLE:
brush = purple_brush.get();
break;
case YELLOW:
brush = yellow_brush.get();
break;
case WHITE:
brush = wxWHITE_BRUSH;
break;
case STEEL:
brush = wxGREY_BRUSH;
break;
default:
brush = wxBLACK_BRUSH;
break;
}
return *brush;
}
void TetrisPanel::Render(wxDC &pdc) {
pdc.SetBackground(*wxBLACK_BRUSH);
pdc.Clear();
for (int y = 0; y < TETRIS_MAPHEIGHT; ++y) {
for (int x = 0; x < TETRIS_MAPWIDTH; ++x) {
pdc.SetBrush(GetBrush(map[x][y]));
pdc.DrawRectangle(x * 16, y * 16, 16, 16);
}
}
for (int y = 0; y < 4; ++y) {
for (int x = 0; x < 4; ++x) {
if (block.structure[x][y] != NO_COLOR) {
pdc.SetBrush(GetBrush(block.structure[x][y]));
pdc.DrawRectangle((block.x + x) * 16, (block.y + y) * 16, 16, 16);
}
}
}
}
void TetrisPanel::OnKey(wxKeyEvent &event, bool down) {
if (!down || dead) {
return;
}
switch (event.GetKeyCode()) {
case WXK_SPACE: {
if (paused()) {
unpause();
} else {
pause();
}
break;
}
case WXK_NUMPAD_UP:
case WXK_UP: {
if (dead) {
return;
}
unpause();
RotateBlock();
break;
}
case WXK_NUMPAD_DOWN:
case WXK_DOWN: {
if (dead) {
return;
}
unpause();
MoveBlock(0, 1);
break;
}
case WXK_NUMPAD_LEFT:
case WXK_LEFT: {
if (dead) {
return;
}
unpause();
MoveBlock(-1, 0);
break;
}
case WXK_NUMPAD_RIGHT:
case WXK_RIGHT: {
if (dead) {
return;
}
unpause();
MoveBlock(1, 0);
break;
}
}
}
void TetrisPanel::NewGame() {
NewBlock();
score = 0;
lines = 0;
game_timer.Start();
unpause();
dead = false;
// Clear map
for (int y = 0; y < TETRIS_MAPHEIGHT; ++y) {
for (int x = 0; x < TETRIS_MAPWIDTH; ++x) {
map[x][y] = NO_COLOR;
}
}
AddScore(0); // Update title
}
void TetrisPanel::AddScore(int lines_added) {
lines += lines_added;
score += lines_added * lines_added * 10;
wxString title = "Canary Map Editor Tetris : ";
title << score << " points ";
title << lines << " lines";
((wxTopLevelWindow*)GetParent())->SetTitle(title);
}
void TetrisPanel::GameLoop(int time) {
MoveBlock(0, 1);
}
bool TetrisPanel::BlockCollisionTest(int mx, int my) const {
int nx = block.x + mx;
int ny = block.y + my;
for (int y = 0; y < 4; ++y) {
for (int x = 0; x < 4; ++x) {
if (block.structure[x][y] != NO_COLOR) {
if (nx + x < 0 || nx + x > TETRIS_MAPWIDTH - 1 || ny + y < 0 || ny + y > TETRIS_MAPHEIGHT - 1) {
return true;
}
}
}
}
for (int y = 0; y < TETRIS_MAPHEIGHT; ++y) {
for (int x = 0; x < TETRIS_MAPWIDTH; ++x) {
if (x >= nx && x < nx + 4 && y >= ny && y < ny + 4) {
if (map[x][y] != NO_COLOR && block.structure[x - nx][y - ny] != NO_COLOR) {
return true;
}
}
}
}
return false;
}
void TetrisPanel::RemoveRow(int row) {
for (int x = 0; x < TETRIS_MAPWIDTH; ++x) {
for (int y = row; y > 0; --y) { // Move all above one step down
map[x][y] = map[x][y - 1];
}
}
}
void TetrisPanel::NewBlock() {
block.x = TETRIS_MAPWIDTH / 2;
block.y = -1;
for (int y = 0; y < 4; ++y) {
for (int x = 0; x < 4; ++x) {
block.structure[x][y] = NO_COLOR;
}
}
switch (random(FIRST_BLOCK, LAST_BLOCK)) {
case BLOCK_TOWER: {
block.structure[1][0] = RED;
block.structure[1][1] = RED;
block.structure[1][2] = RED;
block.structure[1][3] = RED;
block.y = 0;
break;
}
default:
case BLOCK_SQUARE: {
block.structure[1][1] = BLUE;
block.structure[1][2] = BLUE;
block.structure[2][1] = BLUE;
block.structure[2][2] = BLUE;
break;
}
case BLOCK_TRIANGLE: {
block.structure[1][1] = STEEL;
block.structure[0][2] = STEEL;
block.structure[1][2] = STEEL;
block.structure[2][2] = STEEL;
break;
}
case BLOCK_Z: {
block.structure[0][1] = YELLOW;
block.structure[1][1] = YELLOW;
block.structure[1][2] = YELLOW;
block.structure[2][2] = YELLOW;
break;
}
case BLOCK_S: {
block.structure[2][1] = GREEN;
block.structure[1][1] = GREEN;
block.structure[1][2] = GREEN;
block.structure[0][2] = GREEN;
break;
}
case BLOCK_J: {
block.structure[1][1] = WHITE;
block.structure[2][1] = WHITE;
block.structure[2][2] = WHITE;
block.structure[2][3] = WHITE;
break;
}
case BLOCK_L: {
block.structure[2][1] = PURPLE;
block.structure[1][1] = PURPLE;
block.structure[1][2] = PURPLE;
block.structure[1][3] = PURPLE;
// break; missing add it?
}
}
}
void TetrisPanel::MoveBlock(int x, int y) {
if (BlockCollisionTest(x, y)) {
if (y == 1) { // moving down...
if (block.y < 1) { // Out of bounds!
dead = true;
g_gui.PopupDialog("Game Over", "You reached a score of " + i2ws(score) + "!", wxOK);
NewGame();
SetFocus();
} else {
// Freeze the old block onto the map
for (int y = 0; y < 4; ++y) {
for (int x = 0; x < 4; ++x) {
if (block.structure[x][y] != NO_COLOR) {
map[block.x + x][block.y + y] = block.structure[x][y];
}
}
}
// Any cleared rows?
int cleared = 0;
for (int y = 0; y < TETRIS_MAPHEIGHT; ++y) {
bool full = true;
for (int x = 0; x < TETRIS_MAPWIDTH; ++x) {
if (map[x][y] == NO_COLOR) {
full = false;
}
}
if (full) {
RemoveRow(y);
++cleared;
}
}
AddScore(cleared);
NewBlock();
}
} // If we're not moving down, we're not moving the block either
} else {
// No collision so move the block!
block.x += x;
block.y += y;
}
Refresh();
}
void TetrisPanel::RotateBlock() {
Block temp;
for (int y = 0; y < 4; ++y) {
for (int x = 0; x < 4; ++x) {
temp.structure[3 - x][y] = block.structure[y][x];
}
}
for (int y = 0; y < 4; ++y) {
for (int x = 0; x < 4; ++x) {
if (temp.structure[x][y] != NO_COLOR) {
if (block.x + x < 0 || block.x + x > TETRIS_MAPWIDTH - 1 || block.y + y < 0 || block.y + y > TETRIS_MAPHEIGHT - 1) {
return;
}
}
}
}
for (int y = 0; y < TETRIS_MAPWIDTH; ++y) {
for (int x = 0; x < TETRIS_MAPHEIGHT; ++x) {
if (x >= block.x && x < block.x + 4 && y >= block.y && y < block.y + 4) {
if (map[x][y] != NO_COLOR && temp.structure[x - block.x][y - block.y] != NO_COLOR) {
return;
}
}
}
}
for (int y = 0; y < 4; ++y) {
for (int x = 0; x < 4; ++x) {
block.structure[x][y] = temp.structure[x][y];
}
}
Refresh();
}
//=============================================================================
// SnakePanel - A window with a Snake game!
SnakePanel::SnakePanel(wxWindow* parent) :
GamePanel(parent, 16 * SNAKE_MAPWIDTH, 16 * SNAKE_MAPHEIGHT) {
NewGame();
}
SnakePanel::~SnakePanel() {
////
}
void SnakePanel::Render(wxDC &pdc) {
pdc.SetBackground(*wxBLACK_BRUSH);
pdc.Clear();
wxBrush snakebrush(wxColor(0, 0, 255));
wxBrush applebrush(wxColor(255, 0, 0));
double lblue = 1.0;
double lred = 0.5;
double lgreen = 0.0;
for (int y = 0; y < SNAKE_MAPHEIGHT; ++y) {
for (int x = 0; x < SNAKE_MAPWIDTH; ++x) {
if (map[x][y] == -1) { // Apple
pdc.SetBrush(applebrush);
pdc.DrawRectangle(x * 16, y * 16, 16, 16);
} else if (map[x][y] > 0) { // Snake
double snook = double(map[x][y]) / length;
snakebrush.SetColour(wxColor(
int(255.0 * (1.0 - abs(lred - snook))),
int(255.0 * (1.0 - abs(lgreen - snook))),
int(255.0 * (1.0 - abs(lblue - snook)))
)
);
pdc.SetBrush(snakebrush);
pdc.DrawRectangle(x * 16, y * 16, 16, 16);
}
}
}
}
void SnakePanel::OnKey(wxKeyEvent &event, bool down) {
if (!down) {
return;
}
int keyCode = event.GetKeyCode();
if (keyCode == WXK_SPACE) {
if (paused()) {
unpause();
} else {
pause();
}
} else if (!dead) {
switch (keyCode) {
case WXK_NUMPAD_UP:
case WXK_UP: {
unpause();
Move(NORTH);
break;
}
case WXK_NUMPAD_DOWN:
case WXK_DOWN: {
unpause();
Move(SOUTH);
break;
}
case WXK_NUMPAD_LEFT:
case WXK_LEFT: {
unpause();
Move(WEST);
break;
}
case WXK_NUMPAD_RIGHT:
case WXK_RIGHT: {
unpause();
Move(EAST);
break;
}
}
}
}
void SnakePanel::NewGame() {
length = 3;
game_timer.Start();
last_dir = NORTH;
unpause();
dead = false;
// Clear map
for (int y = 0; y < SNAKE_MAPHEIGHT; ++y) {
for (int x = 0; x < SNAKE_MAPWIDTH; ++x) {
map[x][y] = 0;
}
}
map[SNAKE_MAPWIDTH / 2][SNAKE_MAPHEIGHT / 2] = length;
NewApple();
UpdateTitle(); // Update title
}
void SnakePanel::UpdateTitle() {
wxString title = "Canary Map Editor Snake : ";
title << length << " segments";
((wxTopLevelWindow*)GetParent())->SetTitle(title);
}
void SnakePanel::GameLoop(int time) {
Move(last_dir);
}
void SnakePanel::NewApple() {
bool possible = false;
for (int y = 0; y < SNAKE_MAPHEIGHT; ++y) {
for (int x = 0; x < SNAKE_MAPWIDTH; ++x) {
if (map[x][y] == 0) {
possible = true;
}
if (possible) {
break;
}
}
if (possible) {
break;
}
}
if (possible) {
while (true) {
int x = random(0, SNAKE_MAPWIDTH - 1);
int y = random(0, SNAKE_MAPHEIGHT - 1);
if (map[x][y] == 0) {
map[x][y] = -1;
break;
}
}
}
}
void SnakePanel::Move(int dir) {
if ((last_dir == NORTH && dir == SOUTH) || (last_dir == WEST && dir == EAST) || (last_dir == EAST && dir == WEST) || (last_dir == SOUTH && dir == NORTH)) {
return;
}
int nx = 0, ny = 0;
int head_x = 0, head_y = 0;
for (int y = 0; y < SNAKE_MAPHEIGHT; ++y) {
for (int x = 0; x < SNAKE_MAPWIDTH; ++x) {
if (map[x][y] == length) {
head_x = x;
head_y = y;
}
}
}
switch (dir) {
case NORTH: {
nx = head_x;
ny = head_y - 1;
break;
}
case SOUTH: {
nx = head_x;
ny = head_y + 1;
break;
}
case WEST: {
nx = head_x - 1;
ny = head_y;
break;
}
case EAST: {
nx = head_x + 1;
ny = head_y;
break;
}
default:
return;
}
if (map[nx][ny] > 0 || nx < 0 || ny < 0 || nx >= SNAKE_MAPWIDTH || ny >= SNAKE_MAPHEIGHT) {
// Crash
dead = true;
g_gui.PopupDialog("Game Over", "You reached a length of " + i2ws(length) + "!", wxOK);
NewGame();
SetFocus();
} else {
// Walk!
if (map[nx][ny] == -1) {
// Took apple!
length += 1;
UpdateTitle();
NewApple();
} else {
for (int y = 0; y < SNAKE_MAPHEIGHT; ++y) {
for (int x = 0; x < SNAKE_MAPWIDTH; ++x) {
if (map[x][y] > 0) {
map[x][y] -= 1;
}
}
}
}
map[nx][ny] = length;
}
last_dir = dir;
Refresh();
}