Add ammo pickups (#278)

Guns play clicking sound when out of ammo
Cash replaces score if ammo enabled
This commit is contained in:
Cong
2014-11-24 23:33:03 +11:00
parent e686f9931c
commit d5d80ba1bd
61 changed files with 991 additions and 270 deletions

77
data/ammo.json Normal file
View File

@@ -0,0 +1,77 @@
{
"Version": 1,
"Ammo": [
{
"Name": "Bullets",
"Pic": "bullet_pickup",
"Amount": 20,
"Max": 200
},
{
"Name": "Grenades",
"Pic": "grenade_pickup",
"Amount": 1,
"Max": 5
},
{
"Name": "Gas tank",
"Pic": "gas_tank",
"Amount": 30,
"Max": 300
},
{
"Name": "Shells",
"Pic": "shells",
"Amount": 5,
"Max": 50
},
{
"Name": "Cells",
"Pic": "cells",
"Amount": 8,
"Max": 100
},
{
"Name": "Frag grenades",
"Pic": "frag_grenade_pickup",
"Amount": 1,
"Max": 5
},
{
"Name": "Molotovs",
"Pic": "molotov",
"Amount": 1,
"Max": 5
},
{
"Name": "Mines",
"Pic": "mine_pickup",
"Amount": 1,
"Max": 5
},
{
"Name": "Dynamite",
"Pic": "dynamite_pickup",
"Amount": 2,
"Max": 10
},
{
"Name": "Gas bombs",
"Pic": "gas_grenade_pickup",
"Amount": 4,
"Max": 20
},
{
"Name": "Mini cells",
"Pic": "mini_cells",
"Amount": 20,
"Max": 500
},
{
"Name": "Rockets",
"Pic": "rockets",
"Amount": 4,
"Max": 32
}
]
}

View File

@@ -40,7 +40,8 @@
"Sound": "mg",
"Recoil": 0.17,
"MuzzleFlashParticle": "muzzle_flash_mg",
"Brass": "brass_big"
"Brass": "brass_big",
"Ammo": "Bullets"
},
{
"Index": 2,
@@ -51,7 +52,8 @@
"Cost": 20,
"Lock": 30,
"Sound": "launch",
"Elevation": 24
"Elevation": 24,
"Ammo": "Grenades"
},
{
"Index": 3,
@@ -62,7 +64,8 @@
"Lock": 6,
"Sound": "flamer",
"SoundLockLength": 36,
"MuzzleFlashParticle": "muzzle_flash_flamer"
"MuzzleFlashParticle": "muzzle_flash_flamer",
"Ammo": "Gas tank"
},
{
"Index": 4,
@@ -77,7 +80,8 @@
"SpreadCount": 5,
"SpreadWidth": 0.2,
"MuzzleFlashParticle": "muzzle_flash_shotgun",
"Brass": "shotshell"
"Brass": "shotshell",
"Ammo": "Shells"
},
{
"Index": 5,
@@ -87,7 +91,8 @@
"Cost": 2,
"Lock": 20,
"Sound": "fusion",
"MuzzleFlashParticle": "muzzle_flash_powergun"
"MuzzleFlashParticle": "muzzle_flash_powergun",
"Ammo": "Cells"
},
{
"Index": 6,
@@ -98,7 +103,8 @@
"Cost": 20,
"Lock": 30,
"Sound": "launch",
"Elevation": 24
"Elevation": 24,
"Ammo": "Frag grenades"
},
{
"Index": 7,
@@ -109,7 +115,8 @@
"Cost": 20,
"Lock": 30,
"Sound": "launch",
"Elevation": 24
"Elevation": 24,
"Ammo": "Molotovs"
},
{
"Index": 8,
@@ -123,7 +130,8 @@
"ReloadSound": "powergun_r",
"MuzzleFlashSprite": "muzzle_flash_big",
"MuzzleFlashColor": "00ffff",
"MuzzleFlashDuration": 10
"MuzzleFlashDuration": 10,
"Ammo": "Cells"
},
{
"Index": 9,
@@ -136,7 +144,8 @@
"ReloadLead": 15,
"Sound": "hahaha",
"ReloadSound": "package_r",
"MuzzleHeight": 0
"MuzzleHeight": 0,
"Ammo": "Mines"
},
{
"Index": 10,
@@ -149,7 +158,8 @@
"ReloadLead": 15,
"Sound": "hahaha",
"ReloadSound": "package_r",
"MuzzleHeight": 0
"MuzzleHeight": 0,
"Ammo": "Dynamite"
},
{
"Index": 11,
@@ -160,7 +170,8 @@
"Cost": 7,
"Lock": 30,
"Sound": "launch",
"Elevation": 24
"Elevation": 24,
"Ammo": "Gas bombs"
},
{
"Index": 12,
@@ -172,7 +183,8 @@
"ReloadLead": 15,
"Sound": "powergun",
"ReloadSound": "powergun_r",
"MuzzleFlashParticle": "muzzle_flash_petrifier"
"MuzzleFlashParticle": "muzzle_flash_petrifier",
"Ammo": "Cells"
},
{
"Index": 13,
@@ -182,7 +194,8 @@
"Cost": 5,
"Lock": 30,
"Sound": "fusion",
"MuzzleFlashParticle": "muzzle_flash_browny"
"MuzzleFlashParticle": "muzzle_flash_browny",
"Ammo": "Cells"
},
{
"Index": 14,
@@ -193,7 +206,8 @@
"Cost": 10,
"Lock": 30,
"Sound": "launch",
"Elevation": 24
"Elevation": 24,
"Ammo": "Gas bombs"
},
{
"Index": 15,
@@ -204,7 +218,8 @@
"Lock": 6,
"Sound": "flamer",
"SoundLockLength": 36,
"MuzzleFlashParticle": "muzzle_flash_gasgun"
"MuzzleFlashParticle": "muzzle_flash_gasgun",
"Ammo": "Gas tank"
},
{
"Index": 16,
@@ -215,7 +230,8 @@
"Lock": 4,
"Sound": "pulse",
"Recoil": 0.37,
"MuzzleFlashParticle": "muzzle_flash_pulse"
"MuzzleFlashParticle": "muzzle_flash_pulse",
"Ammo": "Mini cells"
},
{
"Index": 17,
@@ -225,7 +241,8 @@
"Cost": 7,
"Lock": 30,
"Sound": "swell",
"MuzzleFlashParticle": "muzzle_flash_heatseeker"
"MuzzleFlashParticle": "muzzle_flash_heatseeker",
"Ammo": "Rockets"
},
{
"Name": "Swarmer",
@@ -237,7 +254,8 @@
"SpreadCount": 4,
"SpreadWidth": 0.4,
"Sound": "swarmer",
"MuzzleFlashParticle": "muzzle_flash_heatseeker"
"MuzzleFlashParticle": "muzzle_flash_heatseeker",
"Ammo": "Rockets"
},
{
"Name": "Launcher",
@@ -246,7 +264,8 @@
"Cost": 15,
"Lock": 70,
"Sound": "rocket_launch",
"MuzzleFlashParticle": "muzzle_flash_shotgun"
"MuzzleFlashParticle": "muzzle_flash_shotgun",
"Ammo": "Rockets"
},
{
"Name": "Pistol",

BIN
graphics/bullet_pickup.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 B

View File

@@ -0,0 +1,4 @@
Adapted from 141 Military Icons Set by AngryMeteor.com
http://opengameart.org/content/141-military-icons-set
http://creativecommons.org/licenses/by/3.0/

BIN
graphics/cells.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 441 B

4
graphics/cells.txt Normal file
View File

@@ -0,0 +1,4 @@
Adapted from 141 Military Icons Set by AngryMeteor.com
http://opengameart.org/content/141-military-icons-set
http://creativecommons.org/licenses/by/3.0/

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 B

View File

@@ -0,0 +1,4 @@
Adapted from 141 Military Icons Set by AngryMeteor.com
http://opengameart.org/content/141-military-icons-set
http://creativecommons.org/licenses/by/3.0/

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 B

View File

@@ -0,0 +1,4 @@
Adapted from 141 Military Icons Set by AngryMeteor.com
http://opengameart.org/content/141-military-icons-set
http://creativecommons.org/licenses/by/3.0/

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 B

View File

@@ -0,0 +1,4 @@
Adapted from 141 Military Icons Set by AngryMeteor.com
http://opengameart.org/content/141-military-icons-set
http://creativecommons.org/licenses/by/3.0/

BIN
graphics/gas_tank.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 B

4
graphics/gas_tank.txt Normal file
View File

@@ -0,0 +1,4 @@
Adapted from 141 Military Icons Set by AngryMeteor.com
http://opengameart.org/content/141-military-icons-set
http://creativecommons.org/licenses/by/3.0/

BIN
graphics/grenade_pickup.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

View File

@@ -0,0 +1,5 @@
Adapted from Various inventory 24 pixel icon set by OceansDream
http://opengameart.org/content/various-inventory-24-pixel-icon-set
http://creativecommons.org/licenses/by/3.0/
http://creativecommons.org/licenses/by-sa/3.0/

BIN
graphics/mine_pickup.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 B

BIN
graphics/mini_cells.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

4
graphics/mini_cells.txt Normal file
View File

@@ -0,0 +1,4 @@
Adapted from 141 Military Icons Set by AngryMeteor.com
http://opengameart.org/content/141-military-icons-set
http://creativecommons.org/licenses/by/3.0/

BIN
graphics/molotov.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 B

4
graphics/molotov.txt Normal file
View File

@@ -0,0 +1,4 @@
Adapted from Molotov Cocktail by OpenClips
http://pixabay.com/en/molotov-cocktail-bottle-explosive-157193/
http://creativecommons.org/publicdomain/zero/1.0/deed.en

View File

@@ -0,0 +1,4 @@
Adapted from 141 Military Icons Set by AngryMeteor.com
http://opengameart.org/content/141-military-icons-set
http://creativecommons.org/licenses/by/3.0/

BIN
graphics/rockets.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 B

BIN
graphics/shells.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 B

4
graphics/shells.txt Normal file
View File

@@ -0,0 +1,4 @@
Adapted from 141 Military Icons Set by AngryMeteor.com
http://opengameart.org/content/141-military-icons-set
http://creativecommons.org/licenses/by/3.0/

BIN
sounds/ammo.ogg Normal file

Binary file not shown.

BIN
sounds/click.ogg Normal file

Binary file not shown.

4
sounds/click.txt Normal file
View File

@@ -0,0 +1,4 @@
Derived from Stapler_Hands_05.wav by Simon_Lacelle
http://freesound.org/people/Simon_Lacelle/sounds/67352/
http://creativecommons.org/licenses/by/3.0/

View File

@@ -53,6 +53,7 @@
#include <SDL.h>
#include <cdogs/ammo.h>
#include <cdogs/campaigns.h>
#include <cdogs/config.h>
#include <cdogs/draw.h>
@@ -377,6 +378,8 @@ int main(int argc, char *argv[])
GetDataFilePath(buf, "data/particles.json");
ParticleClassesInit(&gParticleClasses, buf);
GetDataFilePath(buf, "data/ammo.json");
AmmoInitialize(&gAmmo, buf);
GetDataFilePath(buf, "data/bullets.json");
GetDataFilePath(buf2, "data/guns.json");
BulletAndWeaponInitialize(
@@ -423,6 +426,7 @@ bail:
MapTerminate(&gMap);
PlayerDataTerminate(&gPlayerDatas);
ParticleClassesTerminate(&gParticleClasses);
AmmoTerminate(&gAmmo);
WeaponTerminate(&gGunDescriptions);
BulletTerminate(&gBulletClasses);
MissionOptionsTerminate(&gMission);

View File

@@ -9,6 +9,7 @@ set(CDOGS_SOURCES
ai_coop.c
ai_utils.c
algorithms.c
ammo.c
AStar.c
automap.c
blit.c
@@ -38,7 +39,6 @@ set(CDOGS_SOURCES
grafx.c
grafx_bg.c
handle_game_events.c
health_pickup.c
hiscores.c
hud.c
input.c
@@ -71,6 +71,7 @@ set(CDOGS_SOURCES
pics.c
player.c
player_template.c
powerup.c
quick_play.c
screen_shake.c
sounds.c
@@ -87,6 +88,7 @@ set(CDOGS_HEADERS
ai_coop.h
ai_utils.h
algorithms.h
ammo.h
AStar.h
automap.h
blit.h
@@ -115,7 +117,6 @@ set(CDOGS_HEADERS
grafx.h
grafx_bg.h
handle_game_events.h
health_pickup.h
hiscores.h
hud.h
input.h
@@ -148,6 +149,7 @@ set(CDOGS_HEADERS
pics.h
player.h
player_template.h
powerup.h
quick_play.h
screen_shake.h
sounds.h

View File

@@ -195,7 +195,8 @@ Vec2i PlacePlayer(
if (pumpEvents)
{
// Process the events that actually place the players
HandleGameEvents(&gGameEvents, NULL, NULL, NULL, &gEventHandlers);
HandleGameEvents(
&gGameEvents, NULL, NULL, NULL, NULL, &gEventHandlers);
}
return Vec2iNew(aa.FullPos.x, aa.FullPos.y);

View File

@@ -55,6 +55,7 @@
#include "actor_placement.h"
#include "ai_utils.h"
#include "ammo.h"
#include "character.h"
#include "collision.h"
#include "config.h"
@@ -80,6 +81,7 @@
#define SLIDE_Y (TILE_HEIGHT / 3)
#define VEL_DECAY_X (TILE_WIDTH * 2)
#define VEL_DECAY_Y (TILE_WIDTH * 2) // Note: deliberately tile width
#define SOUND_LOCK_WEAPON_CLICK 20
CArray gPlayerIds;
@@ -604,11 +606,34 @@ void InjureActor(TActor * actor, int injury)
}
}
void ActorAddAmmo(TActor *actor, AddAmmo a)
{
int *ammo = CArrayGet(&actor->ammo, a.Id);
*ammo += a.Amount;
const int ammoMax = AmmoGetById(&gAmmo, a.Id)->Max;
*ammo = CLAMP(*ammo, 0, ammoMax);
}
void Shoot(TActor *actor)
{
Weapon *gun = ActorGetGun(actor);
if (!WeaponCanFire(gun))
if (!ActorCanFire(actor))
{
if (!WeaponIsLocked(gun) && gConfig.Game.Ammo)
{
CASSERT(
*(int *)CArrayGet(&actor->ammo, gun->Gun->AmmoId) == 0,
"should be out of ammo");
// Play a clicking sound if this gun is out of ammo
if (gun->clickLock <= 0)
{
GameEvent e = GameEventNew(GAME_EVENT_SOUND_AT);
e.u.SoundAt.Pos = Vec2iFull2Real(actor->Pos);
e.u.SoundAt.Sound = gSoundDevice.clickSound;
GameEventsEnqueue(&gGameEvents, e);
gun->clickLock = SOUND_LOCK_WEAPON_CLICK;
}
}
return;
}
WeaponFire(
@@ -618,12 +643,24 @@ void Shoot(TActor *actor)
actor->flags,
actor->playerIndex,
actor->uid);
if (actor->playerIndex >= 0 && gun->Gun->Cost != 0)
if (actor->playerIndex >= 0)
{
GameEvent e = GameEventNew(GAME_EVENT_SCORE);
e.u.Score.PlayerIndex = actor->playerIndex;
e.u.Score.Score = -gun->Gun->Cost;
GameEventsEnqueue(&gGameEvents, e);
if (gConfig.Game.Ammo)
{
GameEvent e = GameEventNew(GAME_EVENT_USE_AMMO);
e.u.UseAmmo.PlayerIndex = actor->playerIndex;
e.u.UseAmmo.UseAmmo.Id = gun->Gun->AmmoId;
e.u.UseAmmo.UseAmmo.Amount = -1;
GameEventsEnqueue(&gGameEvents, e);
}
else if (gun->Gun->Cost != 0)
{
// Classic C-Dogs score consumption
GameEvent e = GameEventNew(GAME_EVENT_SCORE);
e.u.Score.PlayerIndex = actor->playerIndex;
e.u.Score.Score = -gun->Gun->Cost;
GameEventsEnqueue(&gGameEvents, e);
}
}
}
@@ -944,6 +981,14 @@ TActor *ActorAdd(NetMsgActorAdd aa)
TActor *actor = CArrayGet(&gActors, aa.Id);
memset(actor, 0, sizeof *actor);
CArrayInit(&actor->guns, sizeof(Weapon));
CArrayInit(&actor->ammo, sizeof(int));
for (int i = 0; i < AmmoGetNumClasses(&gAmmo); i++)
{
// Initialise with twice the standard ammo amount
// TODO: special game modes, keeping track of ammo, ammo persistence
int amount = AmmoGetById(&gAmmo, i)->Amount * 2;
CArrayPushBack(&actor->ammo, &amount);
}
Character *c;
if (aa.PlayerId >= 0)
{
@@ -1002,6 +1047,7 @@ void ActorDestroy(int id)
TActor *actor = CArrayGet(&gActors, id);
CASSERT(actor->isInUse, "Destroying in-use actor");
CArrayTerminate(&actor->guns);
CArrayTerminate(&actor->ammo);
MapRemoveTileItem(&gMap, &actor->tileItem);
for (int i = 0; i < (int)gPlayerDatas.size; i++)
{
@@ -1109,6 +1155,12 @@ Weapon *ActorGetGun(const TActor *a)
{
return CArrayGet(&a->guns, a->gunIndex);
}
bool ActorCanFire(const TActor *a)
{
const Weapon *w = ActorGetGun(a);
const bool hasAmmo = *(int *)CArrayGet(&a->ammo, w->Gun->AmmoId) > 0;
return !WeaponIsLocked(w) && (!gConfig.Game.Ammo || hasAmmo);
}
bool ActorTrySwitchGun(TActor *a)
{
if (a->guns.size < 2)

View File

@@ -125,6 +125,7 @@ typedef struct Actor
int playerIndex; // -1 unless a human player
int uid; // unique ID across all actors
CArray guns; // of Weapon
CArray ammo; // of int
int gunIndex;
int health;
@@ -175,6 +176,13 @@ void BuildTranslationTables(const TPalette palette);
void ActorHeal(TActor *actor, int health);
void InjureActor(TActor * actor, int injury);
typedef struct
{
int Id;
int Amount;
} AddAmmo;
void ActorAddAmmo(TActor *actor, AddAmmo a);
void ActorsInit(void);
void ActorsTerminate(void);
int ActorsGetFreeIndex(void);
@@ -183,6 +191,7 @@ void ActorDestroy(int id);
const Character *ActorGetCharacter(const TActor *a);
Weapon *ActorGetGun(const TActor *a);
bool ActorCanFire(const TActor *a);
bool ActorTrySwitchGun(TActor *a);
bool ActorIsImmune(const TActor *actor, const special_damage_e damage);
// Taking a hit only gives the appearance (pushback, special effect)

View File

@@ -246,7 +246,7 @@ static int WillFire(TActor * actor, int roll)
{
const CharBot *bot = ActorGetCharacter(actor)->bot;
if ((actor->flags & FLAGS_VISIBLE) != 0 &&
WeaponCanFire(ActorGetGun(actor)) &&
ActorCanFire(actor) &&
roll < bot->probabilityToShoot)
{
if ((actor->flags & FLAGS_GOOD_GUY) != 0)
@@ -534,7 +534,8 @@ void InitializeBadGuys(void)
GameEventsEnqueue(&gGameEvents, e);
// Process the events that actually place the actors
HandleGameEvents(&gGameEvents, NULL, NULL, NULL, &gEventHandlers);
HandleGameEvents(
&gGameEvents, NULL, NULL, NULL, NULL, &gEventHandlers);
}
}
else if (mobj->Type == OBJECTIVE_RESCUE)
@@ -563,7 +564,8 @@ void InitializeBadGuys(void)
GameEventsEnqueue(&gGameEvents, e);
// Process the events that actually place the actors
HandleGameEvents(&gGameEvents, NULL, NULL, NULL, &gEventHandlers);
HandleGameEvents(
&gGameEvents, NULL, NULL, NULL, NULL, &gEventHandlers);
}
}
}
@@ -598,6 +600,7 @@ void CreateEnemies(void)
gBaddieCount++;
// Process the events that actually place the actors
HandleGameEvents(&gGameEvents, NULL, NULL, NULL, &gEventHandlers);
HandleGameEvents(
&gGameEvents, NULL, NULL, NULL, NULL, &gEventHandlers);
}
}

166
src/cdogs/ammo.c Normal file
View File

@@ -0,0 +1,166 @@
/*
C-Dogs SDL
A port of the legendary (and fun) action/arcade cdogs.
Copyright (c) 2014, Cong Xu
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
*/
#include "ammo.h"
#include <string.h>
#include "json_utils.h"
#include "pic_manager.h"
#include "utils.h"
AmmoClasses gAmmo;
// TODO: use map structure?
Ammo *StrAmmo(const char *s)
{
return AmmoGetById(&gAmmo, StrAmmoId(s));
}
int StrAmmoId(const char *s)
{
if (s == NULL || strlen(s) == 0)
{
return 0;
}
for (int i = 0; i < (int)gAmmo.CustomAmmo.size; i++)
{
Ammo *a = CArrayGet(&gAmmo.CustomAmmo, i);
if (strcmp(s, a->Name) == 0)
{
return i + (int)gAmmo.Ammo.size;
}
}
for (int i = 0; i < (int)gAmmo.Ammo.size; i++)
{
Ammo *a = CArrayGet(&gAmmo.Ammo, i);
if (strcmp(s, a->Name) == 0)
{
return i;
}
}
CASSERT(false, "cannot parse ammo name");
return 0;
}
#define VERSION 1
static void LoadAmmo(Ammo *a, json_t *node);
void AmmoInitialize(AmmoClasses *ammo, const char *path)
{
memset(ammo, 0, sizeof *ammo);
CArrayInit(&ammo->Ammo, sizeof(Ammo));
CArrayInit(&ammo->CustomAmmo, sizeof(Ammo));
json_t *root = NULL;
enum json_error e;
FILE *f = fopen(path, "r");
if (f == NULL)
{
printf("Error: cannot load ammo file %s\n", path);
goto bail;
}
e = json_stream_parse(f, &root);
if (e != JSON_OK)
{
printf("Error parsing ammo file %s [error %d]\n", path, (int)e);
goto bail;
}
AmmoLoadJSON(&ammo->Ammo, root);
bail:
if (f)
{
fclose(f);
}
json_free_value(&root);
}
void AmmoLoadJSON(CArray *ammo, json_t *node)
{
int version;
LoadInt(&version, node, "Version");
if (version > VERSION || version <= 0)
{
CASSERT(false, "cannot read ammo file version");
return;
}
json_t *ammoNode = json_find_first_label(node, "Ammo")->child;
for (json_t *child = ammoNode->child; child; child = child->next)
{
Ammo a;
LoadAmmo(&a, child);
CArrayPushBack(ammo, &a);
}
}
static void LoadAmmo(Ammo *a, json_t *node)
{
if (json_find_first_label(node, "Name"))
{
a->Name = GetString(node, "Name");
}
char *tmp;
tmp = GetString(node, "Pic");
a->Pic = PicManagerGetPic(&gPicManager, tmp);
CFREE(tmp);
LoadInt(&a->Amount, node, "Amount");
LoadInt(&a->Max, node, "Max");
}
void AmmoClassesClear(CArray *ammo)
{
for (int i = 0; i < (int)ammo->size; i++)
{
Ammo *a = CArrayGet(ammo, i);
CFREE(a->Name);
}
CArrayClear(ammo);
}
void AmmoTerminate(AmmoClasses *ammo)
{
AmmoClassesClear(&ammo->Ammo);
CArrayTerminate(&ammo->Ammo);
AmmoClassesClear(&ammo->CustomAmmo);
CArrayTerminate(&ammo->CustomAmmo);
}
Ammo *AmmoGetById(AmmoClasses *ammo, const int id)
{
if (id < (int)ammo->Ammo.size)
{
return CArrayGet(&ammo->Ammo, id);
}
else
{
return CArrayGet(&ammo->CustomAmmo, id - (int)ammo->Ammo.size);
}
}
int AmmoGetNumClasses(const AmmoClasses *ammo)
{
return (int)ammo->Ammo.size + (int)ammo->CustomAmmo.size;
}

58
src/cdogs/ammo.h Normal file
View File

@@ -0,0 +1,58 @@
/*
C-Dogs SDL
A port of the legendary (and fun) action/arcade cdogs.
Copyright (c) 2014, Cong Xu
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <json/json.h>
#include "c_array.h"
#include "pic.h"
typedef struct
{
char *Name;
const Pic *Pic;
int Amount;
int Max;
} Ammo;
typedef struct
{
CArray Ammo; // of Ammo
CArray CustomAmmo; // of Ammo
} AmmoClasses;
extern AmmoClasses gAmmo;
Ammo *StrAmmo(const char *s);
int StrAmmoId(const char *s);
void AmmoInitialize(AmmoClasses *ammo, const char *path);
void AmmoLoadJSON(CArray *ammo, json_t *node);
void AmmoClassesClear(CArray *ammo);
void AmmoTerminate(AmmoClasses *ammo);
Ammo *AmmoGetById(AmmoClasses *ammo, const int id);
int AmmoGetNumClasses(const AmmoClasses *ammo);

View File

@@ -408,6 +408,7 @@ void ConfigLoadDefault(Config *config)
config->Game.ShotsPushback = 1;
config->Game.AllyCollision = ALLYCOLLISION_REPEL;
config->Game.HealthPickups = true;
config->Game.Ammo = true;
config->Game.Gore = GORE_LOW;
config->Game.LaserSight = LASER_SIGHT_NONE;
config->Graphics.Brightness = 0;

View File

@@ -115,6 +115,7 @@ typedef struct
bool ShotsPushback;
AllyCollision AllyCollision;
bool HealthPickups;
bool Ammo;
GoreAmount Gore;
LaserSight LaserSight;
} GameConfig;

View File

@@ -77,6 +77,7 @@ static void LoadGameConfigNode(
JSON_UTILS_LOAD_ENUM(
config->AllyCollision, node, "AllyCollision", StrAllyCollision);
LoadBool(&config->HealthPickups, node, "HealthPickups");
LoadBool(&config->Ammo, node, "Ammo");
JSON_UTILS_LOAD_ENUM(
config->Gore, node, "Gore", StrGoreAmount);
JSON_UTILS_LOAD_ENUM(
@@ -113,6 +114,8 @@ static void AddGameConfigNode(GameConfig *config, json_t *root)
subConfig, "AllyCollision", config->AllyCollision, AllyCollisionStr);
json_insert_pair_into_object(
subConfig, "HealthPickups", json_new_bool(config->HealthPickups));
json_insert_pair_into_object(
subConfig, "Ammo", json_new_bool(config->Ammo));
json_insert_pair_into_object(root, "Game", subConfig);
JSON_UTILS_ADD_ENUM_PAIR(
subConfig, "Gore", config->Gore, GoreAmountStr);

View File

@@ -47,6 +47,22 @@ FontOpts FontOptsNew(void)
return opts;
}
FontAlign FontAlignOpposite(const FontAlign align)
{
switch (align)
{
case ALIGN_START:
return ALIGN_END;
case ALIGN_CENTER:
return ALIGN_CENTER;
case ALIGN_END:
return ALIGN_END;
default:
CASSERT(false, "Unknown font alignment");
return ALIGN_START;
}
}
void FontLoad(Font *f, const char *imgPath, const char *jsonPath)
{
FILE *file = NULL;

View File

@@ -71,6 +71,8 @@ extern Font gFont;
FontOpts FontOptsNew(void);
FontAlign FontAlignOpposite(const FontAlign align);
void FontLoad(Font *f, const char *imgPath, const char *jsonPath);
void FontFromImage(Font *f, SDL_Surface *image, json_t *data);
void FontTerminate(Font *f);

View File

@@ -54,6 +54,9 @@ typedef enum
GAME_EVENT_ACTOR_MOVE,
GAME_EVENT_ADD_HEALTH_PICKUP,
GAME_EVENT_TAKE_HEALTH_PICKUP,
GAME_EVENT_ADD_AMMO_PICKUP,
GAME_EVENT_TAKE_AMMO_PICKUP,
GAME_EVENT_USE_AMMO,
GAME_EVENT_MOBILE_OBJECT_REMOVE,
GAME_EVENT_PARTICLE_REMOVE,
GAME_EVENT_ADD_BULLET,
@@ -99,11 +102,22 @@ typedef struct
NetMsgActorAdd ActorAdd;
NetMsgActorMove ActorMove;
Vec2i AddPos;
AddAmmoPickup AddAmmoPickup;
struct
{
int PlayerIndex;
int Health;
} Heal;
struct
{
int PlayerIndex;
AddAmmo AddAmmo;
} AddAmmo;
struct
{
int PlayerIndex;
AddAmmo UseAmmo;
} UseAmmo;
int MobileObjectRemoveId;
int ParticleRemoveId;
AddBullet AddBullet;

View File

@@ -39,19 +39,22 @@ static void HandleGameEvent(
GameEvent *e,
HUD *hud,
ScreenShake *shake,
HealthPickups *hp,
PowerupSpawner *healthSpawner,
CArray *ammoSpawners,
EventHandlers *eventHandlers);
void HandleGameEvents(
CArray *store,
HUD *hud,
ScreenShake *shake,
HealthPickups *hp,
PowerupSpawner *healthSpawner,
CArray *ammoSpawners,
EventHandlers *eventHandlers)
{
for (int i = 0; i < (int)store->size; i++)
{
GameEvent *e = CArrayGet(store, i);
HandleGameEvent(e, hud, shake, hp, eventHandlers);
HandleGameEvent(
e, hud, shake, healthSpawner, ammoSpawners, eventHandlers);
}
GameEventsClear(store);
}
@@ -59,7 +62,8 @@ static void HandleGameEvent(
GameEvent *e,
HUD *hud,
ScreenShake *shake,
HealthPickups *hp,
PowerupSpawner *healthSpawner,
CArray *ammoSpawners,
EventHandlers *eventHandlers)
{
e->Delay--;
@@ -137,12 +141,49 @@ static void HandleGameEvent(
break;
}
ActorHeal(a, e->u.Heal.Health);
HealthPickupsRemoveOne(hp);
PowerupSpawnerRemoveOne(healthSpawner);
HUDAddHealthUpdate(
hud, e->u.Heal.PlayerIndex, e->u.Heal.Health);
}
}
break;
case GAME_EVENT_ADD_AMMO_PICKUP:
MapPlaceAmmo(e->u.AddAmmoPickup);
break;
case GAME_EVENT_TAKE_AMMO_PICKUP:
{
const PlayerData *p =
CArrayGet(&gPlayerDatas, e->u.Heal.PlayerIndex);
if (IsPlayerAlive(p))
{
TActor *a = CArrayGet(&gActors, p->Id);
if (!a->isInUse)
{
break;
}
ActorAddAmmo(a, e->u.AddAmmo.AddAmmo);
PowerupSpawnerRemoveOne(
CArrayGet(ammoSpawners, e->u.AddAmmo.AddAmmo.Id));
// TODO: some sort of text effect showing ammo grab
}
}
break;
case GAME_EVENT_USE_AMMO:
{
const PlayerData *p =
CArrayGet(&gPlayerDatas, e->u.UseAmmo.PlayerIndex);
if (IsPlayerAlive(p))
{
TActor *a = CArrayGet(&gActors, p->Id);
if (!a->isInUse)
{
break;
}
ActorAddAmmo(a, e->u.UseAmmo.UseAmmo);
// TODO: some sort of text effect showing ammo usage
}
}
break;
case GAME_EVENT_MOBILE_OBJECT_REMOVE:
MobObjDestroy(e->u.MobileObjectRemoveId);
break;
@@ -217,7 +258,9 @@ static void HandleGameEvent(
e1.u.Score.PlayerIndex =
e->u.UpdateObjective.PlayerIndex;
e1.u.Score.Score = OBJECT_SCORE;
HandleGameEvent(&e1, hud, shake, hp, eventHandlers);
HandleGameEvent(
&e1, hud, shake, healthSpawner, ammoSpawners,
eventHandlers);
}
break;
default:

View File

@@ -30,15 +30,16 @@
#include "c_array.h"
#include "events.h"
#include "health_pickup.h"
#include "hud.h"
#include "powerup.h"
#include "screen_shake.h"
void HandleGameEvents(
CArray *store,
HUD *hud,
ScreenShake *shake,
HealthPickups *hp,
PowerupSpawner *healthSpawner,
CArray *ammoSpawners,
EventHandlers *eventHandlers);
#endif

View File

@@ -1,145 +0,0 @@
/*
Copyright (c) 2014, Cong Xu
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
*/
#include "health_pickup.h"
#include <math.h>
#include "ai_utils.h"
#include "gamedata.h"
#define SPAWN_TIME (20 * FPS_FRAMELIMIT)
#define TIME_DECAY_EXPONENT 1.04
#define HEALTH_W 6
#define HEALTH_H 6
#define MAX_TILES_PER_PICKUP 625
void HealthPickupsInit(HealthPickups *h, Map *map)
{
h->map = map;
h->timer = 0;
h->numPickups = 0;
h->pickupsSpawned = 0;
// Update once
HealthPickupsUpdate(h, 0);
}
static bool TryPlacePickup(HealthPickups *h);
void HealthPickupsUpdate(HealthPickups *h, int ticks)
{
// Don't spawn pickups if not allowed
if (!AreHealthPickupsAllowed(gCampaign.Entry.Mode) ||
!gConfig.Game.HealthPickups)
{
return;
}
double scalar = 1.0;
// Update time until next spawn based on:
// Damage taken (find player with lowest health)
int minHealth = ModeMaxHealth(gCampaign.Entry.Mode);
int maxHealth = minHealth;
for (int i = 0; i < (int)gPlayerDatas.size; i++)
{
const PlayerData *p = CArrayGet(&gPlayerDatas, i);
if (!IsPlayerAlive(p))
{
continue;
}
const TActor *player = CArrayGet(&gActors, p->Id);
minHealth = MIN(minHealth, player->health);
}
// Double spawn rate if near 0 health
scalar *= (minHealth + maxHealth) / (maxHealth * 2.0);
// Scale down over time
scalar *= pow(TIME_DECAY_EXPONENT, h->pickupsSpawned);
h->timeUntilNextSpawn = (int)floor(scalar * SPAWN_TIME);
// Update time
h->timer += ticks;
// Attempt to add health if time reached, and we haven't placed too many
if (h->timer >= h->timeUntilNextSpawn &&
h->map->NumExplorableTiles / MAX_TILES_PER_PICKUP + 1 > h->numPickups)
{
h->timer = 0;
if (TryPlacePickup(h))
{
h->pickupsSpawned++;
h->numPickups++;
}
}
}
static bool TryPlacePickup(HealthPickups *h)
{
Vec2i size = Vec2iNew(HEALTH_W, HEALTH_H);
// Attempt to place one in unexplored area
for (int i = 0; i < 100; i++)
{
const Vec2i v = MapGenerateFreePosition(h->map, size);
if (!Vec2iIsZero(v) && !MapGetTile(h->map, Vec2iToTile(v))->isVisited)
{
MapPlaceHealth(v);
return true;
}
}
// Attempt to place one in out-of-sight area
for (int i = 0; i < 100; i++)
{
const Vec2i v = MapGenerateFreePosition(h->map, size);
const Vec2i fullpos = Vec2iReal2Full(v);
const TActor *closestPlayer = AIGetClosestPlayer(fullpos);
if (!Vec2iIsZero(v) &&
(!closestPlayer || CHEBYSHEV_DISTANCE(
fullpos.x, fullpos.y,
closestPlayer->Pos.x, closestPlayer->Pos.y) >= 256 * 150))
{
MapPlaceHealth(v);
return true;
}
}
// Attempt to place one anyway
for (int i = 0; i < 100; i++)
{
const Vec2i v = MapGenerateFreePosition(h->map, size);
if (!Vec2iIsZero(v))
{
MapPlaceHealth(v);
return true;
}
}
return false;
}
void HealthPickupsRemoveOne(HealthPickups *h)
{
CASSERT(h->numPickups > 0, "unexpectedly removing health pickup");
h->numPickups--;
}

View File

@@ -53,6 +53,7 @@
#include <time.h>
#include "actors.h"
#include "ammo.h"
#include "automap.h"
#include "draw.h"
#include "drawtools.h"
@@ -174,10 +175,9 @@ void HUDDisplayMessage(HUD *hud, const char *msg, int ticks)
hud->messageTicks = ticks;
}
void HUDAddHealthUpdate(HUD *hud, int playerIndex, int health)
static int FindLocalPlayerIndex(const int playerIndex)
{
HUDNumUpdate s;
s.Index = 0;
int idx = 0;
// Find the local index of this player
for (int i = 0; i <= playerIndex; i++)
{
@@ -187,14 +187,26 @@ void HUDAddHealthUpdate(HUD *hud, int playerIndex, int health)
if (!p->IsLocal)
{
// This update was for a non-local player; abort
return;
return -1;
}
}
else if (p->IsLocal)
{
s.Index++;
idx++;
}
}
return idx;
}
void HUDAddHealthUpdate(HUD *hud, int playerIndex, int health)
{
HUDNumUpdate s;
s.Index = FindLocalPlayerIndex(playerIndex);
if (s.Index < 0)
{
// This update was for a non-local player; abort
return;
}
s.Amount = health;
s.Timer = NUM_UPDATE_TIMER_MS;
s.TimerMax = NUM_UPDATE_TIMER_MS;
@@ -204,23 +216,11 @@ void HUDAddHealthUpdate(HUD *hud, int playerIndex, int health)
void HUDAddScoreUpdate(HUD *hud, int playerIndex, int score)
{
HUDNumUpdate s;
s.Index = 0;
// Find the local index of this player
for (int i = 0; i <= playerIndex; i++)
s.Index = FindLocalPlayerIndex(playerIndex);
if (s.Index < 0)
{
const PlayerData *p = CArrayGet(&gPlayerDatas, i);
if (i == playerIndex)
{
if (!p->IsLocal)
{
// This update was for a non-local player; abort
return;
}
}
else if (p->IsLocal)
{
s.Index++;
}
// This update was for a non-local player; abort
return;
}
s.Amount = score;
s.Timer = NUM_UPDATE_TIMER_MS;
@@ -238,6 +238,7 @@ void HUDAddObjectiveUpdate(HUD *hud, int objectiveIndex, int update)
CArrayPushBack(&hud->objectiveUpdates, &u);
}
static void NumUpdate(CArray *updates, const int ms);
void HUDUpdate(HUD *hud, int ms)
{
if (hud->messageTicks >= 0)
@@ -250,33 +251,19 @@ void HUDUpdate(HUD *hud, int ms)
}
FPSCounterUpdate(&hud->fpsCounter, ms);
WallClockUpdate(&hud->clock, ms);
for (int i = 0; i < (int)hud->healthUpdates.size; i++)
NumUpdate(&hud->healthUpdates, ms);
NumUpdate(&hud->scoreUpdates, ms);
NumUpdate(&hud->objectiveUpdates, ms);
}
static void NumUpdate(CArray *updates, const int ms)
{
for (int i = 0; i < (int)updates->size; i++)
{
HUDNumUpdate *health = CArrayGet(&hud->healthUpdates, i);
health->Timer -= ms;
if (health->Timer <= 0)
{
CArrayDelete(&hud->healthUpdates, i);
i--;
}
}
for (int i = 0; i < (int)hud->scoreUpdates.size; i++)
{
HUDNumUpdate *score = CArrayGet(&hud->scoreUpdates, i);
score->Timer -= ms;
if (score->Timer <= 0)
{
CArrayDelete(&hud->scoreUpdates, i);
i--;
}
}
for (int i = 0; i < (int)hud->objectiveUpdates.size; i++)
{
HUDNumUpdate *update = CArrayGet(&hud->objectiveUpdates, i);
HUDNumUpdate *update = CArrayGet(updates, i);
update->Timer -= ms;
if (update->Timer <= 0)
{
CArrayDelete(&hud->objectiveUpdates, i);
CArrayDelete(updates, i);
i--;
}
}
@@ -314,9 +301,10 @@ static void DrawGauge(
#define GAUGE_WIDTH 50
static void DrawWeaponStatus(
GraphicsDevice *device, const Weapon *weapon, Vec2i pos,
GraphicsDevice *device, const TActor *actor, Vec2i pos,
const FontAlign hAlign, const FontAlign vAlign)
{
const Weapon *weapon = ActorGetGun(actor);
// don't draw gauge if not reloading
if (weapon->lock > 0)
{
@@ -343,7 +331,20 @@ static void DrawWeaponStatus(
opts.VAlign = vAlign;
opts.Area = gGraphicsDevice.cachedConfig.Res;
opts.Pad = pos;
FontStrOpt(weapon->Gun->name, Vec2iZero(), opts);
char buf[128];
if (gConfig.Game.Ammo)
{
// Include ammo counter
sprintf(buf, "%s %d/%d",
weapon->Gun->name,
*(int *)CArrayGet(&actor->ammo, weapon->Gun->AmmoId),
AmmoGetById(&gAmmo, weapon->Gun->AmmoId)->Max);
}
else
{
strcpy(buf, weapon->Gun->name);
}
FontStrOpt(buf, Vec2iZero(), opts);
}
static void DrawHealth(
@@ -582,7 +583,15 @@ static void DrawPlayerStatus(
char s[50];
if (IsScoreNeeded(gCampaign.Entry.Mode))
{
sprintf(s, "Score: %d", data->score);
if (gConfig.Game.Ammo)
{
// Display money instead of ammo
sprintf(s, "Cash: $%d", data->score);
}
else
{
sprintf(s, "Score: %d", data->score);
}
}
else
{
@@ -591,8 +600,7 @@ static void DrawPlayerStatus(
if (p)
{
// Weapon
DrawWeaponStatus(
device, ActorGetGun(p), pos, opts.HAlign, opts.VAlign);
DrawWeaponStatus(device, p, pos, opts.HAlign, opts.VAlign);
pos.y += rowHeight;
// Player name
@@ -947,12 +955,12 @@ void HUDDraw(HUD *hud, int isPaused)
}
static void DrawNumUpdate(
HUDNumUpdate *update,
const HUDNumUpdate *update,
const char *formatText, int currentValue, Vec2i pos, int flags);
static void DrawHealthUpdate(HUDNumUpdate *health, int flags)
{
const int rowHeight = 1 + FontH();
int y = 5 + 1 + FontH() + rowHeight * 2;
int y = 5 + rowHeight * 3;
const PlayerData *p = CArrayGet(&gPlayerDatas, health->Index);
if (IsPlayerAlive(p))
{
@@ -967,7 +975,7 @@ static void DrawScoreUpdate(HUDNumUpdate *score, int flags)
return;
}
const int rowHeight = 1 + FontH();
int y = 5 + 1 + FontH() + rowHeight;
int y = 5 + rowHeight * 2;
const PlayerData *p = CArrayGet(&gPlayerDatas, score->Index);
DrawNumUpdate(score, "Score: %d", p->score, Vec2iNew(5, y), flags);
}
@@ -980,7 +988,7 @@ static void DrawScoreUpdate(HUDNumUpdate *score, int flags)
#define NUM_UPDATE_FALL_DOWN_DURATION_MS 100
#define NUM_UPDATE_POP_UP_HEIGHT 5
static void DrawNumUpdate(
HUDNumUpdate *update,
const HUDNumUpdate *update,
const char *formatText, int currentValue, Vec2i pos, int flags)
{
CASSERT(update->Amount != 0, "num update with zero amount");

View File

@@ -52,14 +52,15 @@
#include <string.h>
#include <stdlib.h>
#include "ammo.h"
#include "collision.h"
#include "config.h"
#include "health_pickup.h"
#include "map_build.h"
#include "map_classic.h"
#include "map_static.h"
#include "pic_manager.h"
#include "pickup.h"
#include "powerup.h"
#include "objs.h"
#include "triggers.h"
#include "sounds.h"
@@ -532,8 +533,8 @@ void MapPlaceCollectible(
const Vec2i fullPos = Vec2iReal2Full(realPos);
const int id = PickupAdd(
fullPos,
NULL,
cGeneralPics[o->pickupItem].picIndex,
PicManagerGet(
&gPicManager, NULL, cGeneralPics[o->pickupItem].picIndex),
PICKUP_JEWEL);
Pickup *p = CArrayGet(&gPickups, id);
p->u.Score = PICKUP_SCORE;
@@ -569,7 +570,10 @@ static int MapTryPlaceCollectible(
void MapPlaceHealth(Vec2i pos)
{
const int id = PickupAdd(Vec2iReal2Full(pos), "health", 0, PICKUP_HEALTH);
const int id = PickupAdd(
Vec2iReal2Full(pos),
PicManagerGetPic(&gPicManager, "health"),
PICKUP_HEALTH);
Pickup *p = CArrayGet(&gPickups, id);
p->u.Health = HEALTH_PICKUP_HEAL_AMOUNT;
}
@@ -626,13 +630,22 @@ void MapPlaceKey(
const Vec2i fullPos = Vec2iReal2Full(Vec2iCenterOfTile(pos));
const int id = PickupAdd(
fullPos,
NULL,
cGeneralPics[mo->keyPics[keyIndex]].picIndex,
PicManagerGet(
&gPicManager, NULL, cGeneralPics[mo->keyPics[keyIndex]].picIndex),
PICKUP_KEYCARD);
Pickup *p = CArrayGet(&gPickups, id);
p->u.Keys = 1 << keyIndex;
}
void MapPlaceAmmo(AddAmmoPickup a)
{
const Ammo *ammo = AmmoGetById(&gAmmo, a.Id);
const int id = PickupAdd(Vec2iReal2Full(a.Pos), ammo->Pic, PICKUP_AMMO);
Pickup *p = CArrayGet(&gPickups, id);
p->u.Ammo.Id = a.Id;
p->u.Ammo.Amount = ammo->Amount;
}
static void MapPlaceCard(Map *map, int keyIndex, int map_access)
{
for (;;)

View File

@@ -156,5 +156,11 @@ void MapPlaceHealth(Vec2i pos);
void MapPlaceKey(
Map *map, const struct MissionOptions *mo, const Vec2i pos,
const int keyIndex);
typedef struct
{
Vec2i Pos;
int Id;
} AddAmmoPickup;
void MapPlaceAmmo(AddAmmoPickup a);
#endif

View File

@@ -31,6 +31,7 @@
#include <SDL_image.h>
#include "ammo.h"
#include "json_utils.h"
#include "map_new.h"
#include "physfs/physfs.h"
@@ -109,6 +110,13 @@ int MapNewLoadArchive(const char *filename, CampaignSetting *c)
&gBulletClasses, &gBulletClasses.CustomClasses, root);
}
root = ReadPhysFSJSON(filename, "ammo.json");
if (root != NULL)
{
AmmoLoadJSON(&gAmmo.CustomAmmo, root);
json_free_value(&root);
}
root = ReadPhysFSJSON(filename, "guns.json");
if (root != NULL)
{

View File

@@ -116,7 +116,8 @@ void MapStaticLoadDynamic(
GameEventsEnqueue(&gGameEvents, e);
// Process the events that actually place the players
HandleGameEvents(&gGameEvents, NULL, NULL, NULL, &gEventHandlers);
HandleGameEvents(
&gGameEvents, NULL, NULL, NULL, NULL, &gEventHandlers);
}
}
@@ -185,7 +186,8 @@ void MapStaticLoadDynamic(
obj->placed++;
// Process the events that actually place the objectives
HandleGameEvents(&gGameEvents, NULL, NULL, NULL, &gEventHandlers);
HandleGameEvents(
&gGameEvents, NULL, NULL, NULL, NULL, &gEventHandlers);
}
}

View File

@@ -27,6 +27,7 @@
*/
#include "pickup.h"
#include "ammo.h"
#include "game_events.h"
#include "map.h"
@@ -52,9 +53,7 @@ void PickupsTerminate(void)
CArrayTerminate(&gPickups);
}
static const Pic *GetPickupPic(const int id, Vec2i *offset);
int PickupAdd(
const Vec2i pos, const char *picName, const int oldIdx,
const PickupType type)
int PickupAdd(const Vec2i pos, const Pic *pic, const PickupType type)
{
// Find an empty slot in pickup list
Pickup *p = NULL;
@@ -77,7 +76,7 @@ int PickupAdd(
p = CArrayGet(&gPickups, i);
}
memset(p, 0, sizeof *p);
p->Pic = PicManagerGet(&gPicManager, picName, oldIdx);
p->Pic = pic;
p->Type = type;
p->tileItem.x = p->tileItem.y = -1;
p->tileItem.flags = 0;
@@ -130,6 +129,26 @@ void PickupPickup(const TActor *a, const Pickup *p)
}
break;
case PICKUP_AMMO:
{
// Don't pickup if ammo full
canPickup = false;
const int ammoMax = AmmoGetById(&gAmmo, p->u.Ammo.Id)->Max;
const int current = *(int *)CArrayGet(&a->ammo, p->u.Ammo.Id);
if (current < ammoMax)
{
canPickup = true;
GameEvent e = GameEventNew(GAME_EVENT_TAKE_AMMO_PICKUP);
e.u.AddAmmo.PlayerIndex = a->playerIndex;
e.u.AddAmmo.AddAmmo = p->u.Ammo;
// Note: receiving end will prevent ammo from exceeding max
GameEventsEnqueue(&gGameEvents, e);
// TODO: per-ammo sound
sound = gSoundDevice.ammoSound;
}
}
break;
case PICKUP_KEYCARD:
gMission.flags |= p->u.Keys;
sound = gSoundDevice.keySound;

View File

@@ -37,6 +37,7 @@ typedef enum
PICKUP_NONE,
PICKUP_JEWEL,
PICKUP_HEALTH,
PICKUP_AMMO,
PICKUP_KEYCARD
} PickupType;
@@ -50,6 +51,7 @@ typedef struct
{
int Score;
int Health;
AddAmmo Ammo;
int Keys; // Refer to flags in mission.h
} u;
TTileItem tileItem;
@@ -64,9 +66,7 @@ extern CArray gPickups; // of Pickup
void PickupsInit(void);
void PickupsTerminate(void);
int PickupAdd(
const Vec2i pos, const char *picName, const int oldIdx,
const PickupType type);
int PickupAdd(const Vec2i pos, const Pic *pic, const PickupType type);
void PickupDestroy(const int id);
void PickupPickup(const TActor *a, const Pickup *p);

233
src/cdogs/powerup.c Normal file
View File

@@ -0,0 +1,233 @@
/*
Copyright (c) 2014, Cong Xu
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
*/
#include "powerup.h"
#include <math.h>
#include "ai_utils.h"
#include "ammo.h"
#include "gamedata.h"
#include "game_events.h"
#define TIME_DECAY_EXPONENT 1.04
#define HEALTH_W 6
#define HEALTH_H 6
#define MAX_TILES_PER_PICKUP 625
void PowerupSpawnerInit(PowerupSpawner *p, Map *map)
{
memset(p, 0, sizeof *p);
p->map = map;
p->timer = 0;
p->numPickups = 0;
p->pickupsSpawned = 0;
p->Enabled = true;
}
void PowerupSpawnerTerminate(PowerupSpawner *p)
{
CFREE(p->Data);
}
static bool TryPlacePickup(PowerupSpawner *p);
void PowerupSpawnerUpdate(PowerupSpawner *p, const int ticks)
{
// Don't spawn pickups if not allowed
if (!p->Enabled)
{
return;
}
double scalar = p->RateScaleFunc(p->Data);
// Scale down over time
scalar *= pow(TIME_DECAY_EXPONENT, p->pickupsSpawned);
p->timeUntilNextSpawn = (int)floor(scalar * p->SpawnTime);
// Update time
p->timer += ticks;
// Attempt to add pickup if time reached, and we haven't placed too many
if (p->timer >= p->timeUntilNextSpawn &&
p->map->NumExplorableTiles / MAX_TILES_PER_PICKUP + 1 > p->numPickups)
{
p->timer -= p->timeUntilNextSpawn;
if (TryPlacePickup(p))
{
p->pickupsSpawned++;
p->numPickups++;
}
}
}
static bool TryPlacePickup(PowerupSpawner *p)
{
const Vec2i size = Vec2iNew(HEALTH_W, HEALTH_H);
// Attempt to place one in out-of-sight area
for (int i = 0; i < 100; i++)
{
const Vec2i v = MapGenerateFreePosition(p->map, size);
const Vec2i fullpos = Vec2iReal2Full(v);
const TActor *closestPlayer = AIGetClosestPlayer(fullpos);
if (!Vec2iIsZero(v) &&
(!closestPlayer || CHEBYSHEV_DISTANCE(
fullpos.x, fullpos.y,
closestPlayer->Pos.x, closestPlayer->Pos.y) >= 256 * 150))
{
p->PlaceFunc(v, p->Data);
return true;
}
}
// Attempt to place one anyway
for (int i = 0; i < 100; i++)
{
const Vec2i v = MapGenerateFreePosition(p->map, size);
if (!Vec2iIsZero(v))
{
p->PlaceFunc(v, p->Data);
return true;
}
}
return false;
}
void PowerupSpawnerRemoveOne(PowerupSpawner *p)
{
CASSERT(p->numPickups > 0, "unexpectedly removing powerup");
p->numPickups--;
}
#define HEALTH_SPAWN_TIME (20 * FPS_FRAMELIMIT)
static double HealthScale(void *data);
static void HealthPlace(const Vec2i pos, void *data);
void HealthSpawnerInit(PowerupSpawner *p, Map *map)
{
PowerupSpawnerInit(p, map);
p->Enabled =
AreHealthPickupsAllowed(gCampaign.Entry.Mode) &&
gConfig.Game.HealthPickups;
p->SpawnTime = HEALTH_SPAWN_TIME;
p->RateScaleFunc = HealthScale;
p->PlaceFunc = HealthPlace;
// Update once
PowerupSpawnerUpdate(p, 0);
}
static double HealthScale(void *data)
{
UNUSED(data);
// Update time until next spawn based on:
// Damage taken (find player with lowest health)
int minHealth = ModeMaxHealth(gCampaign.Entry.Mode);
int maxHealth = minHealth;
for (int i = 0; i < (int)gPlayerDatas.size; i++)
{
const PlayerData *p = CArrayGet(&gPlayerDatas, i);
if (!IsPlayerAlive(p))
{
continue;
}
const TActor *player = CArrayGet(&gActors, p->Id);
minHealth = MIN(minHealth, player->health);
}
// Double spawn rate if near 0 health
return (minHealth + maxHealth) / (maxHealth * 2.0);
}
static void HealthPlace(const Vec2i pos, void *data)
{
UNUSED(data);
GameEvent e = GameEventNew(GAME_EVENT_ADD_HEALTH_PICKUP);
e.u.AddPos = pos;
GameEventsEnqueue(&gGameEvents, e);
}
#define AMMO_SPAWN_TIME (20 * FPS_FRAMELIMIT)
static double AmmoScale(void *data);
static void AmmoPlace(const Vec2i pos, void *data);
void AmmoSpawnerInit(PowerupSpawner *p, Map *map, const int ammoId)
{
PowerupSpawnerInit(p, map);
// TODO: disable ammo spawners if map is deathmatch and contains ammo
p->Enabled = gConfig.Game.Ammo;
p->SpawnTime = AMMO_SPAWN_TIME;
p->RateScaleFunc = AmmoScale;
p->PlaceFunc = AmmoPlace;
CMALLOC(p->Data, sizeof ammoId);
*(int *)p->Data = ammoId;
// Update once
PowerupSpawnerUpdate(p, 0);
}
static double AmmoScale(void *data)
{
const int ammoId = *(int *)data;
// Update time until next spawn based on:
// Ammo left (find player with lowest ammo)
// But make sure at least one player has a gun that uses this ammo
int minVal = AmmoGetById(&gAmmo, ammoId)->Max;
int maxVal = minVal;
int numPlayersWithAmmo = 0;
for (int i = 0; i < (int)gPlayerDatas.size; i++)
{
const PlayerData *p = CArrayGet(&gPlayerDatas, i);
if (!IsPlayerAlive(p))
{
continue;
}
const TActor *player = CArrayGet(&gActors, p->Id);
for (int j = 0; j < (int)player->guns.size; j++)
{
const Weapon *w = CArrayGet(&player->guns, j);
if (w->Gun->AmmoId == ammoId)
{
numPlayersWithAmmo++;
}
}
minVal = MIN(minVal, *(int *)CArrayGet(&player->ammo, ammoId));
}
if (numPlayersWithAmmo > 0)
{
// Double spawn rate if near 0 ammo
return (minVal + maxVal) / (maxVal * 2.0) / numPlayersWithAmmo;
}
else
{
// No players have guns that use this ammo; spawn very slowly
return 5.0;
}
}
static void AmmoPlace(const Vec2i pos, void *data)
{
const int ammoId = *(int *)data;
GameEvent e = GameEventNew(GAME_EVENT_ADD_AMMO_PICKUP);
e.u.AddAmmoPickup.Pos = pos;
e.u.AddAmmoPickup.Id = ammoId;
GameEventsEnqueue(&gGameEvents, e);
}

View File

@@ -23,8 +23,7 @@
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef __HEALTH_PICKUP
#define __HEALTH_PICKUP
#pragma once
#include "actors.h"
@@ -37,10 +36,19 @@ typedef struct
int timer;
int numPickups;
int pickupsSpawned;
} HealthPickups;
void HealthPickupsInit(HealthPickups *h, Map *map);
void HealthPickupsUpdate(HealthPickups *h, int ticks);
void HealthPickupsRemoveOne(HealthPickups *h);
bool Enabled;
int SpawnTime;
double (*RateScaleFunc)(void *);
void (*PlaceFunc)(const Vec2i, void *);
void *Data;
} PowerupSpawner;
#endif
void PowerupSpawnerInit(PowerupSpawner *p, Map *map);
void PowerupSpawnerTerminate(PowerupSpawner *p);
void PowerupSpawnerUpdate(PowerupSpawner *p, const int ticks);
void PowerupSpawnerRemoveOne(PowerupSpawner *p);
void HealthSpawnerInit(PowerupSpawner *p, Map *map);
void AmmoSpawnerInit(PowerupSpawner *p, Map *map, const int ammoId);

View File

@@ -153,6 +153,8 @@ void SoundInitialize(
device->switchSound = StrSound("switch");
device->pickupSound = StrSound("pickup");
device->healthSound = StrSound("health");
device->ammoSound = StrSound("ammo");
device->clickSound = StrSound("click");
device->keySound = StrSound("key");
device->wreckSound = StrSound("bang");
CArrayInit(&device->screamSounds, sizeof(Mix_Chunk *));

View File

@@ -96,6 +96,8 @@ typedef struct
Mix_Chunk *switchSound;
Mix_Chunk *pickupSound;
Mix_Chunk *healthSound;
Mix_Chunk *ammoSound;
Mix_Chunk *clickSound;
Mix_Chunk *keySound;
Mix_Chunk *wreckSound;
CArray screamSounds; // of Mix_Chunk *

View File

@@ -53,6 +53,7 @@
#include <json/json.h>
#include "ammo.h"
#include "config.h"
#include "game_events.h"
#include "json_utils.h"
@@ -212,6 +213,13 @@ static void LoadGunDescription(
CFREE(tmp);
}
if (json_find_first_label(node, "Ammo"))
{
tmp = GetString(node, "Ammo");
g->AmmoId = StrAmmoId(tmp);
CFREE(tmp);
}
LoadInt(&g->Cost, node, "Cost");
LoadInt(&g->Lock, node, "Lock");
@@ -351,6 +359,11 @@ void WeaponUpdate(
{
w->soundLock = 0;
}
w->clickLock -= ticks;
if (w->clickLock < 0)
{
w->clickLock = 0;
}
if (w->stateCounter >= 0)
{
w->stateCounter = MAX(0, w->stateCounter - ticks);
@@ -372,9 +385,9 @@ void WeaponUpdate(
}
}
int WeaponCanFire(Weapon *w)
bool WeaponIsLocked(const Weapon *w)
{
return w->lock <= 0;
return w->lock > 0;
}
void WeaponFire(
@@ -390,8 +403,6 @@ void WeaponFire(
return;
}
CASSERT(WeaponCanFire(w), "Can't fire weapon");
const double radians = dir2radians[d];
const Vec2i muzzleOffset = GunGetMuzzleOffset(w->Gun, d);
const Vec2i muzzlePosition = Vec2iAdd(pos, muzzleOffset);

View File

@@ -102,6 +102,7 @@ typedef struct
char *name;
char *Description;
const BulletClass *Bullet;
int AmmoId;
int Cost; // Cost in score to fire weapon
int Lock;
int ReloadLead;
@@ -137,6 +138,7 @@ typedef struct
gunstate_e state;
int lock;
int soundLock;
int clickLock;
int stateCounter;
} Weapon;
@@ -153,7 +155,7 @@ const GunDescription *StrGunDescription(const char *s);
Vec2i GunGetMuzzleOffset(const GunDescription *desc, const direction_e dir);
void WeaponUpdate(
Weapon *w, const int ticks, const Vec2i fullPos, const direction_e d);
int WeaponCanFire(Weapon *w);
bool WeaponIsLocked(const Weapon *w);
void WeaponFire(
Weapon *w, const direction_e d, const Vec2i pos,
const int flags, const int player, const int uid);

View File

@@ -55,6 +55,7 @@
#include <SDL.h>
#include <cdogs/actors.h>
#include <cdogs/ammo.h>
#include <cdogs/automap.h>
#include <cdogs/config.h>
#include <cdogs/draw.h>
@@ -1220,6 +1221,8 @@ int main(int argc, char *argv[])
GetDataFilePath(buf, "data/particles.json");
ParticleClassesInit(&gParticleClasses, buf);
GetDataFilePath(buf, "data/ammo.json");
AmmoInitialize(&gAmmo, buf);
GetDataFilePath(buf, "data/bullets.json");
GetDataFilePath(buf2, "data/guns.json");
BulletAndWeaponInitialize(&gBulletClasses, &gGunDescriptions, buf, buf2);
@@ -1275,6 +1278,7 @@ int main(int argc, char *argv[])
MapTerminate(&gMap);
ParticleClassesTerminate(&gParticleClasses);
AmmoTerminate(&gAmmo);
WeaponTerminate(&gGunDescriptions);
BulletTerminate(&gBulletClasses);
CampaignTerminate(&gCampaign);

View File

@@ -59,13 +59,13 @@
#include <cdogs/actors.h>
#include <cdogs/ai.h>
#include <cdogs/ai_coop.h>
#include <cdogs/ammo.h>
#include <cdogs/automap.h>
#include <cdogs/config.h>
#include <cdogs/draw.h>
#include <cdogs/events.h>
#include <cdogs/game_events.h>
#include <cdogs/handle_game_events.h>
#include <cdogs/health_pickup.h>
#include <cdogs/hud.h>
#include <cdogs/joystick.h>
#include <cdogs/mission.h>
@@ -75,6 +75,7 @@
#include <cdogs/particle.h>
#include <cdogs/pic_manager.h>
#include <cdogs/pics.h>
#include <cdogs/powerup.h>
#include <cdogs/screen_shake.h>
#include <cdogs/triggers.h>
@@ -379,7 +380,8 @@ typedef struct
bool isPaused;
bool isMap;
int cmds[MAX_LOCAL_PLAYERS];
HealthPickups hp;
PowerupSpawner healthSpawner;
CArray ammoSpawners; // of PowerupSpawner
ScreenShake shake;
Vec2i lastPosition;
GameLoopData loop;
@@ -397,7 +399,14 @@ bool RunGame(struct MissionOptions *m, Map *map)
DrawBufferInit(&data.buffer, Vec2iNew(X_TILES, Y_TILES), &gGraphicsDevice);
HUDInit(&data.hud, &gConfig.Interface, &gGraphicsDevice, m);
HealthPickupsInit(&data.hp, map);
HealthSpawnerInit(&data.healthSpawner, map);
CArrayInit(&data.ammoSpawners, sizeof(PowerupSpawner));
for (int i = 0; i < AmmoGetNumClasses(&gAmmo); i++)
{
PowerupSpawner ps;
AmmoSpawnerInit(&ps, map, i);
CArrayPushBack(&data.ammoSpawners, &ps);
}
data.shake = ScreenShakeZero();
if (MusicGetStatus(&gSoundDevice) != MUSIC_OK)
@@ -428,6 +437,12 @@ bool RunGame(struct MissionOptions *m, Map *map)
data.loop.InputEverySecondFrame = true;
GameLoop(&data.loop);
PowerupSpawnerTerminate(&data.healthSpawner);
for (int i = 0; i < (int)data.ammoSpawners.size; i++)
{
PowerupSpawnerTerminate(CArrayGet(&data.ammoSpawners, i));
}
CArrayTerminate(&data.ammoSpawners);
HUDTerminate(&data.hud);
DrawBufferTerminate(&data.buffer);
@@ -602,7 +617,11 @@ static GameLoopResult RunGameUpdate(void *data)
UpdateWatches(&rData->map->triggers);
HealthPickupsUpdate(&rData->hp, ticksPerFrame);
PowerupSpawnerUpdate(&rData->healthSpawner, ticksPerFrame);
for (int i = 0; i < (int)rData->ammoSpawners.size; i++)
{
PowerupSpawnerUpdate(CArrayGet(&rData->ammoSpawners, i), ticksPerFrame);
}
if (!gCampaign.IsClient)
{
@@ -610,7 +629,9 @@ static GameLoopResult RunGameUpdate(void *data)
}
HandleGameEvents(
&gGameEvents, &rData->hud, &rData->shake, &rData->hp, &gEventHandlers);
&gGameEvents, &rData->hud, &rData->shake,
&rData->healthSpawner, &rData->ammoSpawners,
&gEventHandlers);
rData->m->time += ticksPerFrame;

View File

@@ -355,6 +355,12 @@ menu_t *MenuCreateOptionsGame(const char *name, MenuSystem *ms)
"Health pickups",
&gConfig.Game.HealthPickups,
MENU_OPTION_DISPLAY_STYLE_YES_NO));
MenuAddSubmenu(
menu,
MenuCreateOptionToggle(
"Ammo",
&gConfig.Game.Ammo,
MENU_OPTION_DISPLAY_STYLE_YES_NO));
MenuAddSubmenu(
menu,
MenuCreateOptionToggle(