1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250
|
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif // __EMSCRIPTEN__
#include <SDL.h>
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <libtcod.hpp>
#include <limits>
#include <memory>
#include <vector>
static constexpr auto TAU = 6.28318530718f; // The number of radians in a turn. Same as `2 * PI`.
// Initialize the frost color gradient.
static constexpr std::array<int, 4> key_indexes{0, 60, 200, 255};
static constexpr std::array<tcod::ColorRGB, 4> key_colors{{{0, 0, 0}, {0, 0, 127}, {127, 127, 255}, {191, 191, 255}}};
static constexpr std::array<tcod::ColorRGB, 256> frost_gradient =
TCODColor::genMap<256>(key_colors, key_indexes); // Frost color gradient.
static constexpr auto GROW = 5000.0f;
static constexpr auto ANGLE_DELAY = 0.2f;
static constexpr auto FROST_LEVEL = 0.8f;
static constexpr auto SMOOTH = 0.3f;
static constexpr auto PIX_PER_FRAME = 6;
static constexpr auto RANGE = 10; // Maximum range from a frost origin to calculate and apply frost effects.
struct Frost {
explicit Frost(int x, int y) : origin_x{x}, origin_y{y} {}
int origin_x = 0, origin_y = 0; // The frost origin position.
int best_x = 0, best_y = 0; // A relative position to the frost closest to rx,ry.
int rx = 0, ry = 0; // A random relative direction.
int border = 0; // The total number of frames this effect has touched the border.
float timer = 0; // Seconds remaining until this particle changes direction.
};
class FrostManager {
public:
explicit FrostManager(int w, int h) : grid({w, h}), img({w, h}), width{w}, height{h} { clear(); }
inline void clear() {
for (auto& it : img) it = {0, 0, 0};
for (auto& it : grid) it = 0;
}
inline void addFrost(int x, int y) {
frost_objs.emplace_back(Frost(x, y));
setValue(x, y, 1.0f);
}
inline void update(float delta_time) {
auto update_func = [&](auto& it) { return !frost_update(it, delta_time); };
frost_objs.erase(std::remove_if(frost_objs.begin(), frost_objs.end(), update_func), frost_objs.end());
}
inline void render(tcod::Console& console) {
for (auto& it : frost_objs) frost_render(it);
TCOD_image_blit_2x(TCODImage(img).get_data(), console.get(), 0, 0, 0, 0, -1, -1);
}
/**
Returns true if x,y are within the bounds of this manager.
*/
inline bool in_bounds(int x, int y) const noexcept { return 0 <= x && 0 <= y && x < width && y < height; }
inline float getValue(int x, int y) const noexcept {
if (!(in_bounds(x, y))) return 0.0f;
return grid.at({x, y});
}
inline void setValue(int x, int y, float value) noexcept {
if (!(in_bounds(x, y))) return;
grid.at({x, y}) = value;
}
private:
inline float getValue(const Frost& frost, int x, int y) const noexcept {
return getValue(frost.origin_x - RANGE + x, frost.origin_y - RANGE + y);
}
inline void setValue(const Frost& frost, int x, int y, float value) noexcept {
setValue(frost.origin_x - RANGE + x, frost.origin_y - RANGE + y, value);
}
/**
Updates a frost particle with a given delta time.
Returns false if this particle is to be removed.
*/
inline bool frost_update(Frost& frost, float delta_time) {
for (int i = PIX_PER_FRAME; i > 0; i--) {
frost.timer -= delta_time;
if (frost.timer <= 0) {
// find a new random frost direction
const float random_angle = TCODRandom::getInstance()->getFloat(0.0f, TAU);
const float random_range = TCODRandom::getInstance()->getFloat(0, 2 * RANGE);
frost.timer = ANGLE_DELAY;
frost.rx = static_cast<int>(RANGE + random_range * cosf(random_angle));
frost.ry = static_cast<int>(RANGE + random_range * sinf(random_angle));
int minDist = std::numeric_limits<int>::max();
// find closest frost pixel
for (int cy = 1; cy < 2 * RANGE; cy++) {
for (int cx = 1; cx < 2 * RANGE; cx++) {
if (in_bounds(frost.origin_x - RANGE + cx, frost.origin_y - RANGE + cy)) {
if (getValue(frost, cx, cy) > FROST_LEVEL) {
const int dist = (cx - frost.rx) * (cx - frost.rx) + (cy - frost.ry) * (cy - frost.ry);
if (dist < minDist) {
minDist = dist;
frost.best_x = cx;
frost.best_y = cy;
}
}
}
}
}
}
// smoothing
for (int cy = 0; cy < 2 * RANGE + 1; cy++) {
if (frost.origin_y - RANGE + cy < height - 1 && frost.origin_y - RANGE + cy > 0) {
for (int cx = 0; cx < 2 * RANGE + 1; cx++) {
if (frost.origin_x - RANGE + cx < width - 1 && frost.origin_x - RANGE + cx > 0) {
if (getValue(frost, cx, cy) < 1.0f) {
float f = getValue(frost, cx, cy);
const float old_f = f;
f = std::max(f, getValue(frost, cx + 1, cy));
f = std::max(f, getValue(frost, cx - 1, cy));
f = std::max(f, getValue(frost, cx, cy + 1));
f = std::max(f, getValue(frost, cx, cy - 1));
setValue(frost, cx, cy, old_f + (f - old_f) * SMOOTH * delta_time);
}
}
}
}
}
int cur_x = frost.best_x;
int cur_y = frost.best_y;
// frosting
TCODLine::init(cur_x, cur_y, frost.rx, frost.ry);
TCODLine::step(&cur_x, &cur_y);
if (in_bounds(frost.origin_x - RANGE + cur_x, frost.origin_y - RANGE + cur_y)) {
const float frost_value = std::min(1.0f, getValue(frost, cur_x, cur_y) + GROW * delta_time);
setValue(frost, cur_x, cur_y, frost_value);
if (frost_value == 1.0f) {
frost.best_x = cur_x;
frost.best_y = cur_y;
if (frost.best_x == frost.rx && frost.best_y == frost.ry) frost.timer = 0.0f;
frost.timer = 0.0f;
if (cur_x == 0 || cur_x == 2 * RANGE || cur_y == 0 || cur_y == 2 * RANGE) {
frost.border++;
if (frost.border == 20) {
return false; // Delete this particle.
}
}
}
} else
frost.timer = 0.0f;
}
return true;
}
/**
Renders a frost particle.
*/
inline void frost_render(const Frost& frost) {
for (int cy = std::max(frost.origin_y - RANGE, 0); cy < std::min(frost.origin_y + RANGE + 1, height); ++cy) {
for (int cx = std::max(frost.origin_x - RANGE, 0); cx < std::min(frost.origin_x + RANGE + 1, width); ++cx) {
const float f = getValue(frost, cx - (frost.origin_x - RANGE), cy - (frost.origin_y - RANGE));
const int idx = std::max(0, std::min(static_cast<int>(f * 255), 255));
img.at({cx, cy}) = frost_gradient.at(idx);
}
}
}
std::vector<Frost> frost_objs; // A vector of frost effects.
tcod::Matrix<float, 2> grid; // A canvas for holding the freeze effect values.
tcod::Matrix<TCOD_ColorRGB, 2> img; // An image for storing the freeze colors.
int width, height; // The size of the managed frost map.
};
static constexpr int CONSOLE_WIDTH = 80;
static constexpr int CONSOLE_HEIGHT = 50;
tcod::ContextPtr context;
void main_loop() {
static uint32_t last_time_ms = SDL_GetTicks();
static FrostManager frostManager{CONSOLE_WIDTH * 2, CONSOLE_HEIGHT * 2};
static auto console = tcod::Console{CONSOLE_WIDTH, CONSOLE_HEIGHT};
frostManager.render(console);
context->present(console);
SDL_Event event;
while (SDL_PollEvent(&event)) {
switch (event.type) {
case SDL_KEYDOWN:
if (event.key.keysym.sym == SDLK_BACKSPACE) frostManager.clear();
break;
case SDL_MOUSEBUTTONDOWN: {
auto tile_xy = context->pixel_to_tile_coordinates(std::array<int, 2>{{event.motion.x, event.motion.y}});
if (event.button.button == SDL_BUTTON_LEFT) {
frostManager.addFrost(tile_xy.at(0) * 2, tile_xy.at(1) * 2);
}
}
case SDL_MOUSEMOTION: {
auto tile_xy = context->pixel_to_tile_coordinates(std::array<int, 2>{{event.motion.x, event.motion.y}});
if (event.motion.state & SDL_BUTTON_LMASK) {
frostManager.addFrost(tile_xy.at(0) * 2, tile_xy.at(1) * 2);
}
} break;
case SDL_QUIT:
std::exit(EXIT_SUCCESS);
break;
}
}
uint32_t current_time_ms = SDL_GetTicks();
int delta_time_ms = std::max<int>(0, current_time_ms - last_time_ms);
last_time_ms = current_time_ms;
frostManager.update(delta_time_ms / 1000.0f);
}
void on_quit() {
context = nullptr;
SDL_Quit();
}
int main(int argc, char** argv) {
std::atexit(on_quit);
SDL_LogSetAllPriority(SDL_LOG_PRIORITY_INFO);
auto tileset = tcod::load_tilesheet("data/fonts/terminal8x8_gs_tc.png", {32, 8}, tcod::CHARMAP_TCOD);
TCOD_ContextParams params{};
params.tcod_version = TCOD_COMPILEDVERSION;
params.tileset = tileset.get();
params.argc = argc;
params.argv = argv;
params.window_title = "frost test";
params.columns = CONSOLE_WIDTH;
params.rows = CONSOLE_HEIGHT;
params.sdl_window_flags = SDL_WINDOW_RESIZABLE;
params.renderer_type = TCOD_RENDERER_SDL2;
params.vsync = true;
try {
context = tcod::new_context(params);
} catch (const std::exception& e) {
std::cerr << e.what() << "\n";
return EXIT_FAILURE;
}
#ifdef __EMSCRIPTEN__
emscripten_set_main_loop(main_loop, 0, 0);
#else
while (true) main_loop();
#endif
return EXIT_SUCCESS;
}
|