1 /*
2 * This file is part of EasyRPG Player.
3 *
4 * EasyRPG Player is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * EasyRPG Player is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with EasyRPG Player. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 #include "bitmap.h"
19 #include <lcf/rpg/animation.h>
20 #include "output.h"
21 #include "game_battle.h"
22 #include "game_system.h"
23 #include "game_screen.h"
24 #include "game_map.h"
25 #include "main_data.h"
26 #include "filefinder.h"
27 #include "cache.h"
28 #include "battle_animation.h"
29 #include "baseui.h"
30 #include "spriteset_battle.h"
31 #include "player.h"
32 #include "options.h"
33 #include "drawable_mgr.h"
34
BattleAnimation(const lcf::rpg::Animation & anim,bool only_sound,int cutoff)35 BattleAnimation::BattleAnimation(const lcf::rpg::Animation& anim, bool only_sound, int cutoff) :
36 animation(anim), only_sound(only_sound)
37 {
38 num_frames = GetRealFrames() * 2;
39 if (cutoff >= 0 && cutoff < num_frames) {
40 num_frames = cutoff;
41 }
42
43 SetZ(Priority_BattleAnimation);
44
45 StringView name = animation.animation_name;
46 BitmapRef graphic;
47
48 if (name.empty()) return;
49
50 if (animation.large) {
51 FileRequestAsync* request = AsyncHandler::RequestFile("Battle2", name);
52 request->SetGraphicFile(true);
53 request_id = request->Bind(&BattleAnimation::OnBattle2SpriteReady, this);
54 request->Start();
55 } else {
56 FileRequestAsync* request = AsyncHandler::RequestFile("Battle", name);
57 request->SetGraphicFile(true);
58 request_id = request->Bind(&BattleAnimation::OnBattleSpriteReady, this);
59 request->Start();
60 }
61 }
62
Update()63 void BattleAnimation::Update() {
64 if (!IsDone() && (frame & 1) == 0) {
65 // Lookup any timed SFX (SE/flash/shake) data for this frame
66 for (auto& timing: animation.timings) {
67 if (timing.frame == GetRealFrame() + 1) {
68 ProcessAnimationTiming(timing);
69 }
70 }
71 }
72
73 UpdateScreenFlash();
74 UpdateTargetFlash();
75
76 SetFlashEffect(Main_Data::game_screen->GetFlashColor());
77
78 frame++;
79 }
80
OnBattleSpriteReady(FileRequestResult * result)81 void BattleAnimation::OnBattleSpriteReady(FileRequestResult* result) {
82 BitmapRef bitmap = Cache::Battle(result->file);
83 SetBitmap(bitmap);
84 SetSrcRect(Rect(0, 0, 0, 0));
85 }
86
OnBattle2SpriteReady(FileRequestResult * result)87 void BattleAnimation::OnBattle2SpriteReady(FileRequestResult* result) {
88 BitmapRef bitmap = Cache::Battle2(result->file);
89 SetBitmap(bitmap);
90 SetSrcRect(Rect(0, 0, 0, 0));
91 }
92
DrawAt(Bitmap & dst,int x,int y)93 void BattleAnimation::DrawAt(Bitmap& dst, int x, int y) {
94 if (IsDone()) {
95 return;
96 }
97
98 const lcf::rpg::AnimationFrame& anim_frame = animation.frames[GetRealFrame()];
99
100 std::vector<lcf::rpg::AnimationCellData>::const_iterator it;
101 for (it = anim_frame.cells.begin(); it != anim_frame.cells.end(); ++it) {
102 const lcf::rpg::AnimationCellData& cell = *it;
103
104 if (!cell.valid) {
105 // Skip unused cells (they are created by deleting cells in the
106 // animation editor, resulting in gaps)
107 continue;
108 }
109
110 SetX(invert ? x - cell.x : cell.x + x);
111 SetY(cell.y + y);
112 int sx = cell.cell_id % 5;
113 if (invert) sx = 4 - sx;
114 int sy = cell.cell_id / 5;
115 int size = animation.large ? 128 : 96;
116 SetSrcRect(Rect(sx * size, sy * size, size, size));
117 SetOx(size / 2);
118 SetOy(size / 2);
119 SetTone(Tone(cell.tone_red * 128 / 100,
120 cell.tone_green * 128 / 100,
121 cell.tone_blue * 128 / 100,
122 cell.tone_gray * 128 / 100));
123 SetOpacity(255 * (100 - cell.transparency) / 100);
124 SetZoomX(cell.zoom / 100.0);
125 SetZoomY(cell.zoom / 100.0);
126 SetFlipX(invert);
127 Sprite::Draw(dst);
128 }
129
130 if (anim_frame.cells.empty()) {
131 // Draw an empty sprite when no cell is available in the animation
132 SetSrcRect(Rect(0, 0, 0, 0));
133 Sprite::Draw(dst);
134 }
135 }
136
ProcessAnimationFlash(const lcf::rpg::AnimationTiming & timing)137 void BattleAnimation::ProcessAnimationFlash(const lcf::rpg::AnimationTiming& timing) {
138 if (IsOnlySound()) {
139 return;
140 }
141
142 if (timing.flash_scope == lcf::rpg::AnimationTiming::FlashScope_target) {
143 target_flash_timing = &timing - animation.timings.data();
144 } else if (timing.flash_scope == lcf::rpg::AnimationTiming::FlashScope_screen) {
145 screen_flash_timing = &timing - animation.timings.data();
146 }
147 }
148
ProcessAnimationTiming(const lcf::rpg::AnimationTiming & timing)149 void BattleAnimation::ProcessAnimationTiming(const lcf::rpg::AnimationTiming& timing) {
150 // Play the SE.
151 Main_Data::game_system->SePlay(timing.se);
152 if (IsOnlySound()) {
153 return;
154 }
155
156 // Flash.
157 ProcessAnimationFlash(timing);
158
159 // Shake (only happens in battle).
160 if (Game_Battle::IsBattleRunning()) {
161 switch (timing.screen_shake) {
162 case lcf::rpg::AnimationTiming::ScreenShake_nothing:
163 break;
164 case lcf::rpg::AnimationTiming::ScreenShake_target:
165 // FIXME: Estimate, see below for screen shake.
166 ShakeTargets(3, 5, 32);
167 break;
168 case lcf::rpg::AnimationTiming::ScreenShake_screen:
169 Game_Screen* screen = Main_Data::game_screen.get();
170 // FIXME: This is not proven accurate. Screen captures show that
171 // the shake effect lasts for 16 animation frames (32 real frames).
172 // The maximum offset observed was 6 or 7, which makes these numbers
173 // seem reasonable.
174 screen->ShakeOnce(3, 5, 32);
175 break;
176 }
177 }
178 }
179
CalculateFlashPower(int frames,int power)180 static int CalculateFlashPower(int frames, int power) {
181 // This algorithm was determined numerically by measuring the flash
182 // power for each frame of battle animation flashs.
183 int f = 7 - ((frames + 1) / 2);
184 return std::min(f * power / 6, 31);
185 }
186
UpdateFlashGeneric(int timing_idx,int & r,int & g,int & b,int & p)187 void BattleAnimation::UpdateFlashGeneric(int timing_idx, int& r, int& g, int& b, int& p) {
188 r = 0; g = 0; b = 0; p = 0;
189
190 if (timing_idx >= 0) {
191 auto& timing = animation.timings[timing_idx];
192 int start_frame = (timing.frame - 1) * 2;
193 int delta_frames = GetFrame() - start_frame;
194 if (delta_frames <= 10) {
195 r = timing.flash_red;
196 g = timing.flash_green;
197 b = timing.flash_blue;
198 p = CalculateFlashPower(delta_frames, timing.flash_power);
199 }
200 }
201 }
202
UpdateScreenFlash()203 void BattleAnimation::UpdateScreenFlash() {
204 int r, g, b, p;
205 UpdateFlashGeneric(screen_flash_timing, r, g, b, p);
206 Main_Data::game_screen->FlashOnce(r, g, b, p, 0);
207 }
208
UpdateTargetFlash()209 void BattleAnimation::UpdateTargetFlash() {
210 int r, g, b, p;
211 UpdateFlashGeneric(target_flash_timing, r, g, b, p);
212 FlashTargets(r, g, b, p);
213 }
214
215 // For handling the vertical position.
216 // (The first argument should be an lcf::rpg::Animation::Position,
217 // but the position member is an int, so take an int.)
CalculateOffset(int pos,int target_height)218 static int CalculateOffset(int pos, int target_height) {
219 switch (pos) {
220 case lcf::rpg::Animation::Position_down:
221 return target_height / 2;
222 case lcf::rpg::Animation::Position_up:
223 return -(target_height / 2);
224 default:
225 return 0;
226 }
227 }
228
229 /////////
230
BattleAnimationMap(const lcf::rpg::Animation & anim,Game_Character & target,bool global)231 BattleAnimationMap::BattleAnimationMap(const lcf::rpg::Animation& anim, Game_Character& target, bool global) :
232 BattleAnimation(anim), target(target), global(global)
233 {
234 }
235
Draw(Bitmap & dst)236 void BattleAnimationMap::Draw(Bitmap& dst) {
237 if (IsOnlySound()) {
238 return;
239 }
240
241 if (global) {
242 DrawGlobal(dst);
243 } else {
244 DrawSingle(dst);
245 }
246 }
247
DrawGlobal(Bitmap & dst)248 void BattleAnimationMap::DrawGlobal(Bitmap& dst) {
249 auto rect = Main_Data::game_screen->GetScreenEffectsRect();
250
251 for (int y = -1; y < 2; ++y) {
252 for (int x = -1; x < 2; ++x) {
253 DrawAt(dst, rect.width * x + rect.x, rect.height * y + rect.y);
254 }
255 }
256 }
257
DrawSingle(Bitmap & dst)258 void BattleAnimationMap::DrawSingle(Bitmap& dst) {
259 //If animation is targeted on the screen
260 if (animation.scope == lcf::rpg::Animation::Scope_screen) {
261 DrawAt(dst, SCREEN_TARGET_WIDTH / 2, SCREEN_TARGET_HEIGHT / 2);
262 return;
263 }
264 const int character_height = 24;
265 int vertical_center = target.GetScreenY(false, false) - character_height / 2;
266 int offset = CalculateOffset(animation.position, character_height);
267 DrawAt(dst, target.GetScreenX(), vertical_center + offset);
268 }
269
FlashTargets(int r,int g,int b,int p)270 void BattleAnimationMap::FlashTargets(int r, int g, int b, int p) {
271 target.Flash(r, g, b, p, 0);
272 }
273
ShakeTargets(int,int,int)274 void BattleAnimationMap::ShakeTargets(int /* str */, int /* spd */, int /* time */) {
275 }
276
277 /////////
278
BattleAnimationBattle(const lcf::rpg::Animation & anim,std::vector<Game_Battler * > battlers,bool only_sound,int cutoff_frame,bool set_invert)279 BattleAnimationBattle::BattleAnimationBattle(const lcf::rpg::Animation& anim, std::vector<Game_Battler*> battlers, bool only_sound, int cutoff_frame, bool set_invert) :
280 BattleAnimation(anim, only_sound, cutoff_frame), battlers(std::move(battlers))
281 {
282 invert = set_invert;
283 }
284
Draw(Bitmap & dst)285 void BattleAnimationBattle::Draw(Bitmap& dst) {
286 if (IsOnlySound())
287 return;
288 if (animation.scope == lcf::rpg::Animation::Scope_screen) {
289 DrawAt(dst, SCREEN_TARGET_WIDTH / 2, SCREEN_TARGET_HEIGHT / 3);
290 return;
291 }
292
293 for (auto* battler: battlers) {
294 const Sprite_Battler* sprite = battler->GetBattleSprite();
295 int offset = 0;
296 if (sprite) {
297 if (sprite->GetBitmap()) {
298 offset = CalculateOffset(animation.position, sprite->GetHeight());
299 } else {
300 offset = CalculateOffset(animation.position, GetAnimationCellHeight() / 2);
301 }
302 }
303 DrawAt(dst, battler->GetBattlePosition().x, battler->GetBattlePosition().y + offset);
304 }
305 }
FlashTargets(int r,int g,int b,int p)306 void BattleAnimationBattle::FlashTargets(int r, int g, int b, int p) {
307 for (auto& battler: battlers) {
308 battler->Flash(r, g, b, p, 0);
309 }
310 }
311
ShakeTargets(int str,int spd,int time)312 void BattleAnimationBattle::ShakeTargets(int str, int spd, int time) {
313 for (auto& battler: battlers) {
314 battler->ShakeOnce(str, spd, time);
315 }
316 }
317
BattleAnimationBattler(const lcf::rpg::Animation & anim,std::vector<Game_Battler * > battlers,bool only_sound,int cutoff_frame,bool set_invert)318 BattleAnimationBattler::BattleAnimationBattler(const lcf::rpg::Animation& anim, std::vector<Game_Battler*> battlers, bool only_sound, int cutoff_frame, bool set_invert) :
319 BattleAnimation(anim, only_sound, cutoff_frame), battlers(std::move(battlers))
320 {
321 invert = set_invert;
322 }
323
Draw(Bitmap & dst)324 void BattleAnimationBattler::Draw(Bitmap& dst) {
325 if (IsOnlySound())
326 return;
327 if (animation.scope == lcf::rpg::Animation::Scope_screen) {
328 DrawAt(dst, SCREEN_TARGET_WIDTH / 2, SCREEN_TARGET_HEIGHT / 3);
329 return;
330 }
331
332 for (auto* battler: battlers) {
333 SetFlashEffect(battler->GetFlashColor());
334 DrawAt(dst, battler->GetDisplayX(), battler->GetDisplayY());
335 }
336 }
337
FlashTargets(int r,int g,int b,int p)338 void BattleAnimationBattler::FlashTargets(int r, int g, int b, int p) {
339 for (auto& battler: battlers) {
340 battler->Flash(r, g, b, p, 0);
341 }
342 }
343
ShakeTargets(int str,int spd,int time)344 void BattleAnimationBattler::ShakeTargets(int str, int spd, int time) {
345 for (auto& battler: battlers) {
346 battler->ShakeOnce(str, spd, time);
347 }
348 }
349
ProcessAnimationTiming(const lcf::rpg::AnimationTiming & timing)350 void BattleAnimationBattler::ProcessAnimationTiming(const lcf::rpg::AnimationTiming& timing) {
351 // Play the SE.
352 Main_Data::game_system->SePlay(timing.se);
353 if (IsOnlySound()) {
354 return;
355 }
356
357 // Flash.
358 ProcessAnimationFlash(timing);
359 }
360
ProcessAnimationFlash(const lcf::rpg::AnimationTiming & timing)361 void BattleAnimationBattler::ProcessAnimationFlash(const lcf::rpg::AnimationTiming& timing) {
362 if (IsOnlySound()) {
363 return;
364 }
365
366 if (timing.flash_scope == lcf::rpg::AnimationTiming::FlashScope_screen) {
367 target_flash_timing = &timing - animation.timings.data();
368 }
369 }
370
UpdateScreenFlash()371 void BattleAnimationBattler::UpdateScreenFlash() {
372 int r, g, b, p;
373 UpdateFlashGeneric(screen_flash_timing, r, g, b, p);
374 if (r > 0 || g > 0 || b > 0 || p > 0) {
375 Main_Data::game_screen->FlashOnce(r, g, b, p, 0);
376 }
377 }
378
UpdateTargetFlash()379 void BattleAnimationBattler::UpdateTargetFlash() {
380 int r, g, b, p;
381 UpdateFlashGeneric(target_flash_timing, r, g, b, p);
382 if (r > 0 || g > 0 || b > 0 || p > 0) {
383 FlashTargets(r, g, b, p);
384 }
385 }
386
SetFrame(int frame)387 void BattleAnimation::SetFrame(int frame) {
388 // Reset pending flash.
389 int real_frame = frame / 2;
390 screen_flash_timing = -1;
391 target_flash_timing = -1;
392 for (auto& timing: animation.timings) {
393 if (timing.frame > real_frame + 1) {
394 break;
395 }
396 ProcessAnimationFlash(timing);
397 }
398
399 this->frame = frame;
400 }
401
SetInvert(bool inverted)402 void BattleAnimation::SetInvert(bool inverted) {
403 invert = inverted;
404 }
405