mirror of
https://github.com/ddnet/ddnet.git
synced 2024-11-11 02:28:18 +00:00
1283 lines
26 KiB
C++
1283 lines
26 KiB
C++
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <engine/config.h>
|
|
#include "../game.h"
|
|
#include "data.h"
|
|
#include "game_server.h"
|
|
|
|
using namespace baselib;
|
|
|
|
// --------- DEBUG STUFF ---------
|
|
const bool debug_bots = true;
|
|
|
|
// --------- PHYSICS TWEAK! --------
|
|
const float ground_control_speed = 7.0f;
|
|
const float ground_control_accel = 2.0f;
|
|
const float ground_friction = 0.5f;
|
|
const float ground_jump_speed = 12.0f;
|
|
const float air_control_speed = 3.5f;
|
|
const float air_control_accel = 1.2f;
|
|
const float air_friction = 0.95f;
|
|
const float hook_length = 32*10.0f;
|
|
const float hook_fire_speed = 45.0f;
|
|
const float hook_drag_accel = 3.0f;
|
|
const float hook_drag_speed = 15.0f;
|
|
const float gravity = 0.5f;
|
|
|
|
class player* get_player(int index);
|
|
void create_damageind(vec2 p, vec2 dir, int amount);
|
|
void create_explosion(vec2 p, int owner, int weapon, bool bnodamage);
|
|
void create_smoke(vec2 p);
|
|
void create_sound(vec2 pos, int sound, int loopflags = 0);
|
|
class player *intersect_player(vec2 pos0, vec2 pos1, vec2 &new_pos, class entity *notthis = 0);
|
|
|
|
template<typename T>
|
|
T saturated_add(T min, T max, T current, T modifier)
|
|
{
|
|
if(modifier < 0)
|
|
{
|
|
if(current < min)
|
|
return current;
|
|
current += modifier;
|
|
if(current < min)
|
|
current = min;
|
|
return current;
|
|
}
|
|
else
|
|
{
|
|
if(current > max)
|
|
return current;
|
|
current += modifier;
|
|
if(current > max)
|
|
current = max;
|
|
return current;
|
|
}
|
|
}
|
|
|
|
// TODO: rewrite this smarter!
|
|
void move_box(vec2 *inout_pos, vec2 *inout_vel, vec2 size, float elasticity)
|
|
{
|
|
// do the move
|
|
vec2 pos = *inout_pos;
|
|
vec2 vel = *inout_vel;
|
|
|
|
float distance = length(vel);
|
|
int max = (int)distance;
|
|
|
|
vec2 offsets[4] = { vec2(-size.x/2, -size.y/2), vec2( size.x/2, -size.y/2),
|
|
vec2(-size.x/2, size.y/2), vec2( size.x/2, size.y/2)};
|
|
|
|
if(distance > 0.00001f)
|
|
{
|
|
vec2 old_pos = pos;
|
|
for(int i = 0; i <= max; i++)
|
|
{
|
|
float amount = i/(float)max;
|
|
if(max == 0)
|
|
amount = 0;
|
|
|
|
vec2 new_pos = pos + vel*amount; // TODO: this row is not nice
|
|
|
|
for(int p = 0; p < 4; p++)
|
|
{
|
|
vec2 np = new_pos+offsets[p];
|
|
vec2 op = old_pos+offsets[p];
|
|
if(col_check_point(np))
|
|
{
|
|
int affected = 0;
|
|
if(col_check_point(np.x, op.y))
|
|
{
|
|
vel.x = -vel.x*elasticity;
|
|
pos.x = old_pos.x;
|
|
new_pos.x = old_pos.x;
|
|
affected++;
|
|
}
|
|
|
|
if(col_check_point(op.x, np.y))
|
|
{
|
|
vel.y = -vel.y*elasticity;
|
|
pos.y = old_pos.y;
|
|
new_pos.y = old_pos.y;
|
|
affected++;
|
|
}
|
|
|
|
if(!affected)
|
|
{
|
|
new_pos = old_pos;
|
|
pos = old_pos;
|
|
vel *= -elasticity;
|
|
}
|
|
}
|
|
}
|
|
|
|
old_pos = new_pos;
|
|
}
|
|
|
|
pos = old_pos;
|
|
}
|
|
|
|
*inout_pos = pos;
|
|
*inout_vel = vel;
|
|
}
|
|
|
|
//////////////////////////////////////////////////
|
|
// Event handler
|
|
//////////////////////////////////////////////////
|
|
event_handler::event_handler()
|
|
{
|
|
clear();
|
|
}
|
|
|
|
void *event_handler::create(int type, int size)
|
|
{
|
|
void *p = &data[current_offset];
|
|
offsets[num_events] = current_offset;
|
|
types[num_events] = type;
|
|
sizes[num_events] = size;
|
|
current_offset += size;
|
|
num_events++;
|
|
return p;
|
|
}
|
|
|
|
void event_handler::clear()
|
|
{
|
|
num_events = 0;
|
|
current_offset = 0;
|
|
}
|
|
|
|
void event_handler::snap(int snapping_client)
|
|
{
|
|
for(int i = 0; i < num_events; i++)
|
|
{
|
|
void *d = snap_new_item(types[i], i, sizes[i]);
|
|
mem_copy(d, &data[offsets[i]], sizes[i]);
|
|
}
|
|
}
|
|
|
|
event_handler events;
|
|
|
|
//////////////////////////////////////////////////
|
|
// Entity
|
|
//////////////////////////////////////////////////
|
|
entity::entity(int objtype)
|
|
{
|
|
this->objtype = objtype;
|
|
pos = vec2(0,0);
|
|
flags = FLAG_ALIVE;
|
|
proximity_radius = 0;
|
|
|
|
current_id++;
|
|
id = current_id;
|
|
|
|
next_entity = 0;
|
|
prev_entity = 0;
|
|
prev_type_entity = 0;
|
|
next_type_entity = 0;
|
|
}
|
|
|
|
entity::~entity()
|
|
{
|
|
}
|
|
|
|
int entity::current_id = 1;
|
|
|
|
//////////////////////////////////////////////////
|
|
// game world
|
|
//////////////////////////////////////////////////
|
|
game_world::game_world()
|
|
{
|
|
paused = false;
|
|
reset_requested = false;
|
|
first_entity = 0x0;
|
|
for(int i = 0; i < NUM_ENT_TYPES; i++)
|
|
first_entity_types[i] = 0;
|
|
}
|
|
|
|
int game_world::find_entities(vec2 pos, float radius, entity **ents, int max)
|
|
{
|
|
int num = 0;
|
|
for(entity *ent = first_entity; ent; ent = ent->next_entity)
|
|
{
|
|
if(!(ent->flags&entity::FLAG_ALIVE))
|
|
continue;
|
|
|
|
if(distance(ent->pos, pos) < radius+ent->proximity_radius)
|
|
{
|
|
ents[num] = ent;
|
|
num++;
|
|
if(num == max)
|
|
break;
|
|
}
|
|
}
|
|
|
|
return num;
|
|
}
|
|
|
|
int game_world::find_entities(vec2 pos, float radius, entity **ents, int max, const int* types, int maxtypes)
|
|
{
|
|
int num = 0;
|
|
for(int t = 0; t < maxtypes; t++)
|
|
{
|
|
for(entity *ent = first_entity_types[types[t]]; ent; ent = ent->next_type_entity)
|
|
{
|
|
if(!(ent->flags&entity::FLAG_ALIVE))
|
|
continue;
|
|
|
|
if(distance(ent->pos, pos) < radius+ent->proximity_radius)
|
|
{
|
|
ents[num] = ent;
|
|
num++;
|
|
if(num == max)
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return num;
|
|
}
|
|
|
|
void game_world::insert_entity(entity *ent)
|
|
{
|
|
// insert it
|
|
if(first_entity)
|
|
first_entity->prev_entity = ent;
|
|
ent->next_entity = first_entity;
|
|
ent->prev_entity = 0x0;
|
|
first_entity = ent;
|
|
|
|
// into typelist aswell
|
|
if(first_entity_types[ent->objtype])
|
|
first_entity_types[ent->objtype]->prev_type_entity = ent;
|
|
ent->next_type_entity = first_entity_types[ent->objtype];
|
|
ent->prev_type_entity = 0x0;
|
|
first_entity_types[ent->objtype] = ent;
|
|
}
|
|
|
|
void game_world::destroy_entity(entity *ent)
|
|
{
|
|
ent->set_flag(entity::FLAG_DESTROY);
|
|
}
|
|
|
|
void game_world::remove_entity(entity *ent)
|
|
{
|
|
// remove
|
|
if(ent->prev_entity)
|
|
ent->prev_entity->next_entity = ent->next_entity;
|
|
else
|
|
first_entity = ent->next_entity;
|
|
if(ent->next_entity)
|
|
ent->next_entity->prev_entity = ent->prev_entity;
|
|
|
|
if(ent->prev_type_entity)
|
|
ent->prev_type_entity->next_type_entity = ent->next_type_entity;
|
|
else
|
|
first_entity_types[ent->objtype] = ent->next_type_entity;
|
|
if(ent->next_type_entity)
|
|
ent->next_type_entity->prev_type_entity = ent->prev_type_entity;
|
|
}
|
|
|
|
//
|
|
void game_world::snap(int snapping_client)
|
|
{
|
|
for(entity *ent = first_entity; ent; ent = ent->next_entity)
|
|
ent->snap(snapping_client);
|
|
}
|
|
|
|
void game_world::reset()
|
|
{
|
|
// reset all entities
|
|
for(entity *ent = first_entity; ent; ent = ent->next_entity)
|
|
ent->reset();
|
|
remove_entities();
|
|
|
|
for(entity *ent = first_entity; ent; ent = ent->next_entity)
|
|
ent->post_reset();
|
|
remove_entities();
|
|
|
|
reset_requested = false;
|
|
}
|
|
|
|
void game_world::remove_entities()
|
|
{
|
|
// destroy objects marked for destruction
|
|
entity *ent = first_entity;
|
|
while(ent)
|
|
{
|
|
entity *next = ent->next_entity;
|
|
if(ent->flags&entity::FLAG_DESTROY)
|
|
{
|
|
remove_entity(ent);
|
|
ent->destroy();
|
|
}
|
|
ent = next;
|
|
}
|
|
}
|
|
|
|
void game_world::tick()
|
|
{
|
|
if(reset_requested)
|
|
reset();
|
|
|
|
if(!paused)
|
|
{
|
|
// update all objects
|
|
for(entity *ent = first_entity; ent; ent = ent->next_entity)
|
|
ent->tick();
|
|
|
|
for(entity *ent = first_entity; ent; ent = ent->next_entity)
|
|
ent->tick_defered();
|
|
}
|
|
|
|
remove_entities();
|
|
}
|
|
|
|
game_world world;
|
|
|
|
//////////////////////////////////////////////////
|
|
// game object
|
|
//////////////////////////////////////////////////
|
|
gameobject::gameobject()
|
|
: entity(OBJTYPE_GAME)
|
|
{
|
|
game_over_tick = -1;
|
|
sudden_death = 0;
|
|
round_start_tick = server_tick();
|
|
}
|
|
|
|
void gameobject::endround()
|
|
{
|
|
world.paused = true;
|
|
game_over_tick = server_tick();
|
|
sudden_death = 0;
|
|
}
|
|
|
|
void gameobject::resetgame()
|
|
{
|
|
world.reset_requested = true;
|
|
}
|
|
|
|
void gameobject::startround()
|
|
{
|
|
resetgame();
|
|
|
|
round_start_tick = server_tick();
|
|
sudden_death = 0;
|
|
game_over_tick = -1;
|
|
world.paused = false;
|
|
}
|
|
|
|
void gameobject::post_reset()
|
|
{
|
|
for(int i = 0; i < MAX_CLIENTS; i++)
|
|
{
|
|
if(players[i].client_id != -1)
|
|
players[i].respawn();
|
|
}
|
|
}
|
|
|
|
void gameobject::tick()
|
|
{
|
|
if(game_over_tick == -1)
|
|
{
|
|
// game is running
|
|
|
|
// gather some stats
|
|
int topscore = 0;
|
|
int topscore_count = 0;
|
|
for(int i = 0; i < MAX_CLIENTS; i++)
|
|
{
|
|
if(players[i].client_id != -1)
|
|
{
|
|
if(players[i].score > topscore)
|
|
{
|
|
topscore = players[i].score;
|
|
topscore_count = 1;
|
|
}
|
|
else if(players[i].score == topscore)
|
|
topscore_count++;
|
|
}
|
|
}
|
|
|
|
// check score win condition
|
|
if((config.scorelimit > 0 && topscore >= config.scorelimit) ||
|
|
(config.timelimit > 0 && (server_tick()-round_start_tick) >= config.timelimit*server_tickspeed()*60))
|
|
{
|
|
if(topscore_count == 1)
|
|
endround();
|
|
else
|
|
sudden_death = 1;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// game over.. wait for restart
|
|
if(server_tick() > game_over_tick+server_tickspeed()*10)
|
|
startround();
|
|
}
|
|
}
|
|
|
|
void gameobject::snap(int snapping_client)
|
|
{
|
|
obj_game *game = (obj_game *)snap_new_item(OBJTYPE_GAME, id, sizeof(obj_game));
|
|
game->paused = world.paused;
|
|
game->game_over = game_over_tick==-1?0:1;
|
|
game->sudden_death = sudden_death;
|
|
|
|
game->score_limit = config.scorelimit;
|
|
game->time_limit = config.timelimit;
|
|
game->round_start_tick = round_start_tick;
|
|
}
|
|
|
|
gameobject gameobj;
|
|
|
|
//////////////////////////////////////////////////
|
|
// projectile
|
|
//////////////////////////////////////////////////
|
|
projectile::projectile(int type, int owner, vec2 pos, vec2 vel, int span, entity* powner,
|
|
int damage, int flags, float force, int sound_impact, int weapon)
|
|
: entity(OBJTYPE_PROJECTILE)
|
|
{
|
|
this->type = type;
|
|
this->pos = pos;
|
|
this->vel = vel;
|
|
this->lifespan = span;
|
|
this->owner = owner;
|
|
this->powner = powner;
|
|
this->flags = flags;
|
|
this->force = force;
|
|
this->damage = damage;
|
|
this->sound_impact = sound_impact;
|
|
this->weapon = weapon;
|
|
world.insert_entity(this);
|
|
}
|
|
|
|
void projectile::reset()
|
|
{
|
|
world.destroy_entity(this);
|
|
}
|
|
|
|
void projectile::tick()
|
|
{
|
|
vec2 oldpos = pos;
|
|
vel.y += 0.25f;
|
|
pos += vel;
|
|
lifespan--;
|
|
|
|
// check player intersection as well
|
|
entity *targetplayer = (entity*)intersect_player(oldpos, pos, oldpos, powner);
|
|
if(targetplayer || lifespan < 0 || col_check_point((int)pos.x, (int)pos.y))
|
|
{
|
|
if (lifespan >= 0)
|
|
create_sound(pos, sound_impact);
|
|
|
|
if (flags & PROJECTILE_FLAGS_EXPLODE)
|
|
create_explosion(oldpos, owner, weapon, false);
|
|
else if (targetplayer)
|
|
targetplayer->take_damage(normalize(vel) * max(0.001f, force), damage, owner, weapon);
|
|
|
|
world.destroy_entity(this);
|
|
}
|
|
}
|
|
|
|
void projectile::snap(int snapping_client)
|
|
{
|
|
obj_projectile *proj = (obj_projectile *)snap_new_item(OBJTYPE_PROJECTILE, id, sizeof(obj_projectile));
|
|
proj->x = (int)pos.x;
|
|
proj->y = (int)pos.y;
|
|
proj->vx = (int)vel.x; // TODO: should be an angle
|
|
proj->vy = (int)vel.y;
|
|
proj->type = type;
|
|
}
|
|
|
|
//////////////////////////////////////////////////
|
|
// player
|
|
//////////////////////////////////////////////////
|
|
// TODO: move to separate file
|
|
player::player()
|
|
: entity(OBJTYPE_PLAYER)
|
|
{
|
|
init();
|
|
}
|
|
|
|
void player::init()
|
|
{
|
|
proximity_radius = phys_size;
|
|
name[0] = 'n';
|
|
name[1] = 'o';
|
|
name[2] = 'o';
|
|
name[3] = 'b';
|
|
name[4] = 0;
|
|
client_id = -1;
|
|
reset();
|
|
}
|
|
|
|
void player::reset()
|
|
{
|
|
release_hooked();
|
|
release_hooks();
|
|
|
|
pos = vec2(0.0f, 0.0f);
|
|
vel = vec2(0.0f, 0.0f);
|
|
direction = vec2(0.0f, 1.0f);
|
|
score = 0;
|
|
dead = true;
|
|
die_tick = 0;
|
|
}
|
|
|
|
void player::destroy() { }
|
|
|
|
void player::respawn()
|
|
{
|
|
health = PLAYER_MAXHEALTH;
|
|
armor = 0;
|
|
jumped = 0;
|
|
dead = false;
|
|
set_flag(entity::FLAG_ALIVE);
|
|
|
|
mem_zero(&input, sizeof(input));
|
|
vel = vec2(0.0f, 0.0f);
|
|
|
|
// get spawn point
|
|
int start, num;
|
|
map_get_type(1, &start, &num);
|
|
|
|
if(num)
|
|
{
|
|
mapres_spawnpoint *sp = (mapres_spawnpoint*)map_get_item(start + (rand()%num), NULL, NULL);
|
|
pos = vec2((float)sp->x, (float)sp->y);
|
|
}
|
|
else
|
|
pos = vec2(100.0f, -60.0f);
|
|
defered_pos = pos;
|
|
|
|
// init weapons
|
|
mem_zero(&weapons, sizeof(weapons));
|
|
weapons[WEAPON_HAMMER].got = true;
|
|
weapons[WEAPON_HAMMER].ammo = -1;
|
|
weapons[WEAPON_GUN].got = true;
|
|
weapons[WEAPON_GUN].ammo = 10;
|
|
active_weapon = WEAPON_GUN;
|
|
reload_timer = 0;
|
|
|
|
create_sound(pos, SOUND_PLAYER_SPAWN);
|
|
}
|
|
|
|
bool player::is_grounded()
|
|
{
|
|
if(col_check_point((int)(pos.x+phys_size/2), (int)(pos.y+phys_size/2+5)))
|
|
return true;
|
|
if(col_check_point((int)(pos.x-phys_size/2), (int)(pos.y+phys_size/2+5)))
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
// releases the hooked player
|
|
void player::release_hooked()
|
|
{
|
|
hook_state = HOOK_IDLE;
|
|
hooked_player = 0x0;
|
|
}
|
|
|
|
// release all hooks to this player
|
|
void player::release_hooks()
|
|
{
|
|
// TODO: loop thru players only
|
|
for(entity *ent = world.first_entity; ent; ent = ent->next_entity)
|
|
{
|
|
if(ent && ent->objtype == OBJTYPE_PLAYER)
|
|
{
|
|
player *p = (player*)ent;
|
|
if(p->hooked_player == this)
|
|
p->release_hooked();
|
|
}
|
|
}
|
|
}
|
|
|
|
void player::handle_weapons()
|
|
{
|
|
// check reload timer
|
|
if(reload_timer)
|
|
{
|
|
reload_timer--;
|
|
return;
|
|
}
|
|
|
|
// switch weapon if wanted
|
|
if(input.activeweapon >= 0 && input.activeweapon < NUM_WEAPONS && weapons[input.activeweapon].got)
|
|
active_weapon = input.activeweapon;
|
|
|
|
if(input.fire)
|
|
{
|
|
if(reload_timer == 0)
|
|
{
|
|
// fire!
|
|
if(weapons[active_weapon].ammo)
|
|
{
|
|
switch(active_weapon)
|
|
{
|
|
case WEAPON_HAMMER:
|
|
break;
|
|
|
|
case WEAPON_GUN:
|
|
new projectile(WEAPON_PROJECTILETYPE_GUN,
|
|
client_id,
|
|
pos+vec2(0,0),
|
|
direction*30.0f,
|
|
100,
|
|
this,
|
|
1, 0, 0, -1, WEAPON_GUN);
|
|
break;
|
|
case WEAPON_ROCKET:
|
|
new projectile(WEAPON_PROJECTILETYPE_ROCKET,
|
|
client_id,
|
|
pos+vec2(0,0),
|
|
direction*15.0f,
|
|
100,
|
|
this,
|
|
1, projectile::PROJECTILE_FLAGS_EXPLODE, 0, -1, WEAPON_ROCKET);
|
|
break;
|
|
case WEAPON_SHOTGUN:
|
|
for(int i = -2; i <= 2; i++)
|
|
{
|
|
float a = get_angle(direction);
|
|
a += i*0.075f;
|
|
new projectile(WEAPON_PROJECTILETYPE_SHOTGUN,
|
|
client_id,
|
|
pos+vec2(0,0),
|
|
vec2(cosf(a), sinf(a))*25.0f,
|
|
//vec2(cosf(a), sinf(a))*20.0f,
|
|
server_tickspeed()/3,
|
|
this,
|
|
1, 0, 0, -1, WEAPON_SHOTGUN);
|
|
}
|
|
break;
|
|
}
|
|
|
|
weapons[active_weapon].ammo--;
|
|
}
|
|
else
|
|
{
|
|
// click!!! click
|
|
}
|
|
|
|
attack_tick = server_tick();
|
|
reload_timer = 10; // make this variable depending on weapon
|
|
}
|
|
}
|
|
}
|
|
|
|
void player::tick()
|
|
{
|
|
// TODO: rework the input to be more robust
|
|
// TODO: remove this tick count, it feels weird
|
|
if(dead)
|
|
{
|
|
if(server_tick()-die_tick >= server_tickspeed()*3) // auto respawn after 3 sec
|
|
respawn();
|
|
if(input.fire && server_tick()-die_tick >= server_tickspeed()/2) // auto respawn after 0.5 sec
|
|
respawn();
|
|
return;
|
|
}
|
|
|
|
// fetch some info
|
|
bool grounded = is_grounded();
|
|
direction = get_direction(input.angle);
|
|
|
|
float max_speed = grounded ? ground_control_speed : air_control_speed;
|
|
float accel = grounded ? ground_control_accel : air_control_accel;
|
|
float friction = grounded ? ground_friction : air_friction;
|
|
|
|
// handle movement
|
|
if(input.left)
|
|
vel.x = saturated_add(-max_speed, max_speed, vel.x, -accel);
|
|
if(input.right)
|
|
vel.x = saturated_add(-max_speed, max_speed, vel.x, accel);
|
|
|
|
if(!input.left && !input.right)
|
|
vel.x *= friction;
|
|
|
|
// handle jumping
|
|
if(input.jump)
|
|
{
|
|
if(!jumped && grounded)
|
|
{
|
|
create_sound(pos, SOUND_PLAYER_JUMP);
|
|
vel.y = -ground_jump_speed;
|
|
jumped++;
|
|
}
|
|
}
|
|
else
|
|
jumped = 0;
|
|
|
|
// do hook
|
|
if(input.hook)
|
|
{
|
|
if(hook_state == HOOK_IDLE)
|
|
{
|
|
hook_state = HOOK_FLYING;
|
|
hook_pos = pos;
|
|
hook_dir = direction;
|
|
}
|
|
else if(hook_state == HOOK_FLYING)
|
|
{
|
|
vec2 new_pos = hook_pos+hook_dir*hook_fire_speed;
|
|
|
|
// Check against other players first
|
|
for(entity *ent = world.first_entity; ent; ent = ent->next_entity)
|
|
{
|
|
if(ent && ent->objtype == OBJTYPE_PLAYER)
|
|
{
|
|
player *p = (player*)ent;
|
|
if(p != this && distance(p->pos, new_pos) < p->phys_size)
|
|
{
|
|
hook_state = HOOK_GRABBED;
|
|
hooked_player = p;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(hook_state == HOOK_FLYING)
|
|
{
|
|
// check against ground
|
|
if(col_intersect_line(hook_pos, new_pos, &new_pos))
|
|
{
|
|
hook_state = HOOK_GRABBED;
|
|
hook_pos = new_pos;
|
|
}
|
|
else if(distance(pos, new_pos) > hook_length)
|
|
{
|
|
hook_state = HOOK_RETRACTED;
|
|
}
|
|
else
|
|
hook_pos = new_pos;
|
|
}
|
|
|
|
if(hook_state == HOOK_GRABBED)
|
|
create_sound(pos, SOUND_HOOK_ATTACH);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
release_hooked();
|
|
hook_pos = pos;
|
|
}
|
|
|
|
if(hook_state == HOOK_GRABBED)
|
|
{
|
|
if(hooked_player)
|
|
hook_pos = hooked_player->pos;
|
|
|
|
float d = distance(pos, hook_pos);
|
|
vec2 dir = normalize(pos - hook_pos);
|
|
if(d > 10.0f) // TODO: fix tweakable variable
|
|
{
|
|
float accel = hook_drag_accel * (d/hook_length);
|
|
vel.x = saturated_add(-hook_drag_speed, hook_drag_speed, vel.x, -accel*dir.x*0.75f);
|
|
vel.y = saturated_add(-hook_drag_speed, hook_drag_speed, vel.y, -accel*dir.y);
|
|
}
|
|
}
|
|
|
|
// fix influence of other players, collision + hook
|
|
// TODO: loop thru players only
|
|
for(entity *ent = world.first_entity; ent; ent = ent->next_entity)
|
|
{
|
|
if(ent && ent->objtype == OBJTYPE_PLAYER)
|
|
{
|
|
player *p = (player*)ent;
|
|
if(p == this || !(p->flags&FLAG_ALIVE))
|
|
continue; // make sure that we don't nudge our self
|
|
|
|
// handle player <-> player collision
|
|
float d = distance(pos, p->pos);
|
|
vec2 dir = normalize(pos - p->pos);
|
|
if(d < phys_size*1.25f)
|
|
{
|
|
float a = phys_size*1.25f - d;
|
|
vel = vel + dir*a;
|
|
}
|
|
|
|
// handle hook influence
|
|
if(p->hooked_player == this)
|
|
{
|
|
if(d > phys_size*1.50f) // TODO: fix tweakable variable
|
|
{
|
|
float accel = hook_drag_accel * (d/hook_length);
|
|
vel.x = saturated_add(-hook_drag_speed, hook_drag_speed, vel.x, -accel*dir.x);
|
|
vel.y = saturated_add(-hook_drag_speed, hook_drag_speed, vel.y, -accel*dir.y);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// handle weapons
|
|
handle_weapons();
|
|
|
|
// add gravity
|
|
vel.y += gravity;
|
|
|
|
// do the move
|
|
defered_pos = pos;
|
|
move_box(&defered_pos, &vel, vec2(phys_size, phys_size), 0);
|
|
return;
|
|
}
|
|
|
|
void player::tick_defered()
|
|
{
|
|
// apply the new position
|
|
pos = defered_pos;
|
|
}
|
|
|
|
void player::die(int killer, int weapon)
|
|
{
|
|
// send the kill message
|
|
msg_pack_start(MSG_KILLMSG, MSGFLAG_VITAL);
|
|
msg_pack_int(killer);
|
|
msg_pack_int(client_id);
|
|
msg_pack_int(weapon);
|
|
msg_pack_end();
|
|
server_send_msg(-1);
|
|
|
|
// a nice sound
|
|
create_sound(pos, SOUND_PLAYER_DIE);
|
|
|
|
// release all hooks
|
|
release_hooked();
|
|
release_hooks();
|
|
|
|
// set dead state
|
|
dead = true;
|
|
die_tick = server_tick();
|
|
clear_flag(entity::FLAG_ALIVE);
|
|
}
|
|
|
|
bool player::take_damage(vec2 force, int dmg, int from, int weapon)
|
|
{
|
|
vel += force;
|
|
|
|
// create healthmod indicator
|
|
create_damageind(pos, normalize(force), dmg);
|
|
|
|
if(armor)
|
|
{
|
|
armor -= 1;
|
|
dmg--;
|
|
}
|
|
|
|
if(dmg > armor)
|
|
{
|
|
dmg -= armor;
|
|
armor = 0;
|
|
health -= dmg;
|
|
}
|
|
else
|
|
armor -= dmg;
|
|
|
|
damage_taken_tick = server_tick()+50;
|
|
|
|
// check for death
|
|
if(health <= 0)
|
|
{
|
|
// apply score
|
|
if(from != -1)
|
|
{
|
|
if(from == client_id)
|
|
score--;
|
|
else
|
|
{
|
|
player *p = get_player(from);
|
|
p->score++;
|
|
}
|
|
}
|
|
|
|
die(from, weapon);
|
|
return false;
|
|
}
|
|
|
|
if (dmg > 2)
|
|
create_sound(pos, SOUND_PLAYER_PAIN_LONG);
|
|
else
|
|
create_sound(pos, SOUND_PLAYER_PAIN_SHORT);
|
|
|
|
// spawn blood?
|
|
return true;
|
|
}
|
|
|
|
void player::snap(int snaping_client)
|
|
{
|
|
obj_player *player = (obj_player *)snap_new_item(OBJTYPE_PLAYER, client_id, sizeof(obj_player));
|
|
|
|
player->x = (int)pos.x;
|
|
player->y = (int)pos.y;
|
|
player->vx = (int)vel.x;
|
|
player->vy = (int)vel.y;
|
|
player->emote = EMOTE_NORMAL;
|
|
|
|
player->ammocount = weapons[active_weapon].ammo;
|
|
player->health = 0;
|
|
player->armor = 0;
|
|
player->local = 0;
|
|
player->clientid = client_id;
|
|
player->weapon = active_weapon;
|
|
player->attacktick = attack_tick;
|
|
|
|
if(client_id == snaping_client)
|
|
{
|
|
player->local = 1;
|
|
player->health = health;
|
|
player->armor = armor;
|
|
}
|
|
|
|
if(dead)
|
|
player->health = -1;
|
|
|
|
if(length(vel) > 15.0f)
|
|
player->emote = EMOTE_HAPPY;
|
|
|
|
if(damage_taken_tick > server_tick())
|
|
player->emote = EMOTE_PAIN;
|
|
|
|
if(player->emote == EMOTE_NORMAL)
|
|
{
|
|
if((server_tick()%(50*5)) < 10)
|
|
player->emote = EMOTE_BLINK;
|
|
}
|
|
|
|
player->hook_active = hook_state>0?1:0;
|
|
player->hook_x = (int)hook_pos.x;
|
|
player->hook_y = (int)hook_pos.y;
|
|
|
|
player->angle = input.angle;
|
|
player->score = score;
|
|
}
|
|
|
|
player players[MAX_CLIENTS];
|
|
|
|
//////////////////////////////////////////////////
|
|
// powerup
|
|
//////////////////////////////////////////////////
|
|
powerup::powerup(int _type, int _subtype)
|
|
: entity(OBJTYPE_POWERUP)
|
|
{
|
|
type = _type;
|
|
subtype = _subtype;
|
|
proximity_radius = phys_size;
|
|
|
|
reset();
|
|
|
|
// TODO: should this be done here?
|
|
world.insert_entity(this);
|
|
}
|
|
|
|
void powerup::reset()
|
|
{
|
|
spawntick = -1;
|
|
}
|
|
|
|
void powerup::tick()
|
|
{
|
|
// wait for respawn
|
|
if(spawntick > 0)
|
|
{
|
|
if(server_tick() > spawntick)
|
|
spawntick = -1;
|
|
else
|
|
return;
|
|
}
|
|
// Check if a player intersected us
|
|
vec2 meh;
|
|
player* pplayer = intersect_player(pos, pos + vec2(0,16), meh, 0);
|
|
if (pplayer)
|
|
{
|
|
// player picked us up, is someone was hooking us, let them go
|
|
int respawntime = -1;
|
|
switch (type)
|
|
{
|
|
case POWERUP_TYPE_HEALTH:
|
|
if(pplayer->health < PLAYER_MAXHEALTH)
|
|
{
|
|
pplayer->health = min((int)PLAYER_MAXHEALTH, pplayer->health + 1);
|
|
respawntime = 20;
|
|
}
|
|
break;
|
|
case POWERUP_TYPE_ARMOR:
|
|
if(pplayer->armor < PLAYER_MAXARMOR)
|
|
{
|
|
pplayer->armor = min((int)PLAYER_MAXARMOR, pplayer->armor + 1);
|
|
respawntime = 20;
|
|
}
|
|
break;
|
|
|
|
case POWERUP_TYPE_WEAPON:
|
|
if(subtype >= 0 && subtype < NUM_WEAPONS)
|
|
{
|
|
if(pplayer->weapons[subtype].ammo < 10 || !pplayer->weapons[subtype].got)
|
|
{
|
|
pplayer->weapons[subtype].got = true;
|
|
pplayer->weapons[subtype].ammo = min(10, pplayer->weapons[subtype].ammo + 5);
|
|
respawntime = 20;
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
};
|
|
|
|
if(respawntime >= 0)
|
|
spawntick = server_tick() + server_tickspeed() * respawntime;
|
|
}
|
|
}
|
|
|
|
void powerup::snap(int snapping_client)
|
|
{
|
|
if(spawntick != -1)
|
|
return;
|
|
|
|
obj_powerup *up = (obj_powerup *)snap_new_item(OBJTYPE_POWERUP, id, sizeof(obj_powerup));
|
|
up->x = (int)pos.x;
|
|
up->y = (int)pos.y;
|
|
up->type = type; // TODO: two diffrent types? what gives?
|
|
up->subtype = subtype;
|
|
}
|
|
|
|
// POWERUP END ///////////////////////
|
|
|
|
player *get_player(int index)
|
|
{
|
|
return &players[index];
|
|
}
|
|
|
|
void create_damageind(vec2 p, vec2 dir, int amount)
|
|
{
|
|
float a = get_angle(dir);
|
|
float s = a-pi/3;
|
|
float e = a+pi/3;
|
|
for(int i = 0; i < amount; i++)
|
|
{
|
|
float f = mix(s, e, float(i+1)/float(amount+2));
|
|
ev_damageind *ev = (ev_damageind *)events.create(EVENT_DAMAGEINDICATION, sizeof(ev_damageind));
|
|
ev->x = (int)p.x;
|
|
ev->y = (int)p.y;
|
|
ev->angle = (int)(f*256.0f);
|
|
}
|
|
}
|
|
|
|
void create_explosion(vec2 p, int owner, int weapon, bool bnodamage)
|
|
{
|
|
// create the event
|
|
ev_explosion *ev = (ev_explosion *)events.create(EVENT_EXPLOSION, sizeof(ev_explosion));
|
|
ev->x = (int)p.x;
|
|
ev->y = (int)p.y;
|
|
|
|
if (!bnodamage)
|
|
{
|
|
// deal damage
|
|
entity *ents[64];
|
|
const float radius = 128.0f;
|
|
int num = world.find_entities(p, radius, ents, 64);
|
|
for(int i = 0; i < num; i++)
|
|
{
|
|
vec2 diff = ents[i]->pos - p;
|
|
vec2 forcedir(0,1);
|
|
if (length(diff))
|
|
forcedir = normalize(diff);
|
|
float l = length(diff);
|
|
float dmg = 5 * (1 - (l/radius));
|
|
if((int)dmg)
|
|
ents[i]->take_damage(forcedir*dmg*2, (int)dmg, owner, weapon);
|
|
}
|
|
}
|
|
}
|
|
|
|
void create_smoke(vec2 p)
|
|
{
|
|
// create the event
|
|
ev_explosion *ev = (ev_explosion *)events.create(EVENT_SMOKE, sizeof(ev_explosion));
|
|
ev->x = (int)p.x;
|
|
ev->y = (int)p.y;
|
|
}
|
|
|
|
void create_sound(vec2 pos, int sound, int loopingflags)
|
|
{
|
|
if (sound < 0)
|
|
return;
|
|
|
|
// create a sound
|
|
ev_sound *ev = (ev_sound *)events.create(EVENT_SOUND, sizeof(ev_sound));
|
|
ev->x = (int)pos.x;
|
|
ev->y = (int)pos.y;
|
|
ev->sound = sound | loopingflags;
|
|
}
|
|
|
|
// TODO: should be more general
|
|
player* intersect_player(vec2 pos0, vec2 pos1, vec2& new_pos, entity* notthis)
|
|
{
|
|
// Find other players
|
|
entity *ents[64];
|
|
vec2 dir = pos1 - pos0;
|
|
float radius = length(dir * 0.5f);
|
|
vec2 center = pos0 + dir * 0.5f;
|
|
const int types[] = {OBJTYPE_PLAYER};
|
|
int num = world.find_entities(center, radius, ents, 64, types, 1);
|
|
for (int i = 0; i < num; i++)
|
|
{
|
|
// Check if entity is a player
|
|
if (ents[i] != notthis)
|
|
{
|
|
new_pos = ents[i]->pos;
|
|
return (player*)ents[i];
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Server hooks
|
|
void mods_tick()
|
|
{
|
|
// clear all events
|
|
events.clear();
|
|
world.tick();
|
|
|
|
if(world.paused) // make sure that the game object always updates
|
|
gameobj.tick();
|
|
|
|
if(debug_bots)
|
|
{
|
|
static int count = 0;
|
|
if(count >= 0)
|
|
{
|
|
count++;
|
|
if(count == 10)
|
|
{
|
|
for(int i = 0; i < 1; i++)
|
|
{
|
|
mods_client_enter(MAX_CLIENTS-i-1);
|
|
strcpy(players[MAX_CLIENTS-i-1].name, "(bot)");
|
|
}
|
|
count = -1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void mods_snap(int client_id)
|
|
{
|
|
world.snap(client_id);
|
|
events.snap(client_id);
|
|
}
|
|
|
|
void mods_client_input(int client_id, void *input)
|
|
{
|
|
if(!world.paused)
|
|
{
|
|
players[client_id].previnput = players[client_id].input;
|
|
players[client_id].input = *(player_input*)input;
|
|
}
|
|
}
|
|
|
|
void mods_client_enter(int client_id)
|
|
{
|
|
players[client_id].init();
|
|
players[client_id].client_id = client_id;
|
|
world.insert_entity(&players[client_id]);
|
|
players[client_id].respawn();
|
|
|
|
|
|
client_info info; // fetch login name
|
|
if(server_getclientinfo(client_id, &info))
|
|
strcpy(players[client_id].name, info.name);
|
|
else
|
|
strcpy(players[client_id].name, "(bot)");
|
|
|
|
msg_pack_start(MSG_SETNAME, MSGFLAG_VITAL);
|
|
msg_pack_int(client_id);
|
|
msg_pack_string(players[client_id].name, 64);
|
|
msg_pack_end();
|
|
server_send_msg(-1);
|
|
|
|
for(int i = 0; i < MAX_CLIENTS; i++)
|
|
{
|
|
if(players[client_id].client_id != -1)
|
|
{
|
|
msg_pack_start(MSG_SETNAME, MSGFLAG_VITAL);
|
|
msg_pack_int(i);
|
|
msg_pack_string(players[i].name, 64);
|
|
msg_pack_end();
|
|
server_send_msg(client_id);
|
|
}
|
|
}
|
|
}
|
|
|
|
void mods_client_drop(int client_id)
|
|
{
|
|
players[client_id].client_id = -1;
|
|
world.remove_entity(&players[client_id]);
|
|
}
|
|
|
|
void mods_message(int msg, int client_id)
|
|
{
|
|
if(msg == MSG_SAY)
|
|
{
|
|
msg_pack_start(MSG_CHAT, MSGFLAG_VITAL);
|
|
msg_pack_int(client_id);
|
|
msg_pack_string(msg_unpack_string(), 512);
|
|
msg_pack_end();
|
|
server_send_msg(-1);
|
|
}
|
|
}
|
|
|
|
void mods_init()
|
|
{
|
|
col_init(32);
|
|
|
|
int start, num;
|
|
map_get_type(MAPRES_ITEM, &start, &num);
|
|
|
|
// TODO: this is way more complicated then it should be
|
|
for(int i = 0; i < num; i++)
|
|
{
|
|
mapres_item *it = (mapres_item *)map_get_item(start+i, 0, 0);
|
|
|
|
int type = -1;
|
|
int subtype = -1;
|
|
|
|
switch(it->type)
|
|
{
|
|
case ITEM_WEAPON_GUN:
|
|
type = POWERUP_TYPE_WEAPON;
|
|
subtype = WEAPON_TYPE_GUN;
|
|
break;
|
|
case ITEM_WEAPON_SHOTGUN:
|
|
type = POWERUP_TYPE_WEAPON;
|
|
subtype = WEAPON_TYPE_SHOTGUN;
|
|
break;
|
|
case ITEM_WEAPON_ROCKET:
|
|
type = POWERUP_TYPE_WEAPON;
|
|
subtype = WEAPON_TYPE_ROCKET;
|
|
break;
|
|
case ITEM_WEAPON_HAMMER:
|
|
type = POWERUP_TYPE_WEAPON;
|
|
subtype = WEAPON_TYPE_MELEE;
|
|
break;
|
|
|
|
case ITEM_HEALTH:
|
|
type = POWERUP_TYPE_HEALTH;
|
|
break;
|
|
|
|
case ITEM_ARMOR:
|
|
type = POWERUP_TYPE_ARMOR;
|
|
break;
|
|
};
|
|
|
|
powerup *ppower = new powerup(type, subtype);
|
|
ppower->pos = vec2(it->x, it->y);
|
|
}
|
|
|
|
world.insert_entity(&gameobj);
|
|
}
|
|
|
|
void mods_shutdown() {}
|
|
void mods_presnap() {}
|
|
void mods_postsnap() {}
|