Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Import from svn r385

  • Loading branch information...
commit 6ae58751782d55f517930fc0682d5285d188e2e2 1 parent 705b2bf
@clintbellanger authored
Showing with 15,228 additions and 0 deletions.
  1. +3 −0  Launch Flare OSX.command
  2. +1 −0  Launch Flare Windows.bat
  3. +85 −0 build/CMakeLists.txt
  4. +763 −0 src/Avatar.cpp
  5. +86 −0 src/Avatar.h
  6. +163 −0 src/CampaignManager.cpp
  7. +56 −0 src/CampaignManager.h
  8. +741 −0 src/Enemy.cpp
  9. +80 −0 src/Enemy.h
  10. +226 −0 src/EnemyManager.cpp
  11. +58 −0 src/EnemyManager.h
  12. +77 −0 src/FileParser.cpp
  13. +35 −0 src/FileParser.h
  14. +239 −0 src/FontEngine.cpp
  15. +59 −0 src/FontEngine.h
  16. +445 −0 src/GameEngine.cpp
  17. +83 −0 src/GameEngine.h
  18. +140 −0 src/GameSwitcher.cpp
  19. +54 −0 src/GameSwitcher.h
  20. +86 −0 src/Hazard.cpp
  21. +90 −0 src/Hazard.h
  22. +183 −0 src/HazardManager.cpp
  23. +39 −0 src/HazardManager.h
  24. +374 −0 src/InputState.cpp
  25. +70 −0 src/InputState.h
  26. +572 −0 src/ItemDatabase.cpp
  27. +151 −0 src/ItemDatabase.h
  28. +154 −0 src/ItemStorage.cpp
  29. +41 −0 src/ItemStorage.h
  30. +486 −0 src/LootManager.cpp
  31. +103 −0 src/LootManager.h
  32. +155 −0 src/MapCollision.cpp
  33. +54 −0 src/MapCollision.h
  34. +572 −0 src/MapIso.cpp
  35. +130 −0 src/MapIso.h
  36. +358 −0 src/MenuActionBar.cpp
  37. +79 −0 src/MenuActionBar.h
  38. +454 −0 src/MenuCharacter.cpp
  39. +47 −0 src/MenuCharacter.h
  40. +99 −0 src/MenuEnemy.cpp
  41. +41 −0 src/MenuEnemy.h
  42. +108 −0 src/MenuExperience.cpp
  43. +53 −0 src/MenuExperience.h
  44. +336 −0 src/MenuGameSlots.cpp
  45. +77 −0 src/MenuGameSlots.h
  46. +102 −0 src/MenuHPMP.cpp
  47. +37 −0 src/MenuHPMP.h
  48. +98 −0 src/MenuHUDLog.cpp
  49. +44 −0 src/MenuHUDLog.h
  50. +496 −0 src/MenuInventory.cpp
  51. +94 −0 src/MenuInventory.h
  52. +68 −0 src/MenuItemStorage.cpp
  53. +40 −0 src/MenuItemStorage.h
  54. +235 −0 src/MenuLog.cpp
  55. +65 −0 src/MenuLog.h
  56. +530 −0 src/MenuManager.cpp
  57. +96 −0 src/MenuManager.h
  58. +49 −0 src/MenuMiniMap.cpp
  59. +30 −0 src/MenuMiniMap.h
  60. +253 −0 src/MenuPowers.cpp
  61. +51 −0 src/MenuPowers.h
  62. +120 −0 src/MenuTalker.cpp
  63. +49 −0 src/MenuTalker.h
  64. +83 −0 src/MenuTitle.cpp
  65. +42 −0 src/MenuTitle.h
  66. +98 −0 src/MenuTooltip.cpp
  67. +50 −0 src/MenuTooltip.h
  68. +130 −0 src/MenuVendor.cpp
  69. +59 −0 src/MenuVendor.h
  70. +388 −0 src/NPC.cpp
  71. +84 −0 src/NPC.h
  72. +126 −0 src/NPCManager.cpp
  73. +46 −0 src/NPCManager.h
  74. +968 −0 src/PowerManager.cpp
  75. +260 −0 src/PowerManager.h
  76. +131 −0 src/QuestLog.cpp
  77. +38 −0 src/QuestLog.h
  78. +223 −0 src/SaveLoad.cpp
  79. +110 −0 src/Settings.cpp
  80. +41 −0 src/Settings.h
  81. +446 −0 src/StatBlock.cpp
  82. +211 −0 src/StatBlock.h
  83. +87 −0 src/TileSet.cpp
  84. +46 −0 src/TileSet.h
  85. +200 −0 src/Utils.cpp
  86. +106 −0 src/Utils.h
  87. +173 −0 src/UtilsParsing.cpp
  88. +31 −0 src/UtilsParsing.h
  89. +112 −0 src/WidgetButton.cpp
  90. +47 −0 src/WidgetButton.h
  91. +129 −0 src/main.cpp
