use crate::entities::tee::{TeeCore, TEE_PROXIMITY};
use crate::entities::{MapEntityItem, Pickup};
use crate::ids::MapSnapIdGenerator;
use crate::state::Prng;
use crate::tuning::Tuning;
use bitflags::bitflags;
use ndarray::{Array2, Axis};
use serde::{Deserialize, Serialize};
use std::array;
use std::convert::TryFrom;
use twgame_core::normalize;
use twgame_core::twsnap;
use twgame_core::twsnap::enums::{ActiveWeapon, CollectableWeapon, Direction, HookState};
use twgame_core::twsnap::flags::JumpFlags;
use twgame_core::twsnap::time::Instant;
use twgame_core::twsnap::Position;
use twmap::{FrontLayer, GameLayer, SwitchLayer, TeleLayer, TuneLayer};
use twmap::{GameTile, TileFlags};
use twmap::{LoadMultiple, TwMap};
use vek::num_traits::Zero;
use vek::Vec2;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum Tile {
Air,
Collision,
Unhookable,
Kill,
Freeze,
Unfreeze,
DeepFreezeEnable,
DeepFreezeDisable,
EnableEndlessHook,
DisableEndlessHook,
EnableInfiniteJumps,
DisableInfiniteJumps,
EnableWeaponHit,
DisableWeaponHit,
EnableSolo,
DisableSolo,
EnableTeeCollision,
DisableTeeCollision,
EnableTeeHook,
DisableTeeHook,
EnableJetpack,
DisableJetpack,
EnableTelegunGun,
DisableTelegunGun,
EnableTelegunLaser,
DisableTelegunLaser,
EnableTelegunGrenade,
DisableTelegunGrenade,
LiveFreezeEnable,
LiveFreezeDisable,
EvilGunTeleporter,
GunTeleporter,
Walljump,
DoubleJumpRefresher,
OldHookThrough,
HookThroughOnly,
HookThrough,
HookThroughFromDown,
HookThroughFromLeft,
HookThroughFromUp,
HookThroughFromRight,
StopDown,
StopUp,
StopRight,
StopLeft,
StopUpDown,
StopLeftRight,
StopAll,
UnlockTeam,
StartLine,
FinishLine,
}
impl Tile {
pub fn is_solid(self) -> bool {
matches!(self, Tile::Collision | Tile::Unhookable)
}
}
bitflags! {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct CantMove: u8 {
const UP = 0b_0001;
const DOWN = 0b_0010;
const LEFT = 0b_0100;
const RIGHT = 0b_1000;
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum StopperPosition {
Up,
Down,
Left,
Right,
}
impl CantMove {
pub fn apply_on_vel(self, vel: Vec2<f32>) -> Vec2<f32> {
let mut vel = vel;
if self.contains(CantMove::UP) && vel.y < 0.0
|| self.contains(CantMove::DOWN) && vel.y > 0.0
{
vel.y = 0.0;
}
if self.contains(CantMove::LEFT) && vel.x < 0.0
|| self.contains(CantMove::RIGHT) && vel.x > 0.0
{
vel.x = 0.0;
}
vel
}
fn apply_from_direction(self, direction: StopperPosition) -> Self {
match direction {
StopperPosition::Up => self.intersection(CantMove::UP),
StopperPosition::Down => self.intersection(CantMove::DOWN),
StopperPosition::Left => self.intersection(CantMove::LEFT),
StopperPosition::Right => self.intersection(CantMove::RIGHT),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum TeleTile {
Tee,
EvilTee,
Checkpoint,
Weapon,
Hook,
TeeCheckpointOut,
EvilTeeCheckpointOut,
}
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum DisableableWeapon {
Hammer,
Shotgun = 2,
Grenade,
Rifle,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum TelegunWeapon {
All,
Gun,
Grenade,
Rifle,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum SwitchTile {
JumpCountSetter(u8),
WeaponsOn,
WeaponsOff,
SpecificWeaponOn(DisableableWeapon),
SpecificWeaponOff(DisableableWeapon),
TimePenalty(u8),
TimeBonus(u8),
TelegunEvil(TelegunWeapon),
Telegun(TelegunWeapon),
Activate(u8),
Deactivate(u8),
ActivateTime(u8, u8),
DeactivateTime(u8, u8),
Freeze(u8, u8),
Deep(u8),
Undeep(u8),
LiveFreeze(u8),
LiveUnfreeze(u8),
}
#[derive(Clone, Debug)]
pub struct Map {
pub game_layer: Array2<Tile>,
pub front_layer: Option<Array2<Tile>>,
pub switch_layer: Option<Array2<Option<SwitchTile>>>,
pub tele_layer: Option<Array2<Option<(TeleTile, u8)>>>,
pub tele_outs: [Vec<Vec2<i32>>; 256],
pub tele_checkpoint_outs: [Vec<Vec2<i32>>; 256],
pub tune_layer: Option<Array2<u8>>,
pub entities: Vec<MapEntityItem>,
pub spawn_points: [Vec<Vec2<i32>>; 3],
pub tune_zones: [Tuning; 256],
pub snap_id_generator: MapSnapIdGenerator,
}
pub mod coord {
use vek::Vec2;
pub fn to_float(coord: Vec2<i32>) -> Vec2<f32> {
Vec2::new(coord.x as f32 * 32.0, coord.y as f32 * 32.0)
}
pub fn to_int(coord: Vec2<f32>) -> Vec2<i32> {
Vec2::new((coord.x.round() as i32) / 32, (coord.y.round() as i32) / 32)
}
pub fn to_int_without_round(coord: Vec2<f32>) -> Vec2<i32> {
Vec2::new((coord.x as i32) / 32, (coord.y as i32) / 32)
}
}
#[derive(Clone)]
pub struct GameFrontIterator<'a> {
cur: i32,
map: &'a Map,
x: u32,
y: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MapTile {
Tile(Tile),
SwitchTile(SwitchTile),
TeleTile((TeleTile, u8)),
}
impl Iterator for GameFrontIterator<'_> {
type Item = MapTile;
fn next(&mut self) -> Option<Self::Item> {
loop {
match self.cur {
0 => {
self.cur += 1;
return Some(MapTile::Tile(
self.map.game_layer[[self.y as usize, self.x as usize]],
));
}
1 => {
self.cur += 1;
if let Some(tile) = self
.map
.front_layer
.as_ref()
.map(|front_layer| front_layer[[self.y as usize, self.x as usize]])
{
if tile != Tile::Air {
return Some(MapTile::Tile(tile));
}
}
}
2 => {
self.cur += 1;
if let Some(switch_layer) = self.map.switch_layer.as_ref() {
if let Some(switch_tile) = switch_layer[[self.y as usize, self.x as usize]]
{
return Some(MapTile::SwitchTile(switch_tile));
}
}
}
3 => {
self.cur += 1;
if let Some(tele_layer) = self.map.tele_layer.as_ref() {
if let Some((tele_tile, tele_id)) =
tele_layer[[self.y as usize, self.x as usize]]
{
return Some(MapTile::TeleTile((tele_tile, tele_id)));
}
}
}
_ => return None,
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HookHit {
Collision,
Unhookable,
Tele(u8),
}
impl Map {
pub fn dead_reckoning_tick(&self, tee: &mut twsnap::items::Tee) {
tee.tick = tee.tick.advance();
let pos = Vec2::new(tee.pos.x.to_bits() as f32, tee.pos.y.to_bits() as f32);
let vel = Vec2::new(
tee.vel.x.to_bits() as f32 / 256.0,
tee.vel.y.to_bits() as f32 / 256.0,
);
let mut tee_core = TeeCore::with_vel(pos, vel);
let tune_zone = self.tune_zone(pos);
let tuning = self.tuning(tune_zone);
let is_grounded = self.is_grounded(pos);
tee_core.apply_gravity(tuning);
tee_core.set_move_restrictions(self);
tee_core.apply_directions(tuning, is_grounded, tee.direction);
if tee.hook_state == HookState::Grabbed && tee.hooked_player.is_none() {
let hook_pos = Vec2::new(
tee.hook_pos.x.to_bits() as f32,
tee.hook_pos.y.to_bits() as f32,
);
let mut hook_vel = normalize(hook_pos - pos) * tuning.hook_drag_accel;
if hook_vel.y > 0.0 {
hook_vel.y *= 0.3;
}
if hook_vel.x < 0.0 && tee.direction == Direction::Left
|| hook_vel.x > 0.0 && tee.direction == Direction::Right
{
hook_vel.x *= 0.95;
} else {
hook_vel.x *= 0.75;
}
let new_vel = hook_vel + tee_core.vel();
if new_vel.magnitude() < tuning.hook_drag_speed
|| new_vel.magnitude() < tee_core.vel().magnitude()
{
tee_core.set_vel(new_vel); }
}
tee_core.cap_vel();
tee_core.move_vel_ramp(self, tune_zone);
tee_core.round();
let (pos, vel) = tee_core.get_pos_vel();
tee.pos = pos;
tee.vel = vel;
if is_grounded {
tee.jumped.remove(JumpFlags::ALL_AIR_JUMPS_USED);
}
}
fn projectile_pos(
start_pos: Position,
direction: Vec2<f32>, curvature: f32,
speed: f32,
mut time: f32,
) -> Vec2<f32> {
let start_pos = Vec2::new(start_pos.x.to_bits() as f32, start_pos.y.to_bits() as f32);
time *= speed;
let x = start_pos.x + direction.x * time;
let y = start_pos.y + direction.y * time + curvature / 10000.0 * (time * time);
Vec2::new(x, y) / 32.0
}
pub fn projectile_position_at(
&self,
projectile: &twsnap::items::Projectile,
mut time: f32,
) -> Vec2<f32> {
time -= projectile.start_tick.snap_tick() as f32;
time /= 50.0;
let tune_zone = TuneZone(projectile.tune_zone);
let tunings = self.tuning(tune_zone);
let (curvature, speed) = match projectile.kind {
ActiveWeapon::Pistol => (tunings.gun_curvature, tunings.gun_speed),
ActiveWeapon::Grenade => (tunings.grenade_curvature, tunings.grenade_speed),
ActiveWeapon::Shotgun => (tunings.shotgun_curvature, tunings.shotgun_speed),
_ => (0.0, 0.0),
};
let direction = Vec2::new(projectile.direction.x as f32, projectile.direction.y as f32);
let direction = direction.normalized();
Map::projectile_pos(projectile.pos, direction, curvature, speed, time)
}
pub fn map_projectile_position_at(
&self,
projectile: &twsnap::items::MapProjectile,
mut time: f32,
) -> Vec2<f32> {
time -= projectile.start_tick.snap_tick() as f32;
time /= 50.0;
let tune_zone = TuneZone(projectile.tune_zone);
let tunings = self.tuning(tune_zone);
let (curvature, speed) = match projectile.kind {
ActiveWeapon::Pistol => (tunings.gun_curvature, tunings.gun_speed),
ActiveWeapon::Grenade => (tunings.grenade_curvature, tunings.grenade_speed),
ActiveWeapon::Shotgun => (tunings.shotgun_curvature, tunings.shotgun_speed),
_ => (0.0, 0.0),
};
let direction = Vec2::new(projectile.direction.x as f32, projectile.direction.y as f32);
let direction = direction.normalized();
Map::projectile_pos(projectile.pos, direction, curvature, speed, time)
}
}
impl Map {
fn clamp(value: i32, max: usize) -> usize {
value.max(0).min(max as i32 - 1) as usize
}
fn get_game_tile(&self, pos: Vec2<i32>) -> Tile {
self.game_layer[[
Map::clamp(pos.y, self.game_layer.len_of(Axis(0))),
Map::clamp(pos.x, self.game_layer.len_of(Axis(1))),
]]
}
fn get_front_tile(&self, coordinates: Vec2<i32>) -> Tile {
if let Some(front_layer) = &self.front_layer {
front_layer[[
Map::clamp(coordinates.y, front_layer.len_of(Axis(0))),
Map::clamp(coordinates.x, front_layer.len_of(Axis(1))),
]]
} else {
Tile::Air
}
}
pub fn get_tele_tile(&self, coordinates: Vec2<i32>) -> Option<(TeleTile, u8)> {
if let Some(tele_layer) = &self.tele_layer {
tele_layer[[
Map::clamp(coordinates.y, tele_layer.len_of(Axis(0))),
Map::clamp(coordinates.x, tele_layer.len_of(Axis(1))),
]]
} else {
None
}
}
pub(crate) fn game_front(&self, pos: Vec2<i32>) -> GameFrontIterator<'_> {
GameFrontIterator {
cur: 0,
map: self,
x: Map::clamp(pos.x, self.game_layer.len_of(Axis(1))) as u32,
y: Map::clamp(pos.y, self.game_layer.len_of(Axis(0))) as u32,
}
}
pub fn is_tee_in_skippable_radius(&self, pos: Vec2<f32>, tile: Tile) -> bool {
self.get_game_tile(coord::to_int(
pos + Vec2::new(-TEE_PROXIMITY / 3.0, -TEE_PROXIMITY / 3.0),
)) == tile
|| self.get_game_tile(coord::to_int(
pos + Vec2::new(TEE_PROXIMITY / 3.0, -TEE_PROXIMITY / 3.0),
)) == tile
|| self.get_game_tile(coord::to_int(
pos + Vec2::new(-TEE_PROXIMITY / 3.0, TEE_PROXIMITY / 3.0),
)) == tile
|| self.get_game_tile(coord::to_int(
pos + Vec2::new(TEE_PROXIMITY / 3.0, TEE_PROXIMITY / 3.0),
)) == tile
|| self.get_front_tile(coord::to_int(
pos + Vec2::new(-TEE_PROXIMITY / 3.0, -TEE_PROXIMITY / 3.0),
)) == tile
|| self.get_front_tile(coord::to_int(
pos + Vec2::new(TEE_PROXIMITY / 3.0, -TEE_PROXIMITY / 3.0),
)) == tile
|| self.get_front_tile(coord::to_int(
pos + Vec2::new(-TEE_PROXIMITY / 3.0, TEE_PROXIMITY / 3.0),
)) == tile
|| self.get_front_tile(coord::to_int(
pos + Vec2::new(TEE_PROXIMITY / 3.0, TEE_PROXIMITY / 3.0),
)) == tile
}
pub fn is_out_of_map(&self, pos: Vec2<i32>) -> bool {
pos.x < -200
|| pos.x > self.game_layer.len_of(Axis(1)) as i32 + 200
|| pos.y < -200
|| pos.y > self.game_layer.len_of(Axis(0)) as i32 + 200
}
pub fn is_solid(&self, pos: Vec2<i32>) -> bool {
self.get_game_tile(pos).is_solid()
}
pub fn is_grounded(&self, pos: Vec2<f32>) -> bool {
self.is_solid(coord::to_int(
pos + Vec2::new(TEE_PROXIMITY / 2.0, TEE_PROXIMITY / 2.0 + 5.0),
)) || self.is_solid(coord::to_int(
pos + Vec2::new(-TEE_PROXIMITY / 2.0, TEE_PROXIMITY / 2.0 + 5.0),
))
}
pub fn is_solid_tee(&self, pos: Vec2<f32>) -> bool {
self.is_solid(coord::to_int(
pos + Vec2::new(-TEE_PROXIMITY / 2.0, -TEE_PROXIMITY / 2.0),
)) || self.is_solid(coord::to_int(
pos + Vec2::new(TEE_PROXIMITY / 2.0, -TEE_PROXIMITY / 2.0),
)) || self.is_solid(coord::to_int(
pos + Vec2::new(-TEE_PROXIMITY / 2.0, TEE_PROXIMITY / 2.0),
)) || self.is_solid(coord::to_int(
pos + Vec2::new(TEE_PROXIMITY / 2.0, TEE_PROXIMITY / 2.0),
))
}
pub fn tee_move_box(&self, pos: Vec2<f32>, vel: Vec2<f32>) -> (Vec2<f32>, Vec2<f32>) {
let mut pos = pos;
let mut vel = vel;
let distance = vel.magnitude();
let max = distance as i32;
if distance <= 0.00001 {
return (pos, vel);
}
let fraction = 1.0 / (max + 1) as f32;
for _ in 0..=max {
if vel.is_zero() {
return (pos, vel);
}
let mut new_pos = pos + vel * fraction;
if pos == new_pos {
return (pos, vel);
}
if self.is_solid_tee(new_pos) {
let mut hits = 0;
if self.is_solid_tee(Vec2::new(pos.x, new_pos.y)) {
new_pos.y = pos.y;
vel.y = 0.0;
hits += 1;
}
if self.is_solid_tee(Vec2::new(new_pos.x, pos.y)) {
new_pos.x = pos.x;
vel.x = 0.0;
hits += 1;
}
if hits == 0 {
new_pos = pos;
vel = Vec2::zero();
}
}
pos = new_pos;
}
(pos, vel)
}
fn through_offset(from: Vec2<f32>, to: Vec2<f32>) -> Vec2<i32> {
let diff = from - to;
if diff.x.abs() > diff.y.abs() {
if diff.x < 0.0 {
Vec2::new(-1, 0)
} else {
Vec2::new(1, 0)
}
} else if diff.y < 0.0 {
Vec2::new(0, -1)
} else {
Vec2::new(0, 1)
}
}
fn is_hook_through(
&self,
pos: Vec2<i32>,
old_hookthrough_offset: Vec2<i32>,
from: Vec2<f32>,
to: Vec2<f32>,
) -> bool {
let tile = self.get_front_tile(pos);
match tile {
Tile::HookThrough | Tile::HookThroughOnly => return true,
Tile::HookThroughFromDown => {
if from.y > to.y {
return true;
}
}
Tile::HookThroughFromUp => {
if from.y < to.y {
return true;
}
}
Tile::HookThroughFromLeft => {
if from.x < to.x {
return true;
}
}
Tile::HookThroughFromRight => {
if from.x > to.x {
return true;
}
}
_ => {}
}
self.game_front(pos + old_hookthrough_offset)
.any(|t| t == MapTile::Tile(Tile::OldHookThrough))
}
fn is_hook_blocker(&self, pos: Vec2<i32>, from: Vec2<f32>, to: Vec2<f32>) -> bool {
self.game_front(pos).any(|t| {
t == MapTile::Tile(Tile::HookThrough)
|| t == MapTile::Tile(Tile::HookThroughFromDown) && from.y < to.y
|| t == MapTile::Tile(Tile::HookThroughFromUp) && from.y > to.y
|| t == MapTile::Tile(Tile::HookThroughFromLeft) && from.x > to.x
|| t == MapTile::Tile(Tile::HookThroughFromRight) && from.x < to.x
})
}
pub(crate) fn select_tele_out(
&self,
now: Instant,
prng: &mut Prng,
tele_in: u8,
) -> Option<Vec2<f32>> {
if self.tele_outs[tele_in as usize].is_empty() {
return None;
}
let tele_out = prng.random_or_0(now, self.tele_outs[tele_in as usize].len() as u32);
Some(
coord::to_float(self.tele_outs[tele_in as usize][tele_out as usize])
+ Vec2::new(16.0, 16.0),
)
}
pub fn intersect_hook(&self, from: Vec2<f32>, to: &mut Vec2<f32>) -> Option<HookHit> {
let distance = from.distance(*to);
let end = (distance + 1.0) as u32;
let offset = Map::through_offset(from, *to);
for i in 0..=end {
let a = i as f32 / end as f32;
let cur = from + (*to - from) * a;
if let Some((TeleTile::Hook, tele_id)) = self.get_tele_tile(coord::to_int(cur)) {
*to = cur;
return Some(HookHit::Tele(tele_id));
}
let cur_int = coord::to_int(cur);
let game_tile = self.get_game_tile(cur_int);
if game_tile.is_solid() {
if !self.is_hook_through(coord::to_int(cur), offset, from, *to) {
*to = cur;
return Some(if game_tile == Tile::Collision {
HookHit::Collision
} else {
HookHit::Unhookable
});
}
} else if self.is_hook_blocker(cur_int, from, *to) {
*to = cur;
return Some(HookHit::Unhookable);
}
}
None
}
pub fn intersect_projectile(&self, from: Vec2<f32>, to: Vec2<f32>) -> Option<Vec2<f32>> {
let distance = from.distance(to);
let end = (distance + 1.0) as u32;
for i in 0..=end {
let a = i as f32 / end as f32;
let cur = from + (to - from) * a;
let cur_int = coord::to_int(cur);
let game_tile = self.get_game_tile(cur_int);
if game_tile.is_solid() {
return Some(cur);
}
}
None
}
pub fn intersect_laser(
&self,
from: Vec2<f32>,
to: Vec2<f32>,
) -> Option<(Vec2<f32>, Option<u8>)> {
let distance = from.distance(to);
let end = (distance + 1.0) as u32;
let mut last = from;
for i in 0..=end {
let a = i as f32 / end as f32;
let cur = from + (to - from) * a;
let cur_int = coord::to_int(cur);
let game_tile = self.get_game_tile(cur_int);
if let Some((TeleTile::Weapon, tele_id)) = self.get_tele_tile(cur_int) {
return Some((last, Some(tele_id)));
}
if game_tile.is_solid() {
return Some((last, None));
}
last = cur;
}
None
}
pub fn reflect_laser(&self, pos: Vec2<f32>, dir: Vec2<f32>) -> (Vec2<f32>, Vec2<f32>) {
if self.is_solid(coord::to_int(pos + dir)) {
let mut out_vel = dir;
let mut affected = false;
if self.is_solid(coord::to_int(Vec2::new(pos.x + dir.x, pos.y))) {
out_vel.x *= -1.0;
affected = true;
}
if self.is_solid(coord::to_int(Vec2::new(pos.x, pos.y + dir.y))) {
out_vel.y *= -1.0;
affected = true;
}
if !affected {
out_vel *= -1.0;
}
(pos, out_vel)
} else {
(pos + dir, dir)
}
}
pub fn tile_on_line(&self, from: Vec2<f32>, to: Vec2<f32>) -> TilesOnLine<'_> {
let distance = from.distance(to);
TilesOnLine {
map: self,
from,
to,
distance,
last_pos: Vec2::new(-300, -300), i: 0,
end: (distance + 1.0) as i32,
}
}
pub fn get_move_restrictions(&self, pos: Vec2<f32>) -> CantMove {
let directions = [
(Vec2::new(18.0, 0.0), StopperPosition::Right),
(Vec2::new(0.0, 18.0), StopperPosition::Down),
(Vec2::new(-18.0, 0.0), StopperPosition::Left),
(Vec2::new(0.0, -18.0), StopperPosition::Up),
];
let mut cant_move = CantMove::empty();
for tile in self.game_front(coord::to_int(pos)) {
match tile {
MapTile::Tile(Tile::StopDown) => cant_move.insert(CantMove::DOWN),
MapTile::Tile(Tile::StopUp) => cant_move.insert(CantMove::UP),
MapTile::Tile(Tile::StopLeft) => cant_move.insert(CantMove::LEFT),
MapTile::Tile(Tile::StopRight) => cant_move.insert(CantMove::RIGHT),
_ => {}
}
}
for (distance, d) in directions {
for tile in self.game_front(coord::to_int(pos + distance)) {
let add_restriction = match tile {
MapTile::Tile(Tile::StopAll) => CantMove::all(),
MapTile::Tile(Tile::StopLeftRight) => CantMove::LEFT | CantMove::RIGHT,
MapTile::Tile(Tile::StopUpDown) => CantMove::UP | CantMove::DOWN,
MapTile::Tile(Tile::StopDown) => CantMove::DOWN,
MapTile::Tile(Tile::StopUp) => CantMove::UP,
MapTile::Tile(Tile::StopLeft) => CantMove::LEFT,
MapTile::Tile(Tile::StopRight) => CantMove::RIGHT,
_ => continue,
};
cant_move.insert(add_restriction.apply_from_direction(d));
}
}
cant_move
}
}
pub struct TilesOnLine<'a> {
map: &'a Map,
from: Vec2<f32>, to: Vec2<f32>, distance: f32, last_pos: Vec2<i32>, i: i32,
end: i32,
}
impl<'a> Iterator for TilesOnLine<'a> {
type Item = GameFrontIterator<'a>;
fn next(&mut self) -> Option<Self::Item> {
loop {
if self.i >= self.end {
return None;
}
let a: f32 = if self.distance.is_zero() {
0.0
} else {
self.i as f32 / self.distance
};
self.i += 1;
let cur = self.from + (self.to - self.from) * a;
let next_pos = coord::to_int_without_round(cur);
if next_pos != self.last_pos {
self.last_pos = next_pos;
return Some(GameFrontIterator {
map: self.map,
cur: 0,
x: Map::clamp(next_pos.x, self.map.game_layer.len_of(Axis(1))) as u32,
y: Map::clamp(next_pos.y, self.map.game_layer.len_of(Axis(0))) as u32,
});
}
}
}
}
enum Rotation {
Rotation0,
Rotation90,
Rotation180,
Rotation270,
}
impl Rotation {
fn from_flags(tile_flags: TileFlags) -> Option<Rotation> {
if tile_flags == TileFlags::empty() {
Some(Rotation::Rotation0)
} else if tile_flags == TileFlags::ROTATE {
Some(Rotation::Rotation90)
} else if tile_flags == TileFlags::FLIP_X | TileFlags::FLIP_Y {
Some(Rotation::Rotation180)
} else if tile_flags == TileFlags::FLIP_X | TileFlags::FLIP_Y | TileFlags::ROTATE {
Some(Rotation::Rotation270)
} else {
None
}
}
}
bitflags! {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct TileConfig: u8 {
const OLD_SHUTGUN = 0b0_0001;
const COLLISION_OFF = 0b0_0010;
const ENDLESS_HOOK = 0b0_0100;
const WEAPON_HIT_OFF = 0b0_1000;
const HOOKTHROUGH_TEES = 0b1_0000;
}
}
impl Map {
fn twmap_parse_game_layer(
layer: &Array2<GameTile>,
front_layer: bool,
) -> (Array2<Tile>, TileConfig) {
let mut tile_config = TileConfig::empty();
(
layer.map(|tile| match (tile.id, front_layer) {
(0, _) => Tile::Air,
(1, false) => Tile::Collision,
(3, false) => Tile::Unhookable,
(2, _) => Tile::Kill,
(9, _) => Tile::Freeze,
(11, _) => Tile::Unfreeze,
(12, _) => Tile::DeepFreezeEnable,
(13, _) => Tile::DeepFreezeDisable,
(17, _) => Tile::EnableEndlessHook,
(18, _) => Tile::DisableEndlessHook,
(105, _) => Tile::EnableInfiniteJumps,
(89, _) => Tile::DisableInfiniteJumps,
(19, _) => Tile::EnableWeaponHit,
(20, _) => Tile::DisableWeaponHit,
(21, _) => Tile::EnableSolo,
(22, _) => Tile::DisableSolo,
(104, _) => Tile::EnableTeeCollision,
(88, _) => Tile::DisableTeeCollision,
(107, _) => Tile::EnableTeeHook,
(91, _) => Tile::DisableTeeHook,
(106, _) => Tile::EnableJetpack,
(90, _) => Tile::DisableJetpack,
(96, _) => Tile::EnableTelegunGun,
(97, _) => Tile::DisableTelegunGun,
(128, _) => Tile::EnableTelegunLaser,
(129, _) => Tile::DisableTelegunLaser,
(112, _) => Tile::EnableTelegunGrenade,
(113, _) => Tile::DisableTelegunGrenade,
(144, _) => Tile::LiveFreezeEnable,
(145, _) => Tile::LiveFreezeDisable,
(98, true) => Tile::EvilGunTeleporter,
(99, true) => Tile::GunTeleporter,
(16, _) => Tile::Walljump,
(32, _) => Tile::DoubleJumpRefresher,
(76, _) => Tile::UnlockTeam,
(33, _) => Tile::StartLine,
(34, _) => Tile::FinishLine,
(6, _) => Tile::OldHookThrough,
(5, true) => Tile::HookThroughOnly,
(66, _) => Tile::HookThrough,
(67, _) => match Rotation::from_flags(tile.flags) {
None => Tile::Air,
Some(Rotation::Rotation0) => Tile::HookThroughFromDown,
Some(Rotation::Rotation90) => Tile::HookThroughFromLeft,
Some(Rotation::Rotation180) => Tile::HookThroughFromUp,
Some(Rotation::Rotation270) => Tile::HookThroughFromRight,
},
(60, _) => match Rotation::from_flags(tile.flags) {
None => Tile::Air,
Some(Rotation::Rotation0) => Tile::StopDown,
Some(Rotation::Rotation90) => Tile::StopLeft,
Some(Rotation::Rotation180) => Tile::StopUp,
Some(Rotation::Rotation270) => Tile::StopRight,
},
(61, _) => match Rotation::from_flags(tile.flags) {
None => Tile::Air,
Some(Rotation::Rotation0) => Tile::StopUpDown,
Some(Rotation::Rotation90) => Tile::StopLeftRight,
Some(Rotation::Rotation180) => Tile::StopUpDown,
Some(Rotation::Rotation270) => Tile::StopLeftRight,
},
(62, _) => Tile::StopAll,
(71, _) => {
tile_config.insert(TileConfig::OLD_SHUTGUN);
Tile::Air
}
(72, _) => {
tile_config.insert(TileConfig::COLLISION_OFF);
Tile::Air
}
(73, _) => {
tile_config.insert(TileConfig::ENDLESS_HOOK);
Tile::Air
}
(74, _) => {
tile_config.insert(TileConfig::WEAPON_HIT_OFF);
Tile::Air
}
(75, _) => {
tile_config.insert(TileConfig::HOOKTHROUGH_TEES);
Tile::Air
}
(_, _) => Tile::Air,
}),
tile_config,
)
}
fn find_tiles(
layer: &Array2<GameTile>,
front_layer: Option<&Array2<GameTile>>,
tile: u8,
num_tiles: usize,
) -> Vec<Vec2<i32>> {
let mut tiles = Vec::new();
if let Some(front_layer) = front_layer {
for (((y, x), g), f) in layer.indexed_iter().zip(front_layer.iter()) {
if tiles.len() == num_tiles {
break;
}
if g.id == tile {
tiles.push(Vec2::new(x as i32, y as i32));
}
if tiles.len() == num_tiles {
break;
}
if f.id == tile {
tiles.push(Vec2::new(x as i32, y as i32));
}
}
} else {
for ((y, x), t) in layer.indexed_iter() {
if tiles.len() == num_tiles {
break;
}
if t.id == tile {
tiles.push(Vec2::new(x as i32, y as i32));
}
}
}
tiles
}
fn add_entities(
layer: &Array2<GameTile>,
entities: &mut Vec<MapEntityItem>,
snap_id_generator: &mut MapSnapIdGenerator,
) {
for ((y, x), t) in layer.indexed_iter() {
let pos = || coord::to_float(Vec2::new(x as i32, y as i32)) + Vec2::new(16.0, 16.0);
use twgame_core::twsnap::enums::Powerup as PickupKind;
if t.id == 197 {
entities.push(MapEntityItem::pickup(Pickup::new(
snap_id_generator.next_pickup(),
pos(),
PickupKind::Armor,
)))
} else if t.id == 198 {
entities.push(MapEntityItem::pickup(Pickup::new(
snap_id_generator.next_pickup(),
pos(),
PickupKind::Health,
)))
} else if t.id == 199 {
entities.push(MapEntityItem::pickup(Pickup::new(
snap_id_generator.next_pickup(),
pos(),
PickupKind::Weapon(CollectableWeapon::Shotgun),
)))
} else if t.id == 200 {
entities.push(MapEntityItem::pickup(Pickup::new(
snap_id_generator.next_pickup(),
pos(),
PickupKind::Weapon(CollectableWeapon::Grenade),
)))
} else if t.id == 201 {
entities.push(MapEntityItem::pickup(Pickup::new(
snap_id_generator.next_pickup(),
pos(),
PickupKind::Weapon(CollectableWeapon::Ninja),
)))
} else if t.id == 202 {
entities.push(MapEntityItem::pickup(Pickup::new(
snap_id_generator.next_pickup(),
pos(),
PickupKind::Weapon(CollectableWeapon::Rifle),
)))
} else if t.id == 226 {
entities.push(MapEntityItem::pickup(Pickup::new(
snap_id_generator.next_pickup(),
pos(),
PickupKind::Shield(CollectableWeapon::Shotgun),
)))
} else if t.id == 227 {
entities.push(MapEntityItem::pickup(Pickup::new(
snap_id_generator.next_pickup(),
pos(),
PickupKind::Shield(CollectableWeapon::Grenade),
)))
} else if t.id == 228 {
entities.push(MapEntityItem::pickup(Pickup::new(
snap_id_generator.next_pickup(),
pos(),
PickupKind::Shield(CollectableWeapon::Ninja),
)))
} else if t.id == 229 {
entities.push(MapEntityItem::pickup(Pickup::new(
snap_id_generator.next_pickup(),
pos(),
PickupKind::Shield(CollectableWeapon::Rifle),
)))
}
}
}
fn twmap_parse_switch(switch_layer: &SwitchLayer) -> Array2<Option<SwitchTile>> {
switch_layer.tiles.unwrap_ref().map(|tile| match tile.id {
7 => Some(SwitchTile::JumpCountSetter(tile.delay)),
19 => match tile.delay {
0 => Some(SwitchTile::WeaponsOff),
1 => Some(SwitchTile::SpecificWeaponOff(DisableableWeapon::Hammer)),
3 => Some(SwitchTile::SpecificWeaponOff(DisableableWeapon::Shotgun)),
4 => Some(SwitchTile::SpecificWeaponOff(DisableableWeapon::Grenade)),
5 => Some(SwitchTile::SpecificWeaponOff(DisableableWeapon::Rifle)),
_ => None,
},
20 => match tile.delay {
0 => Some(SwitchTile::WeaponsOn),
1 => Some(SwitchTile::SpecificWeaponOn(DisableableWeapon::Hammer)),
3 => Some(SwitchTile::SpecificWeaponOn(DisableableWeapon::Shotgun)),
4 => Some(SwitchTile::SpecificWeaponOn(DisableableWeapon::Grenade)),
5 => Some(SwitchTile::SpecificWeaponOn(DisableableWeapon::Rifle)),
_ => None,
},
79 => Some(SwitchTile::TimePenalty(tile.delay)),
95 => Some(SwitchTile::TimeBonus(tile.delay)),
98 => match tile.number {
1 => Some(SwitchTile::TelegunEvil(TelegunWeapon::Gun)),
4 => Some(SwitchTile::TelegunEvil(TelegunWeapon::Grenade)),
5 => Some(SwitchTile::TelegunEvil(TelegunWeapon::Rifle)),
_ => Some(SwitchTile::TelegunEvil(TelegunWeapon::All)),
},
99 => match tile.number {
1 => Some(SwitchTile::Telegun(TelegunWeapon::Gun)),
4 => Some(SwitchTile::Telegun(TelegunWeapon::Grenade)),
5 => Some(SwitchTile::Telegun(TelegunWeapon::Rifle)),
_ => Some(SwitchTile::Telegun(TelegunWeapon::All)),
},
22 => Some(SwitchTile::ActivateTime(tile.number, tile.delay)),
23 => Some(SwitchTile::DeactivateTime(tile.number, tile.delay)),
24 => Some(SwitchTile::Activate(tile.number)),
25 => Some(SwitchTile::Deactivate(tile.number)),
9 => Some(SwitchTile::Freeze(tile.number, tile.delay)),
12 => Some(SwitchTile::Deep(tile.number)),
13 => Some(SwitchTile::Undeep(tile.number)),
144 => Some(SwitchTile::LiveFreeze(tile.number)),
145 => Some(SwitchTile::LiveUnfreeze(tile.number)),
_ => None,
})
}
#[allow(clippy::type_complexity)]
fn twmap_parse_tele_out(
tele_layer: Option<&TeleLayer>,
) -> ([Vec<Vec2<i32>>; 256], [Vec<Vec2<i32>>; 256]) {
let mut tele_outs: [Vec<Vec2<i32>>; 256] = array::from_fn(|_| vec![]);
let mut tele_checkpoint_outs: [Vec<Vec2<i32>>; 256] = array::from_fn(|_| vec![]);
if let Some(tele_layer) = tele_layer {
for ((y, x), tile) in tele_layer.tiles.unwrap_ref().indexed_iter() {
match tile.id {
27 => tele_outs[tile.number as usize].push(Vec2::new(x as i32, y as i32)),
30 => tele_checkpoint_outs[tile.number as usize]
.push(Vec2::new(x as i32, y as i32)),
_ => {}
}
}
}
(tele_outs, tele_checkpoint_outs)
}
fn twmap_parse_tele_in(
tele_layer: &TeleLayer,
tele_outs: &[Vec<Vec2<i32>>; 256],
tele_checkpoint_outs: &[Vec<Vec2<i32>>; 256],
) -> Array2<Option<(TeleTile, u8)>> {
tele_layer.tiles.unwrap_ref().map(|tile| match tile.id {
10 => {
if !tele_outs[tile.number as usize].is_empty() {
Some((TeleTile::EvilTee, tile.number))
} else {
None
}
}
14 => {
if !tele_outs[tile.number as usize].is_empty() {
Some((TeleTile::Weapon, tile.number))
} else {
None
}
}
15 => Some((TeleTile::Hook, tile.number)),
26 => {
if !tele_outs[tile.number as usize].is_empty() {
Some((TeleTile::Tee, tile.number))
} else {
None
}
}
29 => {
if !tele_checkpoint_outs[tile.number as usize].is_empty() {
}
Some((TeleTile::Checkpoint, tile.number))
}
31 => Some((TeleTile::TeeCheckpointOut, 0)),
63 => Some((TeleTile::EvilTeeCheckpointOut, 0)),
_ => None,
})
}
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct TuneZone(u8);
impl TuneZone {
pub(crate) fn to_save(self) -> u8 {
self.0
}
pub(crate) fn from_save(zone: u8) -> Self {
Self(zone)
}
}
impl Map {
pub(crate) fn tune_zone(&self, pos: Vec2<f32>) -> TuneZone {
self.tune_layer
.as_ref()
.map(|t| {
let pos = coord::to_int(pos);
TuneZone(
t[[
Map::clamp(pos.y, t.len_of(Axis(0))),
Map::clamp(pos.x, t.len_of(Axis(1))),
]],
)
})
.unwrap_or(TuneZone(0))
}
pub(crate) fn tuning(&self, tune_zone: TuneZone) -> &Tuning {
&self.tune_zones[tune_zone.0 as usize]
}
}
impl TryFrom<&mut TwMap> for Map {
type Error = String;
fn try_from(map: &mut TwMap) -> Result<Self, Self::Error> {
map.groups.load().map_err(|err| err.to_string())?;
let game_layer = map
.find_physics_layer::<GameLayer>()
.unwrap()
.tiles
.unwrap_ref();
let front_layer = map
.find_physics_layer::<FrontLayer>()
.map(|l| l.tiles.unwrap_ref());
let spawn_points_normal = Map::find_tiles(game_layer, front_layer, 192, 64);
let spawn_points_red = Map::find_tiles(game_layer, front_layer, 193, 64);
let spawn_points_blue = Map::find_tiles(game_layer, front_layer, 194, 64);
let mut entities = vec![];
let mut snap_id_generator = MapSnapIdGenerator::new();
Map::add_entities(game_layer, &mut entities, &mut snap_id_generator);
if let Some(front_layer) = front_layer {
Map::add_entities(front_layer, &mut entities, &mut snap_id_generator);
}
let tune_layer = map
.find_physics_layer::<TuneLayer>()
.map(|l| l.tiles.unwrap_ref());
let tune_layer = tune_layer.map(|l| l.map(|t| t.number));
let mut tune_zones = [Tuning::new_with_ddnet_parameters(); 256];
for setting in map.info.settings.iter() {
let mut setting = setting.split_ascii_whitespace();
let Some(command) = setting.next() else {
continue;
};
if command == "tune" {
let Some(tune) = setting.next() else {
continue;
};
let Some(value) = setting.next() else {
continue;
};
let Ok(value) = value.parse() else {
continue;
};
if !tune_zones[0].apply_from_config(tune, value) {
println!("unknown tune {tune} {value}")
}
} else if command == "tune_zone" {
let Some(zone) = setting.next() else {
continue;
};
let Ok(zone) = zone.parse() else {
continue;
};
if zone == 0 || zone >= tune_zones.len() {
continue;
}
let zone: usize = zone; let Some(tune) = setting.next() else {
continue;
};
let Some(value) = setting.next() else {
continue;
};
let Ok(value) = value.parse() else {
continue;
};
if !tune_zones[zone].apply_from_config(tune, value) {
println!("unknown tune {tune} {value}")
}
}
}
let (game_layer, mut game_config) = Map::twmap_parse_game_layer(game_layer, false);
let front_layer = if let Some((front_layer, front_config)) =
front_layer.map(|layer| Map::twmap_parse_game_layer(layer, true))
{
game_config = game_config.union(front_config);
Some(front_layer)
} else {
None
};
if game_config.contains(TileConfig::COLLISION_OFF) {
tune_zones[0].player_collision = 0.0;
}
if game_config.contains(TileConfig::HOOKTHROUGH_TEES) {
tune_zones[0].player_hooking = 0.0;
}
let switch_layer = map
.find_physics_layer::<SwitchLayer>()
.map(Map::twmap_parse_switch);
let tele_layer = map.find_physics_layer::<TeleLayer>();
let (tele_outs, tele_checkpoint_outs) = Map::twmap_parse_tele_out(tele_layer);
let tele = tele_layer.map(|tele_layer| {
Map::twmap_parse_tele_in(tele_layer, &tele_outs, &tele_checkpoint_outs)
});
Ok(Self {
game_layer,
front_layer,
switch_layer,
tele_layer: tele,
tele_outs,
tele_checkpoint_outs,
tune_layer,
entities,
spawn_points: [spawn_points_normal, spawn_points_red, spawn_points_blue],
tune_zones,
snap_id_generator,
})
}
}
#[cfg(test)]
mod test {
use super::*;
fn ddnet_map_indices<'a>(
map: &'a Map,
prev_pos: Vec2<f32>,
pos: Vec2<f32>,
) -> Vec<GameFrontIterator<'a>> {
let mut indices = vec![];
let d = prev_pos.distance(pos);
let end = (d + 1.0) as i32;
if d == 0.0 {
let pos = coord::to_int_without_round(pos);
indices.push(map.game_front(pos));
} else {
let mut last_index = Vec2::new(-300, -300);
for i in 0..end {
let a = i as f32 / d;
let tmp = prev_pos + (pos - prev_pos) * a;
let next = coord::to_int_without_round(tmp);
if last_index != next {
indices.push(map.game_front(next));
last_index = next;
}
}
}
indices
}
fn twgame_indices_iter(map: &Map, prev_pos: Vec2<f32>, pos: Vec2<f32>) -> Vec<Vec2<i32>> {
map.tile_on_line(prev_pos, pos)
.map(|e| Vec2::new(e.y as i32, e.x as i32))
.collect()
}
fn ddnet_indices_iter(map: &Map, prev_pos: Vec2<f32>, pos: Vec2<f32>) -> Vec<Vec2<i32>> {
ddnet_map_indices(map, prev_pos, pos)
.iter()
.map(|e| Vec2::new(e.y as i32, e.x as i32))
.collect()
}
#[test]
fn stronghold_error_1() {
let map = &Map {
game_layer: Array2::from_elem((300, 300), Tile::Air),
front_layer: None,
switch_layer: None,
tele_layer: None,
tele_outs: array::from_fn(|_| vec![]),
tele_checkpoint_outs: array::from_fn(|_| vec![]),
tune_layer: None,
entities: vec![],
spawn_points: [vec![], vec![], vec![]],
tune_zones: [Tuning::new_with_ddnet_parameters(); 256],
snap_id_generator: MapSnapIdGenerator::new(),
};
let from = Vec2::new(8836.0, 1952.0);
let to = Vec2::new(8835.0, 1956.0);
assert_eq!(
ddnet_indices_iter(map, from, to),
twgame_indices_iter(map, from, to)
);
}
#[test]
fn stronghold_error_2() {
let map = &Map {
game_layer: Array2::from_elem((400, 400), Tile::Air),
front_layer: None,
switch_layer: None,
tele_layer: None,
tele_outs: array::from_fn(|_| vec![]),
tele_checkpoint_outs: array::from_fn(|_| vec![]),
tune_layer: None,
entities: vec![],
spawn_points: [vec![], vec![], vec![]],
tune_zones: [Tuning::new_with_ddnet_parameters(); 256],
snap_id_generator: MapSnapIdGenerator::new(),
};
let from = Vec2::new(7466.0, 10349.0);
let to = Vec2::new(7471.0, 10353.0);
assert_eq!(
ddnet_indices_iter(map, from, to),
twgame_indices_iter(map, from, to)
);
}
#[test]
fn stronghold_error_3() {
let map = &Map {
game_layer: Array2::from_elem((400, 400), Tile::Air),
front_layer: None,
switch_layer: None,
tele_layer: None,
tele_outs: array::from_fn(|_| vec![]),
tele_checkpoint_outs: array::from_fn(|_| vec![]),
tune_layer: None,
entities: vec![],
spawn_points: [vec![], vec![], vec![]],
tune_zones: [Tuning::new_with_ddnet_parameters(); 256],
snap_id_generator: MapSnapIdGenerator::new(),
};
let from = Vec2::new(7466.0, 10349.0);
let to = Vec2::new(7471.0, 10353.0);
assert_eq!(
ddnet_indices_iter(map, from, to),
twgame_indices_iter(map, from, to)
);
}
}