/* 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