/*
This file is part of Minitrem.
Minitrem is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
Minitrem is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Minitrem. If not, see .
*/
#include "game.hpp"
namespace game {
unit_t::unit_t(game::state_t *game_, unit_t::type_t type_) : game::entity_t(game_, ET_UNIT)
{
type = type_;
}
void unit_t::render_to(render::state_t *render)
{
if (!dead && health < max_health)
{
rectf_t bar;
float frac;
sf::Color color;
bar[0][0] = (render_bounds[0][0] + render_bounds[1][0]) / 2;
bar[0][1] = render_bounds[0][1] - 0.05f;
bar[0][0] -= 0.3f;
bar[1][0] = bar[0][0] + 0.6f;
bar[1][1] = bar[0][1] + 0.05f;
render->render_rect(bar, sf::Color::Black);
frac = (float)health / max_health;
if (frac < 0.3f)
color = sf::Color::Red;
else if (frac < 0.75f)
color = sf::Color::Yellow;
else
color = sf::Color::Green;
bar[1][0] = lerp(bar[0][0], bar[1][0], frac);
render->render_rect(bar, color);
}
if (move.moving && debug_draw_paths)
render->debug_path(x, &move.path);
if (!dead && say_time + 5.0 > game->now) {
v2f_t text_pos;
float height;
text_pos = render_bounds[0] + v2f_t(render_bounds.dim(0) / 2, -render_bounds.dim(1) * 0.1);
height = size.dim_min() * 0.50f;
render->render_text(text_pos, height, say_text,
render::ALIGN_CENTER_BOTTOM,
sf::Color::White);
}
if (debug_AI) {
v2f_t text_pos;
float height;
std::stringstream ss;
if (dead)
ss << "D";
if (awake)
ss << "A";
else
ss << "S";
if (controllable)
ss << "C";
text_pos = render_bounds[0] + v2f_t(render_bounds.dim(0) / 2, -render_bounds.dim(1) * 0.1);
height = size.dim_min() * 0.40f;
text_pos[1] -= height;
render->render_text(text_pos, height, ss.str(),
render::ALIGN_CENTER_BOTTOM,
sf::Color::White);
}
}
void unit_t::say(std::string str)
{
say_text = str;
say_time = game->now;
game->interface->print(name + ": " + str);
}
bool unit_t::keep_moving(double speed)
{
float time;
bool rv = true;
if (!move.moving)
return true;
if (move.blocked && game->now < move.next_attempt)
return true;
time = game->dt * speed;
move.blocked = true;
while (time > 0.0f) {
v2f_t delta, next, x_new;
world::cmodel_t test_cmodel;
if (!move.path.size()) {
move.moving = false;
break;
}
next = *move.path.begin();
delta = next - x;
if (delta.len() != 0.0f)
move.angle = delta.angle();
if (delta.len() >= time) {
x_new = x + delta * time / delta.len();
time -= delta.len();
} else {
x_new = next;
time -= delta.len();
move.path.pop_front();
}
test_cmodel.bounds = size + x_new;
test_cmodel.cflags = move.cflags;
if (!world->test_rect(&test_cmodel, this)) {
x = x_new;
cmodel.bounds = test_cmodel.bounds;
move.blocked = false;
continue;
}
if (move.attempts_left) {
move.attempts_left--;
move.next_attempt = game->now + 0.2f;
} else {
if ((x - move.dst).len() > 1.5f)
rv = false;
move.moving = false;
}
break;
}
place(world, x);
return rv;
}
bool unit_t::start_moving(v2f_t dst)
{
world::cmodel_t rep;
if (!world) {
printf("unit_t::start_moving: entity is not linked\n");
return false;
}
move.dst = dst;
move.path.clear();
rep.cflags = move.cflags & ~(cmodel.cflags);
rep.bounds = cmodel.bounds;
if (!world->find_path(x, move.dst, &rep, this, &move.path)) {
move.moving = false;
return false;
}
move.moving = true;
move.blocked = false;
move.attempts_left = 10;
move.next_attempt = -INFINITY;
return true;
}
void unit_t::damage(int points, unit_t *attacker)
{
fx_blood_t *blood;
blood = new fx_blood_t(game, x, type == UNIT_SPIDER);
blood->place(&game->world);
health -= points;
if (health <= 0)
die(attacker);
}
void unit_t::die(unit_t *killer)
{
game->interface->print(name + " " + text::get(text::UNIT_DEATH) + ".");
dead = true;
death_time = game->now;
cmodel.cflags = 0;
move.path.clear();
sleep();
ignore_waking = true;
on_death();
}
void unit_t::try_attack(unit_t *target)
{
std::stringstream ss;
size_t hit_roll;
size_t dmg_roll;
if (sounds.attack)
sounds.attack->play();
ss << name << " " << text::get(text::UNIT_ATTACK) << " " << target->name << ": ";
hit_roll = game->roll(die_t(20));
ss << hit_roll << " vs " << target->cs.armor_class;
if (hit_roll == 1 || hit_roll < target->cs.armor_class) {
ss << " (" << text::get((hit_roll == 1 ? text::UNIT_CRITICAL_MISS : text::UNIT_MISS)) << ")";
game->interface->print(ss.str());
return;
}
dmg_roll = game->roll(cs.hit_die);
if (hit_roll < 20) {
ss << ", " << text::get(text::UNIT_DAMAGE) << ": ";
ss << dmg_roll - cs.hit_die.bonus << " + " << cs.hit_die.bonus << " = " << dmg_roll;
} else {
ss << " (" << text::get(text::UNIT_CRITICAL_HIT) << ")" << ", " << text::get(text::UNIT_DAMAGE) << ": ";
ss << "(" << dmg_roll - cs.hit_die.bonus << " + " << cs.hit_die.bonus << ") x 2 = " << dmg_roll * 2;
dmg_roll *= 2;
}
game->interface->print(ss.str());
target->damage(dmg_roll, this);
}
static unit_t *find_target(world::world_t *world, v2f_t x, float r,
bool friendly)
{
rectf_t rect;
float nearest_dist = INFINITY;
unit_t *nearest = NULL;
rect[0] = x - v2f_t(r, r);
rect[1] = x + v2f_t(r, r);
for (world::entity_t *ent : world->get_entities(rect, -1)) {
unit_t *unit;
float dist;
if (ent->type != ET_UNIT)
continue;
unit = (unit_t*)ent;
if (unit->friendly != friendly)
continue;
if (unit->dead)
continue;
dist = (unit->x - x).len();
if (dist < nearest_dist) {
nearest_dist = dist;
nearest = unit;
}
}
return nearest;
}
unit_soldier_t::unit_soldier_t(game::state_t *game) : unit_t(game, UNIT_SOLDIER)
{
size[0] = v2f_t(-0.3f, +0.0f);
size[1] = v2f_t(+0.3f, +0.3f);
render_size[0] = v2f_t(-0.5f, -1.2f);
render_size[1] = v2f_t(+0.5f, +0.3f);
cmodel.cflags = CF_BODY;
move.cflags = CF_SOLID | CF_BODY | CF_WATER;
name = text::get(text::UNIT_NAME_SOLDIER);
wake();
friendly = true;
controllable = true;
health = max_health = 20;
cs.armor_class = 10;
cs.hit_die = die_t(8);
sounds.attack = &assets::soldier.fire;
}
void unit_soldier_t::check_area(void)
{
rectf_t bounds;
bounds[0] = x - v2f_t(10, 10);
bounds[1] = x + v2f_t(10, 10);
willpower_bonus = 0;
fear_dc = 0;
for (world::entity_t *went : game->world.get_entities(bounds, -1)) {
auto ent = dynamic_cast(went);
unit_t *unit;
// WTF?
if (!ent)
continue;
if (ent == this)
continue;
// Wake everything around.
if (!ent->ignore_waking) {
ent->wake_time = game->now;
if (!ent->awake)
ent->wake();
}
if (ent->type != ET_UNIT)
continue;
unit = (unit_t*)ent;
if (unit->dead)
continue;
if (unit->friendly)
willpower_bonus += 6;
else {
if (unit->type == UNIT_NEST)
fear_dc += 6;
else
fear_dc += 4;
}
}
}
void unit_soldier_t::target_and_attack(void)
{
unit_t *target;
world::trace_t trace;
fx_tracer_t *tracer;
if (game->now < next_targetting)
return;
next_targetting = game->now + 0.2;
target = find_target(world, x, 10.0f, false);
if (!target)
return;
last_target_time = game->now;
last_target_x = target->x;
if (last_attack + 1.5 > game->now)
return;
trace = world->trace(x, target->x, CF_SOLID);
if (trace.hit)
return;
tracer = new fx_tracer_t(game, x, target->x);
tracer->place(&game->world);
last_attack = game->now;
try_attack(target);
}
void unit_soldier_t::on_think(void)
{
check_area();
if (panic && game->now > panic_end) {
move.moving = false;
move.path.clear();
panic = false;
controllable = true;
}
if (health == max_health)
willpower_bonus += 3;
else if (fear_dc > 1 && health < max_health / 2)
fear_dc += 3;
if (!panic && fear_dc > 1 && game->now > next_fear_test) {
size_t roll;
bool success;
std::stringstream ss;
roll = game->roll(die_t(20));
success = roll + willpower_bonus >= fear_dc;
ss << name << " " << text::get(text::UNIT_SAVING_THROW_WILLPOWER);
ss << ": " << roll << " + " << willpower_bonus << " = " << roll + willpower_bonus;
ss << " vs " << fear_dc << ": ";
if (success)
ss << text::get(text::UNIT_SAVING_THROW_SUCCESS);
else
ss << text::get(text::UNIT_SAVING_THROW_FAILURE);
game->interface->print(ss.str());
if (!success) {
say(text::get(text::SAY_PANIC));
panic = true;
panic_end = game->now + 10;
panic_turn = -INFINITY;
controllable = false;
}
next_fear_test = game->now + 3;
}
if (!panic) {
target_and_attack();
keep_moving(4.0);
if (!move.moving)
move_marker.reset();
} else {
move.moving = true;
keep_moving(6.0);
if (game->now >= panic_turn) {
v2f_t t;
t = game->dice_prng.unit_vec2();
move.path.clear();
move.path.push_back(x + t * 10);
panic_turn = game->now + 0.3;
}
}
}
void unit_soldier_t::on_death(void)
{
render_size[0] = v2f_t(-0.75f, -0.5f);
render_size[1] = v2f_t(+0.75f, +0.5f);
render_layer = -1;
cmodel.cflags = CF_BACKGROUND;
place(world, x);
controllable = false;
game->selected_units.erase(this);
move_marker.reset();
}
void unit_soldier_t::render_to(render::state_t *render)
{
sf::Color selection_color;
if (selected == selection_cookie) {
if (health == max_health)
selection_color = sf::Color::Green;
else if (health >= max_health / 2)
selection_color = sf::Color::Yellow;
else if (dead)
selection_color = sf::Color::Black;
else
selection_color = sf::Color::Red;
if (panic && (game->now - floor(game->now) > 0.5))
selection_color = sf::Color::Blue;
render->render(0.0, &assets::unit_selected, cmodel.bounds, selection_color);
}
if (!dead) {
render::oriented_sprite_t *legs, *body;
float body_angle;
if (move.moving && !move.blocked)
legs = &assets::soldier.legs_walking;
else
legs = &assets::soldier.legs_idle;
if (!panic && last_target_time + 3 > game->now) {
if (last_attack + 0.1 > game->now)
body = &assets::soldier.body_firing;
else
body = &assets::soldier.body_aiming;
body_angle = (last_target_x - x).angle();
} else {
body = &assets::soldier.body_idle;
body_angle = move.angle;
}
render->render(game->now * 10, legs, render_bounds, move.angle);
if (panic)
render->render(game->now * 10, &assets::soldier.body_panic, render_bounds);
else
render->render(game->now * 10, body, render_bounds, body_angle);
render->render(game->now * 10, &assets::soldier.head_idle, render_bounds, body_angle);
} else
render->render(game->now * 10, &assets::soldier.dead, render_bounds);
unit_t::render_to(render);
}
void unit_soldier_t::render_late_to(render::state_t *render)
{
if (selected == selection_cookie)
render->render(0.0, &assets::unit_selected_halo, cmodel.bounds, selection_color);
}
unit_spider_t::unit_spider_t(game::state_t *game) : unit_t(game, UNIT_SPIDER)
{
size[0] = v2f_t(-0.2f, +0.0f);
size[1] = v2f_t(+0.2f, +0.3f);
render_size[0] = v2f_t(-0.3f, -0.3f);
render_size[1] = v2f_t(+0.3f, +0.3f);
cmodel.cflags = CF_BODY_SMALL;
move.cflags = CF_SOLID | CF_WATER | CF_BODY_SMALL;
name = text::get(text::UNIT_NAME_SPIDER);
ignore_waking = false;
health = max_health = 4;
cs.armor_class = 15;
cs.hit_die = die_t(3, 6);
}
void unit_spider_t::target_and_attack(void)
{
unit_t *target;
world::trace_t trace;
if (game->now < next_targetting)
return;
target = find_target(world, x, 10.0f, true);
if (!target)
return;
start_moving(target->x);
next_targetting = game->now + 0.2;
if (last_attack + 0.5 > game->now)
return;
if ((x - target->x).len() >= 1.0f)
return;
trace = world->trace(x, target->x, CF_SOLID);
if (trace.hit)
return;
try_attack(target);
last_attack = game->now;
}
void unit_spider_t::on_think(void)
{
target_and_attack();
keep_moving(7.0);
if (!move.moving && wake_time + 5 < game->now)
sleep();
}
void unit_spider_t::on_wake(void)
{
next_targetting = game->now;
}
void unit_spider_t::on_death(void)
{
render_layer = -1;
cmodel.cflags = CF_BACKGROUND;
}
void unit_spider_t::render_to(render::state_t *render)
{
bool moving;
moving = move.moving && !move.blocked;
if (!dead)
render->render(game->now * 20, (moving ? &assets::spider.walking :
&assets::spider.idle), render_bounds, move.angle);
else
render->render(game->now * 20, &assets::spider.dead, render_bounds);
unit_t::render_to(render);
}
unit_nest_t::unit_nest_t(game::state_t *game_) : unit_t(game_, UNIT_NEST)
{
size[0] = {-0.6f, +0.2f};
size[1] = {+0.6f, +0.6f};
render_size[0] = {-0.6f, -0.6f};
render_size[1] = {+0.6f, +0.6f};
cmodel.cflags = CF_BACKGROUND;
name = text::get(text::UNIT_NAME_NEST);
ignore_waking = false;
health = max_health = 45;
cs.armor_class = 5;
next_spawning = game->now + 5.0;
}
void spawn_spider(game::state_t *game, v2f_t nest)
{
for (size_t i = 0; i < 5; i++) { // Try a few times before giving up.
v2f_t offset, x;
world::cmodel_t cmodel;
unit_spider_t *spider;
offset = game->dice_prng.unit_vec2();
x = nest + offset * (game->dice_prng.next_float() * 0.2 + 0.4);
cmodel.bounds = rectf_t(v2f_t(-0.5f, -0.5f), v2f_t(0.5f, 0.5f)) + x;
cmodel.cflags = CF_SOLID | CF_WATER | CF_BODY_SMALL;
if (game->world.test_rect(&cmodel, NULL))
continue;
spider = new unit_spider_t(game);
spider->place(&game->world, x);
return;
}
}
void unit_nest_t::on_think(void)
{
if (wake_time + 30 > game->now)
sleep();
if (next_spawning > game->now)
return;
spawn_spider(game, x);
next_spawning = game->now + game->dice_prng.next_float() * 10 + 5;
}
void unit_nest_t::on_spawn(void)
{
spawn_spider(game, x);
spawn_spider(game, x);
spawn_spider(game, x);
}
void unit_nest_t::on_death(void)
{
render_layer = -1;
cmodel.cflags = CF_BACKGROUND;
}
void unit_nest_t::render_to(render::state_t *render)
{
if (!dead)
render->render(game->now, &assets::nest.idle, render_bounds);
else
render->render(game->now, &assets::nest.dead, render_bounds);
unit_t::render_to(render);
}
} // namespace game