View
3  Launch Flare OSX.command
@@ -0,0 +1,3 @@
+#!/bin/bash
+cd "`dirname "$0"`"
+./flare
View
1  Launch Flare Windows.bat
@@ -0,0 +1 @@
+flare.exe
View
85 build/CMakeLists.txt
@@ -0,0 +1,85 @@
+Project (Flare)
+cmake_minimum_required (VERSION 2.6)
+
+set (CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/..)
+
+Set (PACKAGE "FLARE")
+Set (VERSION "0.13")
+
+
+# Detect missing dependencies
+
+Find_Package (SDL REQUIRED)
+If (NOT SDL_FOUND)
+ Message (FATAL_ERROR "Couldn't find SDL development files. On Debian-based systems (such as Ubuntu) you should install the 'libsdl1.2-dev' package.")
+Else (NOT SDL_FOUND)
+ Include_Directories (${SDL_INCLUDE_DIR})
+EndIf (NOT SDL_FOUND)
+
+Find_Package (SDL_mixer REQUIRED)
+If (NOT SDLMIXER_FOUND)
+ Message (FATAL_ERROR "Couldn't find SDL-mixer development files. On Debian-based systems (such as Ubuntu) you should install the 'libsdl-mixer1.2-dev' package.")
+Else (NOT SDLMIXER_FOUND)
+ Include_Directories (${SDLMIXER_INCLUDE_DIR})
+EndIf (NOT SDLMIXER_FOUND)
+
+Find_Package (SDL_image REQUIRED)
+If (NOT SDLIMAGE_FOUND)
+ Message (FATAL_ERROR "Couldn't find SDL-image development files. On Debian-based systems (such as Ubuntu) you should install the 'libsdl-image1.2-dev' package.")
+Else (NOT SDLIMAGE_FOUND)
+ Include_Directories (${SDLIMAGE_INCLUDE_DIR})
+EndIf (NOT SDLIMAGE_FOUND)
+
+
+# Sources
+
+Set (FLARE_SOURCES
+ ../src/Avatar.cpp
+ ../src/CampaignManager.cpp
+ ../src/Enemy.cpp
+ ../src/EnemyManager.cpp
+ ../src/FileParser.cpp
+ ../src/FontEngine.cpp
+ ../src/GameEngine.cpp
+ ../src/GameSwitcher.cpp
+ ../src/Hazard.cpp
+ ../src/HazardManager.cpp
+ ../src/InputState.cpp
+ ../src/ItemDatabase.cpp
+ ../src/ItemStorage.cpp
+ ../src/LootManager.cpp
+ ../src/MapCollision.cpp
+ ../src/MapIso.cpp
+ ../src/MenuActionBar.cpp
+ ../src/MenuCharacter.cpp
+ ../src/MenuEnemy.cpp
+ ../src/MenuExperience.cpp
+ ../src/MenuHPMP.cpp
+ ../src/MenuHUDLog.cpp
+ ../src/MenuInventory.cpp
+ ../src/MenuItemStorage.cpp
+ ../src/MenuGameSlots.cpp
+ ../src/MenuLog.cpp
+ ../src/MenuManager.cpp
+ ../src/MenuMiniMap.cpp
+ ../src/MenuPowers.cpp
+ ../src/MenuTalker.cpp
+ ../src/MenuTitle.cpp
+ ../src/MenuTooltip.cpp
+ ../src/MenuVendor.cpp
+ ../src/NPC.cpp
+ ../src/NPCManager.cpp
+ ../src/PowerManager.cpp
+ ../src/QuestLog.cpp
+ ../src/SaveLoad.cpp
+ ../src/Settings.cpp
+ ../src/StatBlock.cpp
+ ../src/TileSet.cpp
+ ../src/Utils.cpp
+ ../src/UtilsParsing.cpp
+ ../src/WidgetButton.cpp
+ ../src/main.cpp
+)
+
+Add_Executable (flare ${FLARE_SOURCES})
+Target_Link_Libraries (flare ${SDL_LIBRARY} ${SDLMIXER_LIBRARY} ${SDLIMAGE_LIBRARY} SDLmain)
View
763 src/Avatar.cpp
@@ -0,0 +1,763 @@
+/**
+ * class Avatar
+ *
+ * Contains logic and rendering routines for the player avatar.
+ *
+ * @author Clint Bellanger
+ * @license GPL
+ */
+
+#include "Avatar.h"
+
+Avatar::Avatar(PowerManager *_powers, InputState *_inp, MapIso *_map) {
+ powers = _powers;
+ inp = _inp;
+ map = _map;
+
+ loadSounds();
+
+ init();
+
+ // default hero animation data
+ stats.cooldown = 4;
+ stats.anim_stance_position = 0;
+ stats.anim_stance_frames = 4;
+ stats.anim_stance_duration = 6;
+ stats.anim_run_position = 4;
+ stats.anim_run_frames = 8;
+ stats.anim_run_duration = 2;
+ stats.anim_melee_position = 12;
+ stats.anim_melee_frames = 4;
+ stats.anim_melee_duration = 3;
+ stats.anim_ment_position = 24;
+ stats.anim_ment_frames = 4;
+ stats.anim_ment_duration = 3;
+ stats.anim_ranged_position = 28;
+ stats.anim_ranged_frames = 4;
+ stats.anim_ranged_duration = 3;
+ stats.anim_block_position = 16;
+ stats.anim_block_frames = 2;
+ stats.anim_block_duration = 2;
+ stats.anim_hit_position = 18;
+ stats.anim_hit_frames = 2;
+ stats.anim_hit_duration = 2;
+ stats.anim_die_position = 18;
+ stats.anim_die_frames = 6;
+ stats.anim_die_duration = 3;
+
+}
+
+void Avatar::init() {
+ // other init
+ sprites = 0;
+ stats.cur_state = AVATAR_STANCE;
+ stats.pos.x = map->spawn.x;
+ stats.pos.y = map->spawn.y;
+ stats.direction = map->spawn_dir;
+ current_power = -1;
+
+ stats.cur_frame = 1;
+ stats.disp_frame = 0;
+ lockSwing = false;
+ lockCast = false;
+ lockShoot = false;
+
+ stats.name = "Unknown";
+ stats.hero = true;
+ stats.level = 1;
+ stats.xp = 0;
+ stats.physical = 1;
+ stats.mental = 1;
+ stats.offense = 1;
+ stats.defense = 1;
+ stats.speed = 14;
+ stats.dspeed = 10;
+ stats.recalc();
+
+ log_msg = "";
+
+ stats.cooldown_ticks = 0;
+
+ haz = NULL;
+
+ img_main = "";
+ img_armor = "";
+ img_off = "";
+
+}
+
+void Avatar::loadGraphics(string _img_main, string _img_armor, string _img_off) {
+ SDL_Surface *gfx_main = NULL;
+ SDL_Surface *gfx_off = NULL;
+ SDL_Rect src;
+ SDL_Rect dest;
+
+ // Default appearance
+ if (_img_armor == "") _img_armor = "clothes";
+
+ // Check if we really need to change the graphics
+ if (_img_main != img_main || _img_armor != img_armor || _img_off != img_off) {
+ img_main = _img_main;
+ img_armor = _img_armor;
+ img_off = _img_off;
+
+ // composite the hero graphic
+ if (sprites) SDL_FreeSurface(sprites);
+ sprites = IMG_Load(("images/avatar/male/" + img_armor + ".png").c_str());
+ if (img_main != "") gfx_main = IMG_Load(("images/avatar/male/" + img_main + ".png").c_str());
+ if (img_off != "") gfx_off = IMG_Load(("images/avatar/male/" + img_off + ".png").c_str());
+
+ SDL_SetColorKey( sprites, SDL_SRCCOLORKEY, SDL_MapRGB(sprites->format, 255, 0, 255) );
+ if (gfx_main) SDL_SetColorKey( gfx_main, SDL_SRCCOLORKEY, SDL_MapRGB(gfx_main->format, 255, 0, 255) );
+ if (gfx_off) SDL_SetColorKey( gfx_off, SDL_SRCCOLORKEY, SDL_MapRGB(gfx_off->format, 255, 0, 255) );
+
+ // assuming the hero is right-handed, we know the layer z-order
+ src.w = dest.w = 4096;
+ src.h = dest.h = 256;
+ src.x = dest.x = 0;
+ src.y = dest.y = 0;
+ if (gfx_main) SDL_BlitSurface(gfx_main, &src, sprites, &dest);
+ src.y = dest.y = 768;
+ if (gfx_main) SDL_BlitSurface(gfx_main, &src, sprites, &dest);
+ src.h = dest.h = 1024;
+ src.y = dest.y = 0;
+ if (gfx_off) SDL_BlitSurface(gfx_off, &src, sprites, &dest);
+ src.h = dest.h = 512;
+ src.y = dest.y = 256;
+ if (gfx_main) SDL_BlitSurface(gfx_main, &src, sprites, &dest);
+
+ if (gfx_main) SDL_FreeSurface(gfx_main);
+ if (gfx_off) SDL_FreeSurface(gfx_off);
+
+ // optimize
+ SDL_Surface *cleanup = sprites;
+ sprites = SDL_DisplayFormatAlpha(sprites);
+ SDL_FreeSurface(cleanup);
+ }
+}
+
+void Avatar::loadSounds() {
+ sound_melee = Mix_LoadWAV("soundfx/melee_attack.ogg");
+ sound_hit = Mix_LoadWAV("soundfx/male_hit.ogg");
+ sound_die = Mix_LoadWAV("soundfx/male_die.ogg");
+ sound_block = Mix_LoadWAV("soundfx/powers/block.ogg");
+ sound_steps[0] = Mix_LoadWAV("soundfx/step_echo1.ogg");
+ sound_steps[1] = Mix_LoadWAV("soundfx/step_echo2.ogg");
+ sound_steps[2] = Mix_LoadWAV("soundfx/step_echo3.ogg");
+ sound_steps[3] = Mix_LoadWAV("soundfx/step_echo4.ogg");
+ level_up = Mix_LoadWAV("soundfx/level_up.ogg");
+
+ if (!sound_melee || !sound_hit || !sound_die || !sound_steps[0] || !level_up) {
+ printf("Mix_LoadWAV: %s\n", Mix_GetError());
+ SDL_Quit();
+ }
+
+}
+
+bool Avatar::pressing_move() {
+ if(MOUSE_MOVE) {
+ return inp->pressing[MAIN1];
+ } else {
+ return inp->pressing[UP] || inp->pressing[DOWN] || inp->pressing[LEFT] || inp->pressing[RIGHT];
+ }
+}
+
+/**
+ * move()
+ * Apply speed to the direction faced.
+ *
+ * @return Returns false if wall collision, otherwise true.
+ */
+bool Avatar::move() {
+
+ if (stats.immobilize_duration > 0) return false;
+
+ int speed_diagonal = stats.dspeed;
+ int speed_straight = stats.speed;
+
+ if (stats.slow_duration > 0) {
+ speed_diagonal /= 2;
+ speed_straight /= 2;
+ }
+ else if (stats.haste_duration > 0) {
+ speed_diagonal *= 2;
+ speed_straight *= 2;
+ }
+
+ switch (stats.direction) {
+ case 0:
+ return map->collider.move(stats.pos.x, stats.pos.y, -1, 1, speed_diagonal);
+ case 1:
+ return map->collider.move(stats.pos.x, stats.pos.y, -1, 0, speed_straight);
+ case 2:
+ return map->collider.move(stats.pos.x, stats.pos.y, -1, -1, speed_diagonal);
+ case 3:
+ return map->collider.move(stats.pos.x, stats.pos.y, 0, -1, speed_straight);
+ case 4:
+ return map->collider.move(stats.pos.x, stats.pos.y, 1, -1, speed_diagonal);
+ case 5:
+ return map->collider.move(stats.pos.x, stats.pos.y, 1, 0, speed_straight);
+ case 6:
+ return map->collider.move(stats.pos.x, stats.pos.y, 1, 1, speed_diagonal);
+ case 7:
+ return map->collider.move(stats.pos.x, stats.pos.y, 0, 1, speed_straight);
+ }
+ return true;
+}
+
+void Avatar::set_direction() {
+ // handle direction changes
+ if(MOUSE_MOVE) {
+ Point target = screen_to_map(inp->mouse.x, inp->mouse.y, stats.pos.x, stats.pos.y);
+ stats.direction = face(target.x, target.y);
+ } else {
+ if(inp->pressing[UP] && inp->pressing[LEFT]) stats.direction = 1;
+ else if(inp->pressing[UP] && inp->pressing[RIGHT]) stats.direction = 3;
+ else if(inp->pressing[DOWN] && inp->pressing[RIGHT]) stats.direction = 5;
+ else if(inp->pressing[DOWN] && inp->pressing[LEFT]) stats.direction = 7;
+ else if(inp->pressing[LEFT]) stats.direction = 0;
+ else if(inp->pressing[UP]) stats.direction = 2;
+ else if(inp->pressing[RIGHT]) stats.direction = 4;
+ else if(inp->pressing[DOWN]) stats.direction = 6;
+ }
+}
+
+
+/**
+ * Change direction to face the target map location
+ */
+int Avatar::face(int mapx, int mapy) {
+
+ // inverting Y to convert map coordinates to standard cartesian coordinates
+ int dx = mapx - stats.pos.x;
+ int dy = stats.pos.y - mapy;
+
+ // avoid div by zero
+ if (dx == 0) {
+ if (dy > 0) return 3;
+ else return 7;
+ }
+
+ float slope = ((float)dy)/((float)dx);
+ if (0.5 <= slope && slope <= 2.0) {
+ if (dy > 0) return 4;
+ else return 0;
+ }
+ if (-0.5 <= slope && slope <= 0.5) {
+ if (dx > 0) return 5;
+ else return 1;
+ }
+ if (-2.0 <= slope && slope <= -0.5) {
+ if (dx > 0) return 6;
+ else return 2;
+ }
+ if (2 <= slope || -2 >= slope) {
+ if (dy > 0) return 3;
+ else return 7;
+ }
+ return stats.direction;
+}
+
+
+/**
+ * logic()
+ * Handle a single frame. This includes:
+ * - move the avatar based on buttons pressed
+ * - calculate the next frame of animation
+ * - calculate camera position based on avatar position
+ *
+ * @param power_index The actionbar power activated. -1 means no power.
+ */
+void Avatar::logic(int actionbar_power, bool restrictPowerUse) {
+
+ Point target;
+ int stepfx;
+ stats.logic();
+ if (stats.stun_duration > 0) return;
+ bool allowed_to_move;
+ bool allowed_to_use_power;
+ int max_frame;
+ int mid_frame;
+
+ // check level up
+ if (stats.level < 17 && stats.xp >= stats.xp_table[stats.level]) {
+ stats.level++;
+ stringstream ss;
+ ss << "Congratulations, you have reached level " << stats.level << "! You may increase one attribute through the Character Menu.";
+ log_msg = ss.str();
+ stats.recalc();
+ Mix_PlayChannel(-1, level_up, 0);
+ }
+
+ // check for bleeding spurt
+ if (stats.bleed_duration % 30 == 1) {
+ powers->activate(POWER_SPARK_BLOOD, &stats, stats.pos);
+ }
+ // check for bleeding to death
+ if (stats.hp == 0 && !(stats.cur_state == AVATAR_DEAD)) {
+ stats.cur_state = AVATAR_DEAD;
+ stats.cur_frame = 0;
+ }
+
+ // assist mouse movement
+ if (!inp->pressing[MAIN1]) drag_walking = false;
+
+ switch(stats.cur_state) {
+ case AVATAR_STANCE:
+
+ // handle animation
+ stats.cur_frame++;
+ // stance is a back/forth animation
+ mid_frame = stats.anim_stance_frames * stats.anim_stance_duration;
+ max_frame = mid_frame + mid_frame;
+ if (stats.cur_frame >= max_frame) stats.cur_frame = 0;
+ if (stats.cur_frame >= mid_frame)
+ stats.disp_frame = (max_frame -1 - stats.cur_frame) / stats.anim_stance_duration + stats.anim_stance_position;
+ else
+ stats.disp_frame = stats.cur_frame / stats.anim_stance_duration + stats.anim_stance_position;
+
+ // allowed to move or use powers?
+ if (MOUSE_MOVE) {
+ allowed_to_move = restrictPowerUse && (!inp->lock[MAIN1] || drag_walking);
+ allowed_to_use_power = !allowed_to_move;
+ }
+ else {
+ allowed_to_move = true;
+ allowed_to_use_power = true;
+ }
+
+ // handle transitions to RUN
+ if (allowed_to_move)
+ set_direction();
+
+ if (pressing_move() && allowed_to_move) {
+ if (MOUSE_MOVE && inp->pressing[MAIN1]) {
+ inp->lock[MAIN1] = true;
+ drag_walking = true;
+ }
+
+ if (move()) { // no collision
+ stats.cur_frame = 1;
+ stats.cur_state = AVATAR_RUN;
+ break;
+ }
+
+ }
+ // handle power usage
+ if (allowed_to_use_power && actionbar_power != -1 && stats.cooldown_ticks == 0) {
+ target = screen_to_map(inp->mouse.x, inp->mouse.y + powers->powers[actionbar_power].aim_assist, stats.pos.x, stats.pos.y);
+
+ // check requirements
+ if (powers->powers[actionbar_power].requires_mp && stats.mp <= 0)
+ break;
+ if (powers->powers[actionbar_power].requires_physical_weapon && !stats.wielding_physical)
+ break;
+ if (powers->powers[actionbar_power].requires_mental_weapon && !stats.wielding_mental)
+ break;
+ if (powers->powers[actionbar_power].requires_offense_weapon && !stats.wielding_offense)
+ break;
+ if (powers->powers[actionbar_power].requires_los && !map->collider.line_of_sight(stats.pos.x, stats.pos.y, target.x, target.y))
+ break;
+ if (powers->powers[actionbar_power].requires_empty_target && !map->collider.is_empty(target.x, target.y))
+ break;
+
+ current_power = actionbar_power;
+ act_target.x = target.x;
+ act_target.y = target.y;
+
+ // is this a power that requires changing direction?
+ if (powers->powers[current_power].face) {
+ stats.direction = face(target.x, target.y);
+ }
+
+ // handle melee powers
+ if (powers->powers[current_power].new_state == POWSTATE_SWING) {
+ stats.cur_frame = 0;
+ stats.cur_state = AVATAR_MELEE;
+ break;
+ }
+ // handle ranged powers
+ if (powers->powers[current_power].new_state == POWSTATE_SHOOT) {
+ stats.cur_frame = 0;
+ stats.cur_state = AVATAR_SHOOT;
+ break;
+ }
+ // handle ment powers
+ if (powers->powers[current_power].new_state == POWSTATE_CAST) {
+ stats.cur_frame = 0;
+ stats.cur_state = AVATAR_CAST;
+ break;
+ }
+ if (powers->powers[current_power].new_state == POWSTATE_BLOCK) {
+ stats.cur_frame = 0;
+ stats.cur_state = AVATAR_BLOCK;
+ stats.blocking = true;
+ break;
+ }
+ }
+
+ break;
+
+ case AVATAR_RUN:
+
+ // handle animation
+ stats.cur_frame++;
+ // run is a looped animation
+ max_frame = stats.anim_run_frames * stats.anim_run_duration;
+ if (stats.cur_frame >= max_frame) stats.cur_frame = 0;
+ stats.disp_frame = (stats.cur_frame / stats.anim_run_duration) + stats.anim_run_position;
+
+ stepfx = rand() % 4;
+
+ if (stats.cur_frame == 0 || stats.cur_frame == max_frame/2) {
+ Mix_PlayChannel(-1, sound_steps[stepfx], 0);
+ }
+
+ // allowed to move or use powers?
+ if (MOUSE_MOVE) {
+ allowed_to_use_power = !(restrictPowerUse && !inp->lock[MAIN1]);
+ }
+ else {
+ allowed_to_use_power = true;
+ }
+
+ // handle direction changes
+ set_direction();
+
+ // handle transition to STANCE
+ if (!pressing_move()) {
+ stats.cur_state = AVATAR_STANCE;
+ break;
+ }
+ else if (!move()) { // collide with wall
+ stats.cur_state = AVATAR_STANCE;
+ break;
+ }
+
+ // handle power usage
+ if (allowed_to_use_power && actionbar_power != -1 && stats.cooldown_ticks == 0) {
+
+ target = screen_to_map(inp->mouse.x, inp->mouse.y + powers->powers[actionbar_power].aim_assist, stats.pos.x, stats.pos.y);
+
+ // check requirements
+ if (powers->powers[actionbar_power].requires_mp && stats.mp <= 0)
+ break;
+ if (powers->powers[actionbar_power].requires_physical_weapon && !stats.wielding_physical)
+ break;
+ if (powers->powers[actionbar_power].requires_mental_weapon && !stats.wielding_mental)
+ break;
+ if (powers->powers[actionbar_power].requires_offense_weapon && !stats.wielding_offense)
+ break;
+ if (powers->powers[actionbar_power].requires_los && !map->collider.line_of_sight(stats.pos.x, stats.pos.y, target.x, target.y))
+ break;
+ if (powers->powers[actionbar_power].requires_empty_target && !map->collider.is_empty(target.x, target.y))
+ break;
+
+ current_power = actionbar_power;
+ act_target.x = target.x;
+ act_target.y = target.y;
+
+ // is this a power that requires changing direction?
+ if (powers->powers[current_power].face) {
+ stats.direction = face(target.x, target.y);
+ }
+
+ // handle melee powers
+ if (powers->powers[current_power].new_state == POWSTATE_SWING) {
+ stats.cur_frame = 0;
+ stats.cur_state = AVATAR_MELEE;
+ break;
+ }
+ // handle ranged powers
+ if (powers->powers[current_power].new_state == POWSTATE_SHOOT) {
+ stats.cur_frame = 0;
+ stats.cur_state = AVATAR_SHOOT;
+ break;
+ }
+ // handle ment powers
+ if (powers->powers[current_power].new_state == POWSTATE_CAST) {
+ stats.cur_frame = 0;
+ stats.cur_state = AVATAR_CAST;
+ break;
+ }
+ if (powers->powers[current_power].new_state == POWSTATE_BLOCK) {
+ stats.cur_frame = 0;
+ stats.cur_state = AVATAR_BLOCK;
+ stats.blocking = true;
+ break;
+ }
+ }
+
+ break;
+
+ case AVATAR_MELEE:
+
+ // handle animation
+ stats.cur_frame++;
+ // melee is a play-once animation
+ max_frame = stats.anim_melee_frames * stats.anim_melee_duration;
+ stats.disp_frame = (stats.cur_frame / stats.anim_melee_duration) + stats.anim_melee_position;
+
+ if (stats.cur_frame == 1) {
+ Mix_PlayChannel(-1, sound_melee, 0);
+ }
+
+ // do power
+ if (stats.cur_frame == max_frame/2) {
+ powers->activate(current_power, &stats, act_target);
+ }
+
+ if (stats.cur_frame == max_frame-1) {
+ stats.cur_frame = 0;
+ stats.cur_state = AVATAR_STANCE;
+ if (stats.haste_duration == 0) stats.cooldown_ticks += stats.cooldown;
+ }
+ break;
+
+ case AVATAR_CAST:
+
+ // handle animation
+ stats.cur_frame++;
+ max_frame = stats.anim_ment_frames * stats.anim_ment_duration;
+ stats.disp_frame = (stats.cur_frame / stats.anim_ment_duration) + stats.anim_ment_position;
+
+ // do power
+ if (stats.cur_frame == max_frame/2) {
+ powers->activate(current_power, &stats, act_target);
+ }
+
+ if (stats.cur_frame == max_frame-1) {
+ stats.cur_frame = 0;
+ stats.cur_state = AVATAR_STANCE;
+ if (stats.haste_duration == 0) stats.cooldown_ticks += stats.cooldown;
+ }
+ break;
+
+
+ case AVATAR_SHOOT:
+
+ // handle animation
+ stats.cur_frame++;
+ max_frame = stats.anim_ranged_frames * stats.anim_ranged_duration;
+ stats.disp_frame = (stats.cur_frame / stats.anim_ranged_duration) + stats.anim_ranged_position;
+
+ // do power
+ if (stats.cur_frame == max_frame/2) {
+ powers->activate(current_power, &stats, act_target);
+ }
+
+ if (stats.cur_frame == max_frame-1) {
+ stats.cur_frame = 0;
+ stats.cur_state = AVATAR_STANCE;
+ if (stats.haste_duration == 0) stats.cooldown_ticks += stats.cooldown;
+ }
+ break;
+
+ case AVATAR_BLOCK:
+
+ max_frame = stats.anim_block_frames * stats.anim_block_duration -1;
+ if (stats.cur_frame < max_frame) stats.cur_frame++;
+ stats.disp_frame = (stats.cur_frame / stats.anim_block_duration) + stats.anim_block_position;
+
+ if (powers->powers[actionbar_power].new_state != POWSTATE_BLOCK) {
+ stats.cur_frame = 0;
+ stats.cur_state = AVATAR_STANCE;
+ stats.blocking = false;
+ }
+ break;
+
+ case AVATAR_HIT:
+ stats.cur_frame++;
+
+ // hit is a back/forth animation
+ mid_frame = stats.anim_hit_frames * stats.anim_hit_duration;
+ max_frame = mid_frame + mid_frame;
+ if (stats.cur_frame >= mid_frame)
+ stats.disp_frame = (max_frame -1 - stats.cur_frame) / stats.anim_hit_duration + stats.anim_hit_position;
+ else
+ stats.disp_frame = stats.cur_frame / stats.anim_hit_duration + stats.anim_hit_position;
+
+ if (stats.cur_frame >= max_frame-1) {
+ stats.cur_state = AVATAR_STANCE;
+ stats.cur_frame = 0;
+ }
+
+ break;
+
+ case AVATAR_DEAD:
+
+ max_frame = (stats.anim_die_frames-1) * stats.anim_die_duration;
+ if (stats.cur_frame < max_frame) stats.cur_frame++;
+ if (stats.cur_frame == max_frame) stats.corpse = true;
+ stats.disp_frame = (stats.cur_frame / stats.anim_die_duration) + stats.anim_die_position;
+
+ if (stats.cur_frame == 1) {
+ Mix_PlayChannel(-1, sound_die, 0);
+ log_msg = "You are defeated. You lose half your gold. Press Enter to continue.";
+ }
+
+ // allow respawn with Accept
+ if (inp->pressing[ACCEPT]) {
+ stats.hp = stats.maxhp;
+ stats.mp = stats.maxmp;
+ stats.alive = true;
+ stats.corpse = false;
+ stats.cur_frame = 0;
+ stats.cur_state = AVATAR_STANCE;
+
+ // remove temporary effects
+ stats.clearEffects();
+
+ // set teleportation variables. GameEngine acts on these.
+ map->teleportation = true;
+ map->teleport_mapname = map->respawn_map;
+ map->teleport_destination.x = map->respawn_point.x;
+ map->teleport_destination.y = map->respawn_point.y;
+ }
+
+ break;
+
+ default:
+ break;
+ }
+
+ // calc new cam position from player position
+ // cam is focused at player position
+ map->cam.x = stats.pos.x;
+ map->cam.y = stats.pos.y;
+ map->hero_tile.x = stats.pos.x / 32;
+ map->hero_tile.y = stats.pos.y / 32;
+
+ // check for map events
+ map->checkEvents(stats.pos);
+}
+
+/**
+ * Called by HazardManager
+ * Return false on a miss
+ */
+bool Avatar::takeHit(Hazard h) {
+
+ if (stats.cur_state != AVATAR_DEAD) {
+
+ // auto-miss if recently attacked
+ // this is mainly to prevent slow, wide missiles from getting multiple attack attempts
+ if (stats.targeted > 0) return false;
+ stats.targeted = 5;
+
+ // check miss
+ int avoidance = stats.avoidance;
+ if (stats.blocking) avoidance *= 2;
+ if (rand() % 100 > (h.accuracy - avoidance + 25)) return false;
+
+ int dmg;
+ if (h.dmg_min == h.dmg_max) dmg = h.dmg_min;
+ else dmg = h.dmg_min + (rand() % (h.dmg_max - h.dmg_min + 1));
+
+ // apply elemental resistance
+ // TODO: make this generic
+ if (h.trait_elemental == ELEMENT_FIRE) {
+ dmg = (dmg * stats.attunement_fire) / 100;
+ }
+ if (h.trait_elemental == ELEMENT_WATER) {
+ dmg = (dmg * stats.attunement_ice) / 100;
+ }
+
+ // apply absorption
+ int absorption;
+ if (!h.trait_armor_penetration) { // armor penetration ignores all absorption
+ if (stats.absorb_min == stats.absorb_max) absorption = stats.absorb_min;
+ else absorption = stats.absorb_min + (rand() % (stats.absorb_max - stats.absorb_min + 1));
+
+ if (stats.blocking) absorption += absorption + stats.absorb_max; // blocking doubles your absorb amount
+
+ dmg = dmg - absorption;
+ if (dmg < 1 && !stats.blocking) dmg = 1; // when blocking, dmg can be reduced to 0
+ if (dmg <= 0) {
+ dmg = 0;
+ Mix_PlayChannel(-1, sound_block, 0);
+ stats.cur_frame = 0; // shield stutter
+ }
+
+ }
+
+
+ int prev_hp = stats.hp;
+ stats.takeDamage(dmg);
+
+ // after effects
+ if (stats.hp > 0 && stats.immunity_duration == 0 && dmg > 0) {
+ if (h.stun_duration > stats.stun_duration) stats.stun_duration = h.stun_duration;
+ if (h.slow_duration > stats.slow_duration) stats.slow_duration = h.slow_duration;
+ if (h.bleed_duration > stats.bleed_duration) stats.bleed_duration = h.bleed_duration;
+ if (h.immobilize_duration > stats.immobilize_duration) stats.immobilize_duration = h.immobilize_duration;
+ }
+
+ // post effect power
+ Point pt;
+ pt.x = pt.y = 0;
+ if (h.post_power >= 0 && dmg > 0) {
+ powers->activate(h.post_power, &stats, pt);
+ }
+
+ // Power-specific: Vengeance gains stacks when blocking
+ if (stats.blocking && stats.physdef >= 9) {
+ if (stats.vengeance_stacks < 3)
+ stats.vengeance_stacks++;
+ }
+
+
+ if (stats.hp <= 0) {
+ stats.cur_frame = 0;
+ stats.disp_frame = 18;
+ stats.cur_state = AVATAR_DEAD;
+
+ // raise the death penalty flag. Another module will read this and reset.
+ stats.death_penalty = true;
+ }
+ else if (prev_hp > stats.hp) { // only interrupt if damage was taken
+ Mix_PlayChannel(-1, sound_hit, 0);
+ stats.cur_frame = 0;
+ stats.disp_frame = 18;
+ stats.cur_state = AVATAR_HIT;
+ }
+
+ return true;
+ }
+ return false;
+}
+
+/**
+ * getRender()
+ * Map objects need to be drawn in Z order, so we allow a parent object (GameEngine)
+ * to collect all mobile sprites each frame.
+ */
+Renderable Avatar::getRender() {
+ Renderable r;
+ r.map_pos.x = stats.pos.x;
+ r.map_pos.y = stats.pos.y;
+ r.sprite = sprites;
+ r.src.x = 128 * stats.disp_frame;
+ r.src.y = 128 * stats.direction;
+ r.src.w = 128;
+ r.src.h = 128;
+ r.offset.x = 64;
+ r.offset.y = 96; // 112
+ r.object_layer = true;
+ return r;
+}
+
+Avatar::~Avatar() {
+ SDL_FreeSurface(sprites);
+ Mix_FreeChunk(sound_melee);
+ Mix_FreeChunk(sound_hit);
+ Mix_FreeChunk(sound_die);
+ Mix_FreeChunk(sound_block);
+ Mix_FreeChunk(sound_steps[0]);
+ Mix_FreeChunk(sound_steps[1]);
+ Mix_FreeChunk(sound_steps[2]);
+ Mix_FreeChunk(sound_steps[3]);
+ Mix_FreeChunk(level_up);
+
+ delete haz;
+}
View
86 src/Avatar.h
@@ -0,0 +1,86 @@
+/**
+ * class Avatar
+ *
+ * Contains logic and rendering routines for the player avatar.
+ *
+ * @author Clint Bellanger
+ * @license GPL
+ */
+#ifndef AVATAR_H
+#define AVATAR_H
+
+
+#include <sstream>
+#include "SDL.h"
+#include "SDL_image.h"
+#include "SDL_mixer.h"
+
+#include "Utils.h"
+#include "InputState.h"
+#include "MapIso.h"
+#include "StatBlock.h"
+#include "Hazard.h"
+#include "PowerManager.h"
+
+// AVATAR State enum
+const int AVATAR_STANCE = 0;
+const int AVATAR_RUN = 1;
+const int AVATAR_MELEE = 2;
+const int AVATAR_BLOCK = 3;
+const int AVATAR_HIT = 4;
+const int AVATAR_DEAD = 5;
+const int AVATAR_CAST = 6;
+const int AVATAR_SHOOT = 7;
+
+class Avatar {
+private:
+
+ PowerManager *powers;
+ InputState *inp;
+ MapIso *map;
+
+ SDL_Surface *sprites;
+
+ bool lockSwing;
+ bool lockCast;
+ bool lockShoot;
+ bool animFwd;
+
+ Mix_Chunk *sound_melee;
+ Mix_Chunk *sound_hit;
+ Mix_Chunk *sound_die;
+ Mix_Chunk *sound_block;
+ Mix_Chunk *sound_steps[4];
+ Mix_Chunk *level_up;
+
+ string img_main;
+ string img_armor;
+ string img_off;
+
+public:
+ Avatar(PowerManager *_powers, InputState *_inp, MapIso *_map);
+ ~Avatar();
+
+ void init();
+ void loadGraphics(string img_main, string img_armor, string img_off);
+ void loadSounds();
+
+ void logic(int actionbar_power, bool restrictPowerUse);
+ bool pressing_move();
+ bool move();
+ void set_direction();
+ int face(int mapx, int mapy);
+ Renderable getRender();
+ bool takeHit(Hazard h);
+ string log_msg;
+
+ // vars
+ StatBlock stats;
+ Hazard *haz;
+ int current_power;
+ Point act_target;
+ bool drag_walking;
+
+};
+
+#endif
View
163 src/CampaignManager.cpp
@@ -0,0 +1,163 @@
+/**
+ * class CampaignManager
+ *
+ * Contains data for story mode
+ *
+ * @author Clint Bellanger
+ * @license GPL
+ */
+
+#include "CampaignManager.h"
+
+CampaignManager::CampaignManager() {
+
+ drop_stack.item = 0;
+ drop_stack.quantity = 0;
+
+ items = NULL;
+ carried_items = NULL;
+ currency = NULL;
+ xp = NULL;
+
+ log_msg = "";
+ quest_update = true;
+
+ clearAll();
+
+}
+
+void CampaignManager::clearAll() {
+ // clear campaign data
+ for (int i=0; i<MAX_STATUS; i++) {
+ status[i] = "";
+ }
+ status_count = 0;
+}
+
+/**
+ * Take the savefile camaign= and convert to status array
+ */
+void CampaignManager::setAll(std::string s) {
+ string str = s + ',';
+ string token;
+ while (str != "" && status_count < MAX_STATUS) {
+ token = eatFirstString(str, ',');
+ if (token != "") status[status_count++] = token;
+ }
+ quest_update = true;
+}
+
+/**
+ * Convert status array to savefile campaign= (status csv)
+ */
+std::string CampaignManager::getAll() {
+ stringstream ss;
+ ss.str("");
+ for (int i=0; i<status_count; i++) {
+ ss << status[i];
+ if (i < status_count-1) ss << ',';
+ }
+ return ss.str();
+}
+
+bool CampaignManager::checkStatus(std::string s) {
+
+ // avoid searching empty statuses
+ if (s == "") return false;
+
+ for (int i=0; i<status_count; i++) {
+ if (status[i] == s) return true;
+ }
+ return false;
+}
+
+void CampaignManager::setStatus(std::string s) {
+
+ // avoid adding empty statuses
+ if (s == "") return;
+
+ // hit upper limit for status
+ // TODO: add a warning
+ if (status_count >= MAX_STATUS) return;
+
+ // if it's already set, don't add it again
+ if (checkStatus(s)) return;
+
+ status[status_count++] = s;
+ quest_update = true;
+}
+
+void CampaignManager::unsetStatus(std::string s) {
+
+ // avoid searching empty statuses
+ if (s == "") return;
+
+ for (int i=status_count-1; i>=0; i--) {
+ if (status[i] == s) {
+
+ // bubble existing statuses down
+ for (int j=i; j<status_count-1; j++) {
+ status[j] = status[j+1];
+ }
+ status_count--;
+ quest_update = true;
+ return;
+ }
+ }
+}
+
+bool CampaignManager::checkItem(int item_id) {
+ return carried_items->contain(item_id);
+}
+
+void CampaignManager::removeItem(int item_id) {
+ carried_items->remove(item_id);
+}
+
+void CampaignManager::rewardItem(ItemStack istack) {
+
+ if (carried_items->full()) {
+ drop_stack.item = istack.item;
+ drop_stack.quantity = istack.quantity;
+ }
+ else {
+ carried_items->add(istack);
+
+ stringstream ss;
+ ss.str("");
+ ss << "You receive " << items->items[istack.item].name;
+ if (istack.quantity > 1) ss << " x" << istack.quantity;
+ ss << ".";
+ addMsg(ss.str());
+
+ items->playSound(istack.item);
+ }
+}
+
+void CampaignManager::rewardCurrency(int amount) {
+ *currency += amount;
+
+ stringstream ss;
+ ss.str("");
+ ss << "You receive " << amount << " gold.";
+ addMsg(ss.str());
+
+ items->playCoinsSound();
+}
+
+void CampaignManager::rewardXP(int amount) {
+ *xp += amount;
+
+ stringstream ss;
+ ss.str("");
+ ss << "You receive " << amount << " XP.";
+ addMsg(ss.str());
+}
+
+void CampaignManager::addMsg(string msg) {
+ if (log_msg != "") log_msg += " ";
+ log_msg += msg;
+}
+
+CampaignManager::~CampaignManager() {
+}
View
56 src/CampaignManager.h
@@ -0,0 +1,56 @@
+/**
+ * class CampaignManager
+ *
+ * Contains data for story mode
+ *
+ * @author Clint Bellanger
+ * @license GPL
+ */
+
+#ifndef CAMPAIGN_MANAGER_H
+#define CAMPAIGN_MANAGER_H
+
+#include <string>
+#include <sstream>
+#include "UtilsParsing.h"
+#include "MenuItemStorage.h"
+#include "ItemDatabase.h"
+
+const int MAX_STATUS = 1024;
+
+class CampaignManager {
+private:
+
+public:
+ CampaignManager();
+ ~CampaignManager();
+
+ void clearAll();
+ void setAll(std::string s);
+ std::string getAll();
+ bool checkStatus(std::string s);
+ void setStatus(std::string s);
+ void unsetStatus(std::string s);
+ bool checkItem(int item_id);
+ void removeItem(int item_id);
+ void rewardItem(ItemStack istack);
+ void rewardCurrency(int amount);
+ void rewardXP(int amount);
+ void addMsg(string msg);
+
+ string status[MAX_STATUS];
+ int status_count;
+ string log_msg;
+ ItemStack drop_stack;
+
+ // pointers to various info that can be changed
+ ItemDatabase *items;
+ MenuItemStorage *carried_items;
+ int *currency;
+ int *xp;
+
+ bool quest_update;
+};
+
+
+#endif
View
741 src/Enemy.cpp
@@ -0,0 +1,741 @@
+/*
+ * class Enemy
+ *
+ * @author Clint Bellanger
+ * @license GPL
+ *
+ */
+
+#include "Enemy.h"
+
+Enemy::Enemy(PowerManager *_powers, MapIso *_map) {
+ powers = _powers;
+ map = _map;
+
+ stats.cur_state = ENEMY_STANCE;
+ stats.cur_frame = 0;
+ stats.disp_frame = 0;
+ stats.dir_ticks = FRAMES_PER_SEC;
+ stats.patrol_ticks = 0;
+ stats.cooldown = 0;
+ stats.last_seen.x = -1;
+ stats.last_seen.y = -1;
+ stats.in_combat = false;
+
+ haz = NULL;
+
+ sfx_phys = false;
+ sfx_ment = false;
+ sfx_hit = false;
+ sfx_die = false;
+ sfx_critdie = false;
+ loot_drop = false;
+ reward_xp = false;
+}
+
+/**
+ * move()
+ * Apply speed to the direction faced.
+ *
+ * @return Returns false if wall collision, otherwise true.
+ */
+bool Enemy::move() {
+
+ if (stats.immobilize_duration > 0) return false;
+
+ int speed_diagonal = stats.dspeed;
+ int speed_straight = stats.speed;
+
+ if (stats.slow_duration > 0) {
+ speed_diagonal /= 2;
+ speed_straight /= 2;
+ }
+ else if (stats.haste_duration > 0) {
+ speed_diagonal *= 2;
+ speed_straight *= 2;
+ }
+
+ switch (stats.direction) {
+ case 0:
+ return map->collider.move(stats.pos.x, stats.pos.y, -1, 1, speed_diagonal);
+ case 1:
+ return map->collider.move(stats.pos.x, stats.pos.y, -1, 0, speed_straight);
+ case 2:
+ return map->collider.move(stats.pos.x, stats.pos.y, -1, -1, speed_diagonal);
+ case 3:
+ return map->collider.move(stats.pos.x, stats.pos.y, 0, -1, speed_straight);
+ case 4:
+ return map->collider.move(stats.pos.x, stats.pos.y, 1, -1, speed_diagonal);
+ case 5:
+ return map->collider.move(stats.pos.x, stats.pos.y, 1, 0, speed_straight);
+ case 6:
+ return map->collider.move(stats.pos.x, stats.pos.y, 1, 1, speed_diagonal);
+ case 7:
+ return map->collider.move(stats.pos.x, stats.pos.y, 0, 1, speed_straight);
+ }
+ return true;
+}
+
+/**
+ * Change direction to face the target map location
+ */
+int Enemy::face(int mapx, int mapy) {
+
+ // inverting Y to convert map coordinates to standard cartesian coordinates
+ int dx = mapx - stats.pos.x;
+ int dy = stats.pos.y - mapy;
+
+ // avoid div by zero
+ if (dx == 0) {
+ if (dy > 0) return 3;
+ else return 7;
+ }
+
+ float slope = ((float)dy)/((float)dx);
+ if (0.5 <= slope && slope <= 2.0) {
+ if (dy > 0) return 4;
+ else return 0;
+ }
+ if (-0.5 <= slope && slope <= 0.5) {
+ if (dx > 0) return 5;
+ else return 1;
+ }
+ if (-2.0 <= slope && slope <= -0.5) {
+ if (dx > 0) return 6;
+ else return 2;
+ }
+ if (2 <= slope || -2 >= slope) {
+ if (dy > 0) return 3;
+ else return 7;
+ }
+ return stats.direction;
+}
+
+/**
+ * The current direction leads to a wall. Try the next best direction, if one is available.
+ */
+int Enemy::faceNextBest(int mapx, int mapy) {
+ int dx = abs(mapx - stats.pos.x);
+ int dy = abs(mapy - stats.pos.y);
+ switch (stats.direction) {
+ case 0:
+ if (dy > dx) return 7;
+ else return 1;
+ case 1:
+ if (mapy > stats.pos.y) return 0;
+ else return 2;
+ case 2:
+ if (dx > dy) return 1;
+ else return 3;
+ case 3:
+ if (mapx < stats.pos.x) return 2;
+ else return 4;
+ case 4:
+ if (dy > dx) return 3;
+ else return 5;
+ case 5:
+ if (mapy < stats.pos.y) return 4;
+ else return 6;
+ case 6:
+ if (dx > dy) return 5;
+ else return 7;
+ case 7:
+ if (mapx > stats.pos.x) return 6;
+ else return 0;
+ }
+ return 0;
+}
+
+/**
+ * Calculate distance between the enemy and the hero
+ */
+int Enemy::getDistance(Point dest) {
+ int dx = dest.x - stats.pos.x;
+ int dy = dest.y - stats.pos.y;
+ double step1 = (double)dx * (double)dx + (double)dy * (double)dy;
+ double step2 = sqrt(step1);
+ return int(step2);
+}
+
+void Enemy::newState(int state) {
+
+ stats.cur_state = state;
+ stats.cur_frame = 0;
+}
+
+/**
+ * logic()
+ * Handle a single frame. This includes:
+ * - move the enemy based on AI % chances
+ * - calculate the next frame of animation
+ */
+void Enemy::logic() {
+
+ stats.logic();
+ if (stats.stun_duration > 0) return;
+ // check for bleeding to death
+ if (stats.hp <= 0 && !(stats.cur_state == ENEMY_DEAD || stats.cur_state == ENEMY_CRITDEAD)) {
+ doRewards();
+ stats.cur_state = ENEMY_DEAD;
+ stats.cur_frame = 0;
+ }
+ // check for bleeding spurt
+ if (stats.bleed_duration % 30 == 1) {
+ powers->activate(POWER_SPARK_BLOOD, &stats, stats.pos);
+ }
+ // check for teleport powers
+ if (stats.teleportation) {
+ stats.pos.x = stats.teleport_destination.x;
+ stats.pos.y = stats.teleport_destination.y;
+ stats.teleportation = false;
+ }
+
+ int dist;
+ int prev_direction;
+ bool los = false;
+ Point pursue_pos;
+ int max_frame;
+ int mid_frame;
+
+
+ // SECTION 1: Steering and Vision
+ // ------------------------------
+
+ // check distance and line of sight between enemy and hero
+ if (stats.hero_alive)
+ dist = getDistance(stats.hero_pos);
+ else
+ dist = 0;
+
+ // if the hero is too far away or dead, abandon combat and do nothing
+ if (dist > stats.threat_range+stats.threat_range || !stats.hero_alive) {
+ stats.in_combat = false;
+ stats.patrol_ticks = 0;
+ stats.last_seen.x = -1;
+ stats.last_seen.y = -1;
+ }
+
+ if (dist < stats.threat_range && stats.hero_alive)
+ los = map->collider.line_of_sight(stats.pos.x, stats.pos.y, stats.hero_pos.x, stats.hero_pos.y);
+ else
+ los = false;
+
+ // if the enemy can see the hero, it pursues.
+ // otherwise, it will head towards where it last saw the hero
+ if (los && dist < stats.threat_range) {
+ stats.in_combat = true;
+ stats.last_seen.x = stats.hero_pos.x;
+ stats.last_seen.y = stats.hero_pos.y;
+ }
+ else if (stats.last_seen.x >= 0 && stats.last_seen.y >= 0) {
+ if (getDistance(stats.last_seen) <= (stats.speed+stats.speed) && stats.patrol_ticks == 0) {
+ stats.last_seen.x = -1;
+ stats.last_seen.y = -1;
+ stats.patrol_ticks = 8; // start patrol; see note on "patrolling" below
+ }
+ }
+
+
+
+ // where is the creature heading?
+ // TODO: add fleeing for X ticks
+ if (los) {
+ pursue_pos.x = stats.last_seen.x = stats.hero_pos.x;
+ pursue_pos.y = stats.last_seen.y = stats.hero_pos.y;
+ stats.patrol_ticks = 0;
+ }
+ else if (stats.in_combat) {
+
+ // "patrolling" is a simple way to help steering.
+ // When the enemy arrives at where he last saw the hero, it continues
+ // walking a few steps. This gives a better chance of re-establishing
+ // line of sight around corners.
+
+ if (stats.patrol_ticks > 0) {
+ stats.patrol_ticks--;
+ if (stats.patrol_ticks == 0) {
+ stats.in_combat = false;
+ }
+ }
+ pursue_pos.x = stats.last_seen.x;
+ pursue_pos.y = stats.last_seen.y;
+ }
+
+
+
+ // SECTION 2: States
+ // -----------------
+
+ switch(stats.cur_state) {
+
+ case ENEMY_STANCE:
+
+ // handle animation
+ stats.cur_frame++;
+
+ // stance is a back/forth animation
+ mid_frame = stats.anim_stance_frames * stats.anim_stance_duration;
+ max_frame = mid_frame + mid_frame;
+ if (stats.cur_frame >= max_frame) stats.cur_frame = 0;
+ if (stats.cur_frame >= mid_frame)
+ stats.disp_frame = (max_frame -1 - stats.cur_frame) / stats.anim_stance_duration + stats.anim_stance_position;
+ else
+ stats.disp_frame = stats.cur_frame / stats.anim_stance_duration + stats.anim_stance_position;
+
+ if (stats.in_combat) {
+
+ // update direction to face the target
+ if (++stats.dir_ticks > stats.dir_favor && stats.patrol_ticks == 0) {
+ stats.direction = face(pursue_pos.x, pursue_pos.y);
+ stats.dir_ticks = 0;
+ }
+
+ // performed ranged actions
+ if (dist > stats.melee_range && stats.cooldown_ticks == 0) {
+
+ // CHECK: ranged physical!
+ //if (!powers->powers[stats.power_index[RANGED_PHYS]].requires_los || los) {
+ if (los) {
+ if ((rand() % 100) < stats.power_chance[RANGED_PHYS] && stats.power_ticks[RANGED_PHYS] == 0) {
+
+ newState(ENEMY_RANGED_PHYS);
+ break;
+ }
+ }
+ // CHECK: ranged spell!
+ //if (!powers->powers[stats.power_index[RANGED_MENT]].requires_los || los) {
+ if (los) {
+ if ((rand() % 100) < stats.power_index[RANGED_MENT] && stats.power_ticks[RANGED_MENT] == 0) {
+
+ newState(ENEMY_RANGED_MENT);
+ break;
+ }
+ }
+
+ // CHECK: flee!
+
+ // CHECK: pursue!
+ if ((rand() % 100) < stats.chance_pursue) {
+ if (move()) { // no collision
+ newState(ENEMY_MOVE);
+ }
+ else {
+ // hit an obstacle, try the next best angle
+ prev_direction = stats.direction;
+ stats.direction = faceNextBest(pursue_pos.x, pursue_pos.y);
+ if (move()) {
+ newState(ENEMY_MOVE);
+ break;
+ }
+ else stats.direction = prev_direction;
+ }
+ }
+
+ }
+ // perform melee actions
+ else if (dist <= stats.melee_range && stats.cooldown_ticks == 0) {
+
+ // CHECK: melee attack!
+ //if (!powers->powers[stats.power_index[MELEE_PHYS]].requires_los || los) {
+ if (los) {
+ if ((rand() % 100) < stats.power_chance[MELEE_PHYS] && stats.power_ticks[MELEE_PHYS] == 0) {
+
+ newState(ENEMY_MELEE_PHYS);
+ break;
+ }
+ }
+ // CHECK: melee ment!
+ //if (!powers->powers[stats.power_index[MELEE_MENT]].requires_los || los) {
+ if (los) {
+ if ((rand() % 100) < stats.power_chance[MELEE_MENT] && stats.power_ticks[MELEE_MENT] == 0) {
+
+ newState(ENEMY_MELEE_MENT);
+ break;
+ }
+ }
+ }
+ }
+
+ break;
+
+ case ENEMY_MOVE:
+
+ // handle animation
+ stats.cur_frame++;
+
+ // run is a looped animation
+ max_frame = stats.anim_run_frames * stats.anim_run_duration;
+ if (stats.cur_frame >= max_frame) stats.cur_frame = 0;
+ stats.disp_frame = (stats.cur_frame / stats.anim_run_duration) + stats.anim_run_position;
+
+ if (stats.in_combat) {
+
+ if (++stats.dir_ticks > stats.dir_favor && stats.patrol_ticks == 0) {
+ stats.direction = face(pursue_pos.x, pursue_pos.y);
+ stats.dir_ticks = 0;
+ }
+
+ if (dist > stats.melee_range && stats.cooldown_ticks == 0) {
+
+ // check ranged physical!
+ //if (!powers->powers[stats.power_index[RANGED_PHYS]].requires_los || los) {
+ if (los) {
+ if ((rand() % 100) < stats.power_chance[RANGED_PHYS] && stats.power_ticks[RANGED_PHYS] == 0) {
+
+ newState(ENEMY_RANGED_PHYS);
+ break;
+ }
+ }
+ // check ranged spell!
+ // if (!powers->powers[stats.power_index[RANGED_MENT]].requires_los || los) {
+ if (los) {
+ if ((rand() % 100) < stats.power_chance[RANGED_MENT] && stats.power_ticks[RANGED_MENT] == 0) {
+
+ newState(ENEMY_RANGED_MENT);
+ break;
+ }
+ }
+
+ if (!move()) {
+ // hit an obstacle. Try the next best angle
+ prev_direction = stats.direction;
+ stats.direction = faceNextBest(pursue_pos.x, pursue_pos.y);
+ if (!move()) {
+ newState(ENEMY_STANCE);
+ stats.direction = prev_direction;
+ }
+ }
+ }
+ else {
+ newState(ENEMY_STANCE);
+ }
+ }
+ else {
+ newState(ENEMY_STANCE);
+ }
+ break;
+
+ case ENEMY_MELEE_PHYS:
+
+ // handle animation
+ stats.cur_frame++;
+
+ // melee is a play-once animation
+ max_frame = stats.anim_melee_frames * stats.anim_melee_duration;
+ stats.disp_frame = (stats.cur_frame / stats.anim_melee_duration) + stats.anim_melee_position;
+
+ if (stats.cur_frame == 1) {
+ sfx_phys = true;
+ }
+
+ // the attack hazard is alive for a single frame
+ if (stats.cur_frame == max_frame/2 && haz == NULL) {
+ powers->activate(stats.power_index[MELEE_PHYS], &stats, pursue_pos);
+ stats.power_ticks[MELEE_PHYS] = stats.power_cooldown[MELEE_PHYS];
+ }
+
+ if (stats.cur_frame == max_frame-1) {
+ newState(ENEMY_STANCE);
+ stats.cooldown_ticks = stats.cooldown;
+ }
+ break;
+
+ case ENEMY_RANGED_PHYS:
+
+ // monsters turn to keep aim at the hero
+ stats.direction = face(pursue_pos.x, pursue_pos.y);
+
+ // handle animation
+ stats.cur_frame++;
+ max_frame = stats.anim_ranged_frames * stats.anim_ranged_duration;
+ stats.disp_frame = (stats.cur_frame / stats.anim_ranged_duration) + stats.anim_ranged_position;
+
+ if (stats.cur_frame == 1) {
+ sfx_phys = true;
+ }
+
+ // the attack hazard is alive for a single frame
+ if (stats.cur_frame == max_frame/2 && haz == NULL) {
+ powers->activate(stats.power_index[RANGED_PHYS], &stats, pursue_pos);
+ stats.power_ticks[RANGED_PHYS] = stats.power_cooldown[RANGED_PHYS];
+ }
+
+ if (stats.cur_frame == max_frame-1) {
+ newState(ENEMY_STANCE);
+ stats.cooldown_ticks = stats.cooldown;
+ }
+ break;
+
+
+ case ENEMY_MELEE_MENT:
+
+ // handle animation
+ stats.cur_frame++;
+ max_frame = stats.anim_ment_frames * stats.anim_ment_duration;
+ stats.disp_frame = (stats.cur_frame / stats.anim_ment_duration) + stats.anim_ment_position;
+
+ if (stats.cur_frame == 1) {
+ sfx_ment = true;
+ }
+
+ // the attack hazard is alive for a single frame
+ if (stats.cur_frame == max_frame/2 && haz == NULL) {
+ powers->activate(stats.power_index[MELEE_MENT], &stats, pursue_pos);
+ stats.power_ticks[MELEE_MENT] = stats.power_cooldown[MELEE_MENT];
+ }
+
+ if (stats.cur_frame == max_frame-1) {
+ newState(ENEMY_STANCE);
+ stats.cooldown_ticks = stats.cooldown;
+ }
+ break;
+
+ case ENEMY_RANGED_MENT:
+
+ // monsters turn to keep aim at the hero
+ stats.direction = face(pursue_pos.x, pursue_pos.y);
+
+ // handle animation
+ stats.cur_frame++;
+ max_frame = stats.anim_ment_frames * stats.anim_ment_duration;
+ stats.disp_frame = (stats.cur_frame / stats.anim_ment_duration) + stats.anim_ment_position;
+
+ if (stats.cur_frame == 1) {
+ sfx_ment = true;
+ }
+
+ // the attack hazard is alive for a single frame
+ if (stats.cur_frame == max_frame/2 && haz == NULL) {
+ powers->activate(stats.power_index[RANGED_MENT], &stats, pursue_pos);
+ stats.power_ticks[RANGED_MENT] = stats.power_cooldown[RANGED_MENT];
+ }
+
+ if (stats.cur_frame == max_frame-1) {
+ newState(ENEMY_STANCE);
+ stats.cooldown_ticks = stats.cooldown;
+ }
+ break;
+
+ case ENEMY_HIT:
+ // enemy has taken damage (but isn't dead)
+
+ stats.cur_frame++;
+
+ // hit is a back/forth animation
+ mid_frame = stats.anim_hit_frames * stats.anim_hit_duration;
+ max_frame = mid_frame + mid_frame;
+ if (stats.cur_frame >= mid_frame)
+ stats.disp_frame = (max_frame -1 - stats.cur_frame) / stats.anim_hit_duration + stats.anim_hit_position;
+ else
+ stats.disp_frame = stats.cur_frame / stats.anim_hit_duration + stats.anim_hit_position;
+
+
+
+ if (stats.cur_frame == max_frame-1) {
+ newState(ENEMY_STANCE);
+ }
+
+ break;
+
+ case ENEMY_DEAD:
+
+ // corpse means the creature is dead and done animating
+ if (!stats.corpse) {
+ max_frame = (stats.anim_die_frames-1) * stats.anim_die_duration;
+ if (stats.cur_frame < max_frame) stats.cur_frame++;
+ if (stats.cur_frame == max_frame) stats.corpse = true;
+ stats.disp_frame = (stats.cur_frame / stats.anim_die_duration) + stats.anim_die_position;
+ if (stats.cur_frame == 1) sfx_die = true;
+ }
+
+ break;
+
+ case ENEMY_CRITDEAD:
+ // critdead is an optional, more gruesome death animation
+
+ // corpse means the creature is dead and done animating
+ if (!stats.corpse) {
+ max_frame = (stats.anim_critdie_frames-1) * stats.anim_critdie_duration;
+ if (stats.cur_frame < max_frame) stats.cur_frame++;
+ if (stats.cur_frame == max_frame) stats.corpse = true;
+ stats.disp_frame = (stats.cur_frame / stats.anim_critdie_duration) + stats.anim_critdie_position;
+ if (stats.cur_frame == 1) sfx_critdie = true;
+ }
+
+ break;
+ }
+
+}
+
+/**
+ * Whenever a hazard collides with an enemy, this function resolves the effect
+ * Called by HazardManager
+ *
+ * Returns false on miss
+ */
+bool Enemy::takeHit(Hazard h) {
+ if (stats.cur_state != ENEMY_DEAD && stats.cur_state != ENEMY_CRITDEAD) {
+
+ if (!stats.in_combat) {
+ stats.in_combat = true;
+ stats.last_seen.x = stats.hero_pos.x;
+ stats.last_seen.y = stats.hero_pos.y;
+ }
+
+ // auto-miss if recently attacked
+ // this is mainly to prevent slow, wide missiles from getting multiple attack attempts
+ if (stats.targeted > 0) return false;
+ stats.targeted = 5;
+
+ // if it's a miss, do nothing
+ if (rand() % 100 > (h.accuracy - stats.avoidance + 25)) return false;
+
+ // calculate base damage
+ int dmg;
+ if (h.dmg_max > h.dmg_min) dmg = rand() % (h.dmg_max - h.dmg_min + 1) + h.dmg_min;
+ else dmg = h.dmg_min;
+
+ // apply elemental resistance
+ // TODO: make this generic
+ if (h.trait_elemental == ELEMENT_FIRE) {
+ dmg = (dmg * stats.attunement_fire) / 100;
+ }
+ if (h.trait_elemental == ELEMENT_WATER) {
+ dmg = (dmg * stats.attunement_ice) / 100;
+ }
+
+ // substract absorption from armor
+ int absorption;
+ if (!h.trait_armor_penetration) { // armor penetration ignores all absorption
+ if (stats.absorb_min == stats.absorb_max) absorption = stats.absorb_min;
+ else absorption = stats.absorb_min + (rand() % (stats.absorb_max - stats.absorb_min + 1));
+ dmg = dmg - absorption;
+ if (dmg < 1 && h.dmg_min >= 1) dmg = 1; // TODO: when blocking, dmg can be reduced to 0
+ if (dmg < 0) dmg = 0;
+ }
+
+ // check for crits
+ int true_crit_chance = h.crit_chance;
+ if (stats.stun_duration > 0 || stats.immobilize_duration > 0 || stats.slow_duration > 0)
+ true_crit_chance += h.trait_crits_impaired;
+
+ bool crit = (rand() % 100) < true_crit_chance;
+ if (crit) {
+ dmg = dmg + h.dmg_max;
+ map->shaky_cam_ticks = FRAMES_PER_SEC/2;
+ }
+
+ // apply damage
+ stats.takeDamage(dmg);
+
+ // damage always breaks stun
+ if (dmg > 0) stats.stun_duration=0;
+
+ // after effects
+ if (stats.hp > 0) {
+ if (h.stun_duration > stats.stun_duration) stats.stun_duration = h.stun_duration;
+ if (h.slow_duration > stats.slow_duration) stats.slow_duration = h.slow_duration;
+ if (h.bleed_duration > stats.bleed_duration) stats.bleed_duration = h.bleed_duration;
+ if (h.immobilize_duration > stats.immobilize_duration) stats.immobilize_duration = h.immobilize_duration;
+ }
+
+ // post effect power
+ Point pt;
+ pt.x = pt.y = 0;
+ if (h.post_power >= 0 && dmg > 0) {
+ powers->activate(h.post_power, &stats, pt);
+ }
+
+ // interrupted to new state
+ if (dmg > 0) {
+ sfx_hit = true;
+ stats.cur_frame = 0;
+
+ if (stats.hp <= 0 && crit) {
+ doRewards();
+ stats.disp_frame = stats.anim_critdie_position;
+ stats.cur_state = ENEMY_CRITDEAD;
+ }
+ else if (stats.hp <= 0) {
+ doRewards();
+ stats.disp_frame = stats.anim_die_position;
+ stats.cur_state = ENEMY_DEAD;
+ }
+ // don't go through a hit animation if stunned
+ else if (h.stun_duration == 0) {
+ stats.disp_frame = stats.anim_hit_position;
+ stats.cur_state = ENEMY_HIT;
+ }
+ }
+
+ return true;
+ }
+ return false;
+}
+
+/**
+ * Upon enemy death, handle rewards (gold, xp, loot)
+ */
+void Enemy::doRewards() {
+
+ int roll = rand() % 100;
+ if (roll < stats.loot_chance) {
+ loot_drop = true;
+ }
+ reward_xp = true;
+
+ // some creatures create special loot if we're on a quest
+ if (stats.quest_loot_requires != "") {
+
+ // the loot manager will check quest_loot_id
+ // if set (not zero), the loot manager will 100% generate that loot.
+ if (map->camp->checkStatus(stats.quest_loot_requires) && !map->camp->checkStatus(stats.quest_loot_not)) {
+ loot_drop = true;
+ }
+ else {
+ stats.quest_loot_id = 0;
+ }
+ }
+
+ // some creatures drop special loot the first time they are defeated
+ // this must be done in conjunction with defeat status
+ if (stats.first_defeat_loot > 0) {
+ if (!map->camp->checkStatus(stats.defeat_status)) {
+ loot_drop = true;
+ stats.quest_loot_id = stats.first_defeat_loot;
+ }
+ }
+
+ // defeating some creatures (e.g. bosses) affects the story
+ if (stats.defeat_status != "") {
+ map->camp->setStatus(stats.defeat_status);
+ }
+
+}
+
+/**
+ * getRender()
+ * Map objects need to be drawn in Z order, so we allow a parent object (GameEngine)
+ * to collect all mobile sprites each frame.
+ */
+Renderable Enemy::getRender() {
+ Renderable r;
+ r.map_pos.x = stats.pos.x;
+ r.map_pos.y = stats.pos.y;
+ r.src.x = stats.render_size.x * stats.disp_frame;
+ r.src.y = stats.render_size.y * stats.direction;
+ r.src.w = stats.render_size.x;
+ r.src.h = stats.render_size.y;
+ r.offset.x = stats.render_offset.x;
+ r.offset.y = stats.render_offset.y;
+
+ // draw corpses below objects so that floor loot is more visible
+ r.object_layer = !stats.corpse;
+
+ return r;
+}
+
+Enemy::~Enemy() {
+ delete haz;
+}
+
View
80 src/Enemy.h
@@ -0,0 +1,80 @@
+/*
+ * class Enemy
+ *
+ * @author Clint Bellanger
+ * @license GPL
+ *
+ */
+
+#ifndef ENEMY_H
+#define ENEMY_H
+
+
+#include <math.h>
+#include <string>
+#include "SDL.h"
+#include "SDL_image.h"
+#include "SDL_mixer.h"
+
+#include "Utils.h"
+#include "InputState.h"
+#include "MapIso.h"
+#include "StatBlock.h"
+#include "Hazard.h"
+#include "Settings.h"
+#include "PowerManager.h"
+
+// active states
+const int ENEMY_STANCE = 0;
+const int ENEMY_MOVE = 1;
+const int ENEMY_CHARGE = 2;
+const int ENEMY_MELEE_PHYS = 3;
+const int ENEMY_MELEE_MENT = 4;
+const int ENEMY_RANGED_PHYS = 5;
+const int ENEMY_RANGED_MENT = 6;
+
+// interrupt states
+const int ENEMY_BLOCK = 7;
+const int ENEMY_HIT = 8;
+const int ENEMY_DEAD = 9;
+const int ENEMY_CRITDEAD = 10;
+
+class Enemy {
+private:
+ MapIso *map;
+ PowerManager *powers;
+
+public:
+ Enemy(PowerManager *_powers, MapIso *_map);
+ ~Enemy();
+ bool lineOfSight();
+ bool move();
+ void logic();
+ int face(int mapx, int mapy);
+ int faceNextBest(int mapx, int mapy);
+ void newState(int state);
+ int getDistance(Point dest);
+ bool takeHit(Hazard h);
+ void doRewards();
+
+ Renderable getRender();
+
+ Hazard *haz;
+ StatBlock stats;
+
+
+ // sound effects flags
+ bool sfx_phys;
+ bool sfx_ment;
+
+ bool sfx_hit;
+ bool sfx_die;
+ bool sfx_critdie;
+
+ // other flags
+ bool loot_drop;
+ bool reward_xp;
+};
+
+
+#endif
View
226 src/EnemyManager.cpp
@@ -0,0 +1,226 @@
+/*
+ * class EnemyManager
+ *
+ * @author Clint Bellanger
+ * @license GPL
+ *
+ */
+
+#include "EnemyManager.h"
+
+EnemyManager::EnemyManager(PowerManager *_powers, MapIso *_map) {
+ powers = _powers;
+ map = _map;
+ enemy_count = 0;
+ sfx_count = 0;
+ gfx_count = 0;
+ hero_pos.x = hero_pos.y = -1;
+ hero_alive = true;
+ handleNewMap();
+}
+
+
+
+/**
+ * Enemies share graphic/sound resources (usually there are groups of similar enemies)
+ */
+void EnemyManager::loadGraphics(string type_id) {
+
+ // TODO: throw an error if a map tries to use too many monsters
+ if (gfx_count == max_gfx) return;
+
+ // first check to make sure the sprite isn't already loaded
+ for (int i=0; i<gfx_count; i++) {
+ if (gfx_prefixes[i] == type_id) {
+ return; // already have this one
+ }
+ }
+
+ sprites[gfx_count] = IMG_Load(("images/enemies/" + type_id + ".png").c_str());
+ if(!sprites[gfx_count]) {
+ fprintf(stderr, "Couldn't load image: %s\n", IMG_GetError());
+ SDL_Quit();
+ }
+ SDL_SetColorKey( sprites[gfx_count], SDL_SRCCOLORKEY, SDL_MapRGB(sprites[gfx_count]->format, 255, 0, 255) );
+
+ // optimize
+ SDL_Surface *cleanup = sprites[gfx_count];
+ sprites[gfx_count] = SDL_DisplayFormatAlpha(sprites[gfx_count]);
+ SDL_FreeSurface(cleanup);
+
+ gfx_prefixes[gfx_count] = type_id;
+ gfx_count++;
+
+}
+
+void EnemyManager::loadSounds(string type_id) {
+
+ // TODO: throw an error if a map tries to use too many monsters
+ if (sfx_count == max_sfx) return;
+
+ // first check to make sure the sprite isn't already loaded
+ for (int i=0; i<sfx_count; i++) {
+ if (sfx_prefixes[i] == type_id) {
+ return; // already have this one
+ }
+ }
+
+ sound_phys[sfx_count] = Mix_LoadWAV(("soundfx/enemies/" + type_id + "_phys.ogg").c_str());
+ sound_ment[sfx_count] = Mix_LoadWAV(("soundfx/enemies/" + type_id + "_ment.ogg").c_str());
+ sound_hit[sfx_count] = Mix_LoadWAV(("soundfx/enemies/" + type_id + "_hit.ogg").c_str());
+ sound_die[sfx_count] = Mix_LoadWAV(("soundfx/enemies/" + type_id + "_die.ogg").c_str());
+ sound_critdie[sfx_count] = Mix_LoadWAV(("soundfx/enemies/" + type_id + "_critdie.ogg").c_str());
+
+ sfx_prefixes[sfx_count] = type_id;
+ sfx_count++;
+}
+
+/**
+ * When loading a new map, we eliminate existing enemies and load the new ones.
+ * The map will have loaded Entity blocks into an array; retrieve the Enemies and init them
+ */
+void EnemyManager::handleNewMap () {
+
+ Map_Enemy me;
+
+ // delete existing enemies
+ for (int i=0; i<enemy_count; i++) {
+ delete(enemies[i]);
+ }
+ enemy_count = 0;
+
+ // free shared resources
+ for (int j=0; j<gfx_count; j++) {
+ SDL_FreeSurface(sprites[j]);
+ }
+ for (int j=0; j<sfx_count; j++) {
+ Mix_FreeChunk(sound_phys[j]);
+ Mix_FreeChunk(sound_ment[j]);
+ Mix_FreeChunk(sound_hit[j]);
+ Mix_FreeChunk(sound_die[j]);
+ Mix_FreeChunk(sound_critdie[j]);
+ }
+ gfx_count = 0;
+ sfx_count = 0;
+
+ // load new enemies
+ while (!map->enemies.empty()) {
+ me = map->enemies.front();
+ map->enemies.pop();
+
+ enemies[enemy_count] = new Enemy(powers, map);
+ enemies[enemy_count]->stats.pos.x = me.pos.x;
+ enemies[enemy_count]->stats.pos.y = me.pos.y;
+ enemies[enemy_count]->stats.direction = me.direction;
+ enemies[enemy_count]->stats.load("enemies/" + me.type + ".txt");
+ loadGraphics(enemies[enemy_count]->stats.gfx_prefix);
+ loadSounds(enemies[enemy_count]->stats.sfx_prefix);
+ enemy_count++;
+ }
+}
+
+/**
+ * perform logic() for all enemies
+ */
+void EnemyManager::logic() {
+ int pref_id;
+
+ for (int i=0; i<enemy_count; i++) {
+
+ // hazards are processed after Avatar and Enemy[]
+ // so process and clear sound effects from previous frames
+ // check sound effects
+ for (int j=0; j<sfx_count; j++) {
+ if (sfx_prefixes[j] == enemies[i]->stats.sfx_prefix)
+ pref_id = j;
+ }
+
+ if (enemies[i]->sfx_phys) Mix_PlayChannel(-1, sound_phys[pref_id], 0);
+ if (enemies[i]->sfx_ment) Mix_PlayChannel(-1, sound_ment[pref_id], 0);
+ if (enemies[i]->sfx_hit) Mix_PlayChannel(-1, sound_hit[pref_id], 0);
+ if (enemies[i]->sfx_die) Mix_PlayChannel(-1, sound_die[pref_id], 0);
+ if (enemies[i]->sfx_critdie) Mix_PlayChannel(-1, sound_critdie[pref_id], 0);
+
+ // clear sound flags
+ enemies[i]->sfx_hit = false;
+ enemies[i]->sfx_phys = false;
+ enemies[i]->sfx_ment = false;
+ enemies[i]->sfx_die = false;
+ enemies[i]->sfx_critdie = false;
+
+ // new actions this round
+ enemies[i]->stats.hero_pos = hero_pos;
+ enemies[i]->stats.hero_alive = hero_alive;
+ enemies[i]->logic();
+
+ }
+}
+
+Enemy* EnemyManager::enemyFocus(Point mouse, Point cam, bool alive_only) {
+ Point p;
+ SDL_Rect r;
+ for(int i = 0; i < enemy_count; i++) {
+ if(alive_only && (enemies[i]->stats.cur_state == ENEMY_DEAD || enemies[i]->stats.cur_state == ENEMY_CRITDEAD)) {
+ continue;
+ }
+ p = map_to_screen(enemies[i]->stats.pos.x, enemies[i]->stats.pos.y, cam.x, cam.y);
+
+ r.w = enemies[i]->stats.render_size.x;
+ r.h = enemies[i]->stats.render_size.y;
+ r.x = p.x - enemies[i]->stats.render_offset.x;
+ r.y = p.y - enemies[i]->stats.render_offset.y;
+
+ if (isWithin(r, mouse)) {
+ Enemy *enemy = enemies[i];
+ return enemy;
+ }
+ }
+ return NULL;
+}
+
+/**
+ * If an enemy has died, reward the hero with experience points
+ */
+void EnemyManager::checkEnemiesforXP(StatBlock *stats) {
+ for (int i=0; i<enemy_count; i++) {
+ if (enemies[i]->reward_xp) {
+ stats->xp += enemies[i]->stats.level;
+ enemies[i]->reward_xp = false; // clear flag
+ }
+ }
+}
+
+/**
+ * getRender()
+ * Map objects need to be drawn in Z order, so we allow a parent object (GameEngine)
+ * to collect all mobile sprites each frame.
+ *
+ * This wrapper function is necessary because EnemyManager holds shared sprites for identical-looking enemies
+ */
+Renderable EnemyManager::getRender(int enemyIndex) {
+ Renderable r = enemies[enemyIndex]->getRender();
+ for (int i=0; i<gfx_count; i++) {
+ if (gfx_prefixes[i] == enemies[enemyIndex]->stats.gfx_prefix)
+ r.sprite = sprites[i];
+ }
+ return r;
+}
+
+EnemyManager::~EnemyManager() {
+ for (int i=0; i<enemy_count; i++) {
+ delete enemies[i];
+ }
+
+ for (int i=0; i<gfx_count; i++) {
+ SDL_FreeSurface(sprites[i]);
+ }
+ for (int i=0; i<sfx_count; i++) {
+ Mix_FreeChunk(sound_phys[i]);
+ Mix_FreeChunk(sound_ment[i]);
+ Mix_FreeChunk(sound_hit[i]);
+ Mix_FreeChunk(sound_die[i]);
+ Mix_FreeChunk(sound_critdie[i]);
+ }
+
+}
+
View
58 src/EnemyManager.h
@@ -0,0 +1,58 @@
+/*
+ * class EnemyManager
+ *
+ * @author Clint Bellanger
+ * @license GPL
+ *
+ */
+
+#ifndef ENEMY_MANAGER_H
+#define ENEMY_MANAGER_H
+
+#include "MapIso.h"
+#include "Enemy.h"
+#include "Utils.h"
+#include "PowerManager.h"
+
+// TODO: rename these to something more specific to EnemyManager
+const int max_sfx = 8;
+const int max_gfx = 32;
+
+class EnemyManager {
+private:
+
+ MapIso *map;
+ PowerManager *powers;
+ void loadGraphics(string type_id);
+ void loadSounds(string type_id);
+
+ string gfx_prefixes[max_gfx];
+ int gfx_count;
+ string sfx_prefixes[max_sfx];
+ int sfx_count;
+
+ SDL_Surface *sprites[max_gfx];
+ Mix_Chunk *sound_phys[max_sfx];
+ Mix_Chunk *sound_ment[max_sfx];
+ Mix_Chunk *sound_hit[max_sfx];
+ Mix_Chunk *sound_die[max_sfx];
+ Mix_Chunk *sound_critdie[max_sfx];
+
+public:
+ EnemyManager(PowerManager *_powers, MapIso *_map);
+ ~EnemyManager();
+ void handleNewMap();
+ void logic();
+ Renderable getRender(int enemyIndex);
+ void checkEnemiesforXP(StatBlock *stats);
+ Enemy *enemyFocus(Point mouse, Point cam, bool alive_only);
+
+ // vars
+ Enemy *enemies[256]; // TODO: change to dynamic list without limits
+ Point hero_pos;
+ bool hero_alive;
+ int enemy_count;
+};
+
+
+#endif
View
77 src/FileParser.cpp
@@ -0,0 +1,77 @@
+#include "FileParser.h"
+
+FileParser::FileParser() {
+ line = "";
+ section = "";
+ key = "";
+ val = "";
+}
+
+bool FileParser::open(string filename) {
+
+ infile.open(filename.c_str(), ios::in);
+ return infile.is_open();
+}
+
+void FileParser::close() {
+ if (infile.is_open())
+ infile.close();
+}
+
+/**
+ * Advance to the next key pair
+ * Take note if a new section header is encountered
+ *
+ * @return false if EOF, otherwise true
+ */
+bool FileParser::next() {
+
+ string starts_with = "";
+ new_section = false;
+
+ while (!infile.eof()) {
+
+ line = getLine(infile);
+
+ // skip ahead if this line is empty
+ if (line.length() == 0) continue;
+
+ starts_with = line.at(0);
+
+ // skip ahead if this line is a comment
+ if (starts_with == "#") continue;
+
+ // set new section if this line is a section declaration
+ if (starts_with == "[") {
+ new_section = true;
+ section = parse_section_title(line);
+
+ // keep searching for a key-pair
+ continue;
+ }
+
+ // this is a keypair. Perform basic parsing and return
+ parse_key_pair(line, key, val);
+ return true;
+
+ }
+
+ // hit the end of file
+ return false;
+}
+
+/**
+ * Get an unparsed, unfiltered line from the input file
+ */
+string FileParser::getRawLine() {
+ line = "";
+
+ if (!infile.eof()) {
+ line = getLine(infile);
+ }
+ return line;
+}
+
+FileParser::~FileParser() {
+ close();
+}
View
35 src/FileParser.h
@@ -0,0 +1,35 @@
+/**
+ * FileParser
+ *
+ * Abstract the generic key-value pair ini-style file format
+ */
+
+#ifndef FILE_PARSER_H
+#define FILE_PARSER_H
+
+#include <fstream>
+#include <string>
+#include "UtilsParsing.h"
+
+class FileParser {
+private:
+ ifstream infile;
+ string line;
+
+public: