File: frost.cpp

package info (click to toggle)
libtcod 1.24.0%2Bdfsg-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 5,756 kB
  • sloc: ansic: 46,186; cpp: 13,523; python: 4,814; makefile: 44; sh: 25
file content (250 lines) | stat: -rw-r--r-- 9,424 bytes parent folder | download | duplicates (2)
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;
}