├── .gitignore ├── core ├── src │ ├── types.rs │ ├── options.rs │ ├── object.rs │ ├── geom.rs │ ├── misc.rs │ ├── attack.rs │ ├── unit.rs │ ├── fov.rs │ ├── player.rs │ ├── dir.rs │ ├── print_info.rs │ ├── sector.rs │ ├── event.rs │ ├── fow.rs │ ├── map.rs │ ├── position.rs │ ├── ai.rs │ ├── movement.rs │ ├── filter.rs │ └── db.rs └── Cargo.toml ├── src ├── screen.rs ├── types.rs ├── pipeline.rs ├── texture.rs ├── main.rs ├── fs.rs ├── move_helper.rs ├── pick.rs ├── text.rs ├── mesh.rs ├── unit_type_visual_info.rs ├── end_turn_screen.rs ├── selection.rs ├── player_info.rs ├── game_results_screen.rs ├── camera.rs ├── geom.rs ├── visualizer.rs ├── gui.rs ├── map_text.rs ├── obj.rs ├── scene.rs ├── reinforcements_popup.rs ├── mesh_manager.rs ├── main_menu_screen.rs ├── gen.rs └── context.rs ├── Makefile ├── .travis.yml ├── Cargo.toml ├── LICENSE-MIT ├── appveyor.yml ├── README.rst └── LICENSE-APACHE /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | assets 3 | -------------------------------------------------------------------------------- /core/src/types.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Copy, Debug)] 2 | pub struct Size2 { 3 | pub w: i32, 4 | pub h: i32, 5 | } 6 | -------------------------------------------------------------------------------- /core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | 3 | name = "core" 4 | version = "0.0.1" 5 | authors = ["ozkriff "] 6 | 7 | [lib] 8 | name = "core" 9 | doctest = false 10 | 11 | [dependencies] 12 | cgmath = "0.12" 13 | rand = "0.3" 14 | -------------------------------------------------------------------------------- /core/src/options.rs: -------------------------------------------------------------------------------- 1 | #[derive(PartialEq, Eq, Clone, Copy, Debug)] 2 | pub enum GameType { 3 | Hotseat, 4 | SingleVsAi, 5 | } 6 | 7 | impl Default for GameType { 8 | fn default() -> GameType { 9 | GameType::Hotseat 10 | } 11 | } 12 | 13 | #[derive(Clone, Debug)] 14 | pub struct Options { 15 | pub game_type: GameType, 16 | pub map_name: String, 17 | pub players_count: i32, // TODO: must it be defined by map/scenario? 18 | } 19 | -------------------------------------------------------------------------------- /src/screen.rs: -------------------------------------------------------------------------------- 1 | use glutin::{WindowEvent}; 2 | use context::{Context}; 3 | use types::{Time}; 4 | 5 | pub enum ScreenCommand { 6 | PopScreen, 7 | PopPopup, 8 | PushScreen(Box), 9 | PushPopup(Box), 10 | } 11 | 12 | #[derive(Clone, Copy, PartialEq, Debug)] 13 | pub enum EventStatus { 14 | Handled, 15 | NotHandled, 16 | } 17 | 18 | pub trait Screen { 19 | fn tick(&mut self, context: &mut Context, dtime: Time); 20 | fn handle_event(&mut self, context: &mut Context, event: &WindowEvent) -> EventStatus; 21 | } 22 | -------------------------------------------------------------------------------- /core/src/object.rs: -------------------------------------------------------------------------------- 1 | use position::{ExactPos}; 2 | use player::{PlayerId}; 3 | 4 | #[derive(Debug, PartialEq, Clone, Copy)] 5 | pub enum ObjectClass { 6 | Building, 7 | Road, 8 | Smoke, 9 | ReinforcementSector, 10 | } 11 | 12 | #[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Hash, Clone, Copy)] 13 | pub struct ObjectId { 14 | pub id: i32, 15 | } 16 | 17 | #[derive(Debug, Clone)] 18 | pub struct Object { 19 | pub pos: ExactPos, 20 | pub class: ObjectClass, 21 | pub timer: Option, 22 | pub owner_id: Option, 23 | } 24 | -------------------------------------------------------------------------------- /core/src/geom.rs: -------------------------------------------------------------------------------- 1 | use cgmath::{Vector2}; 2 | use position::{MapPos}; 3 | 4 | pub const HEX_EX_RADIUS: f32 = 1.4; 5 | 6 | // (pow(1.0, 2) - pow(0.5, 2)).sqrt() 7 | pub const HEX_IN_RADIUS: f32 = 0.866025403784 * HEX_EX_RADIUS; 8 | 9 | pub fn map_pos_to_world_pos(i: MapPos) -> Vector2 { 10 | let v = Vector2 { 11 | x: (i.v.x as f32) * HEX_IN_RADIUS * 2.0, 12 | y: (i.v.y as f32) * HEX_EX_RADIUS * 1.5, 13 | }; 14 | if i.v.y % 2 == 0 { 15 | Vector2{x: v.x + HEX_IN_RADIUS, y: v.y} 16 | } else { 17 | v 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | use cgmath::{Vector3, Vector2}; 2 | 3 | pub use core::types::{Size2}; 4 | 5 | #[derive(Copy, Clone, Debug)] 6 | pub struct WorldPos{pub v: Vector3} 7 | 8 | #[derive(Copy, Clone, Debug)] 9 | pub struct VertexCoord{pub v: Vector3} 10 | 11 | #[derive(Copy, Clone, Debug)] 12 | pub struct ScreenPos{pub v: Vector2} 13 | 14 | #[derive(Copy, Clone, Debug)] 15 | pub struct Time{pub n: f32} 16 | 17 | #[derive(Copy, Clone, Debug)] 18 | pub struct Speed{pub n: f32} 19 | 20 | #[derive(Copy, Clone, Debug)] 21 | pub struct WorldDistance{pub n: f32} 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CARGO_FLAGS += --release 2 | # CARGO_FLAGS += --verbose 3 | 4 | zoc: assets 5 | cargo build $(CARGO_FLAGS) 6 | 7 | test: 8 | cargo test --package core $(CARGO_FLAGS) 9 | cargo test $(CARGO_FLAGS) 10 | 11 | run: assets 12 | RUST_BACKTRACE=1 cargo run $(CARGO_FLAGS) 13 | 14 | assets: 15 | git clone --depth=1 https://github.com/ozkriff/zoc_assets assets 16 | 17 | APK = ./target/android-artifacts/build/bin/zoc-debug.apk 18 | 19 | android: assets 20 | cargo apk 21 | 22 | android_run: android 23 | adb install -r $(APK) 24 | adb logcat -c 25 | adb shell am start -n rust.zoc/rust.zoc.MainActivity 26 | adb logcat -v time | grep 'Rust\|DEBUG' 27 | 28 | .PHONY: zoc run android android_run test 29 | -------------------------------------------------------------------------------- /src/pipeline.rs: -------------------------------------------------------------------------------- 1 | use gfx; 2 | use gfx::state::ColorMask; 3 | 4 | pub type ColorFormat = gfx::format::Srgba8; 5 | pub type DepthFormat = gfx::format::DepthStencil; 6 | 7 | gfx_defines! { 8 | vertex Vertex { 9 | pos: [f32; 3] = "a_Pos", 10 | uv: [f32; 2] = "a_Uv", 11 | } 12 | 13 | pipeline pipe { 14 | basic_color: gfx::Global<[f32; 4]> = "u_Basic_color", 15 | mvp: gfx::Global<[[f32; 4]; 4]> = "u_ModelViewProj", 16 | vbuf: gfx::VertexBuffer = (), 17 | texture: gfx::TextureSampler<[f32; 4]> = "t_Tex", 18 | out: gfx::BlendTarget = ("Target0", ColorMask::all(), gfx::preset::blend::ALPHA), 19 | out_depth: gfx::DepthTarget = gfx::preset::depth::LESS_EQUAL_WRITE, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | 3 | env: 4 | - zipname="zoc-$TRAVIS_OS_NAME.tar.gz" 5 | 6 | cache: cargo 7 | 8 | rust: 9 | - nightly 10 | - beta 11 | - stable 12 | 13 | matrix: 14 | allow_failures: 15 | - rust: nightly 16 | 17 | os: 18 | - linux 19 | - osx 20 | 21 | sudo: false 22 | 23 | script: 24 | - make && make test 25 | 26 | before_deploy: 27 | - mv target/release/zoc . 28 | - tar -zcvf $zipname assets zoc 29 | 30 | deploy: 31 | provider: releases 32 | skip_cleanup: true 33 | overwrite: true 34 | file: $zipname 35 | api_key: 36 | secure: IomsNtPkLF5KULhYX5Shgjpt14BSF+pY2aQOkk5c1BeMftnHXQBZV5Kt6FdZYB6ASFJGfOWqmkYy+pi81aN+hzzk0PXYDtK0bbpucbovoao9GJhorU3wEeBajloICkQ3PEjSJ5T+dtAmQLGKgx4YTTUyJSEsdJTULP5AIn5dNPU= 37 | on: 38 | tags: true 39 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | 3 | name = "zoc" 4 | version = "0.0.1" 5 | authors = ["ozkriff "] 6 | description = "ZoC is turn-based hexagonal strategy game written in Rust" 7 | readme = "README.md" 8 | license = "MIT OR Apache-2.0" 9 | keywords = ["opengl", "3D", "game", "gfx"] 10 | repository = "https://github.com/ozkriff/zoc" 11 | 12 | [package.metadata.android] 13 | assets = "assets" 14 | 15 | [dependencies.core] 16 | path = "core" 17 | 18 | [dependencies] 19 | gfx_core = "0.9" 20 | gfx_device_gl = "0.16" 21 | gfx_window_glutin = "0.30" 22 | gfx = "0.18" 23 | glutin = "0.20" 24 | collision = "0.9" 25 | cgmath = "0.12" 26 | rand = "0.3" 27 | rusttype = "0.2" 28 | 29 | [dependencies.image] 30 | version = "0.10" 31 | default-features = false 32 | features = ["png_codec"] 33 | 34 | [target.arm-linux-androideabi.dependencies] 35 | android_glue = "0.2.0" 36 | -------------------------------------------------------------------------------- /src/texture.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Cursor}; 2 | use image; 3 | use gfx::handle::{ShaderResourceView}; 4 | use gfx::{self, texture}; 5 | use gfx_gl; 6 | use types::{Size2}; 7 | use pipeline::{ColorFormat}; 8 | use context::{Context}; 9 | 10 | pub type Texture = gfx::handle::ShaderResourceView; 11 | 12 | pub fn load_texture(context: &mut Context, data: &[u8]) -> ShaderResourceView { 13 | let img = image::load(Cursor::new(data), image::PNG).unwrap().to_rgba(); 14 | let (w, h) = img.dimensions(); 15 | let size = Size2{w: w as i32, h: h as i32}; 16 | load_texture_raw(context.factory_mut(), size, &img.into_vec()) 17 | } 18 | 19 | pub fn load_texture_raw(factory: &mut F, size: Size2, data: &[u8]) -> ShaderResourceView 20 | where R: gfx::Resources, F: gfx::Factory 21 | { 22 | let kind = texture::Kind::D2(size.w as texture::Size, size.h as texture::Size, texture::AaMode::Single); 23 | let mipmap = gfx::texture::Mipmap::Provided; 24 | let (_, view) = factory.create_texture_immutable_u8::(kind, mipmap, &[data]).unwrap(); 25 | view 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT/X Consortium License 2 | 3 | @ 2013-2016 Andrey Lesnikov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | DEALINGS IN THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_os = "android")] 2 | #[macro_use] 3 | extern crate android_glue; 4 | 5 | #[macro_use] 6 | extern crate gfx; 7 | 8 | extern crate gfx_window_glutin as gfx_glutin; 9 | extern crate gfx_device_gl as gfx_gl; 10 | extern crate rand; 11 | extern crate cgmath; 12 | extern crate collision; 13 | extern crate glutin; 14 | extern crate core; 15 | extern crate image; 16 | extern crate rusttype; 17 | 18 | mod visualizer; 19 | mod pick; 20 | mod gen; 21 | mod gui; 22 | mod obj; 23 | mod scene; 24 | mod event_visualizer; 25 | mod unit_type_visual_info; 26 | mod mesh_manager; 27 | mod player_info; 28 | mod selection; 29 | mod types; 30 | mod pipeline; 31 | mod map_text; 32 | mod move_helper; 33 | mod camera; 34 | mod geom; 35 | mod screen; 36 | mod texture; 37 | mod tactical_screen; 38 | mod context_menu_popup; 39 | mod reinforcements_popup; 40 | mod main_menu_screen; 41 | mod end_turn_screen; 42 | mod game_results_screen; 43 | mod context; 44 | mod text; 45 | mod mesh; 46 | mod fs; 47 | 48 | use visualizer::{Visualizer}; 49 | 50 | pub fn main() { 51 | std::env::set_var("RUST_BACKTRACE", "1"); 52 | let mut visualizer = Visualizer::new(); 53 | while visualizer.is_running() { 54 | visualizer.tick(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/fs.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path}; 2 | use std::io::{Cursor}; 3 | 4 | pub fn load_as_string>(path: P) -> String { 5 | String::from_utf8(load(path).into_inner()).unwrap() 6 | } 7 | 8 | #[cfg(not(target_os = "android"))] 9 | pub fn load>(path: P) -> Cursor> { 10 | use std::fs::{File}; 11 | use std::io::{Read}; 12 | 13 | let mut buf = Vec::new(); 14 | let fullpath = &Path::new("assets").join(&path); 15 | let mut file = match File::open(&fullpath) { 16 | Ok(file) => file, 17 | Err(err) => { 18 | panic!("Can`t open file '{}' ({})", fullpath.display(), err); 19 | }, 20 | }; 21 | match file.read_to_end(&mut buf) { 22 | Ok(_) => Cursor::new(buf), 23 | Err(err) => { 24 | panic!("Can`t read file '{}' ({})", fullpath.display(), err); 25 | }, 26 | } 27 | } 28 | 29 | #[cfg(target_os = "android")] 30 | pub fn load>(path: P) -> Cursor> { 31 | use android_glue; 32 | 33 | let filename = path.as_ref().to_str() 34 | .expect("Can`t convert Path to &str"); 35 | match android_glue::load_asset(filename) { 36 | Ok(buf) => Cursor::new(buf), 37 | // TODO: more info about error 38 | Err(_) => panic!("Can`t load asset '{}'", filename), 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/move_helper.rs: -------------------------------------------------------------------------------- 1 | use cgmath::{Vector3, InnerSpace}; 2 | use geom; 3 | use types::{WorldPos, Time, Speed, WorldDistance}; 4 | 5 | #[derive(Clone, Debug)] 6 | pub struct MoveHelper { 7 | to: WorldPos, 8 | current: WorldPos, 9 | dist: WorldDistance, 10 | current_dist: WorldDistance, 11 | dir: Vector3, 12 | } 13 | 14 | impl MoveHelper { 15 | pub fn new(from: WorldPos, to: WorldPos, speed: Speed) -> MoveHelper { 16 | let dir = (to.v - from.v).normalize(); 17 | let dist = geom::dist(from, to); 18 | MoveHelper { 19 | to: to, 20 | current: from, 21 | dist: dist, 22 | current_dist: WorldDistance{n: 0.0}, 23 | dir: dir * speed.n, 24 | } 25 | } 26 | 27 | pub fn progress(&self) -> f32 { 28 | self.current_dist.n / self.dist.n 29 | } 30 | 31 | pub fn is_finished(&self) -> bool { 32 | self.current_dist.n >= self.dist.n 33 | } 34 | 35 | pub fn step(&mut self, dtime: Time) -> WorldPos { 36 | let _ = self.step_diff(dtime); 37 | self.current 38 | } 39 | 40 | pub fn destination(&self) -> WorldPos { 41 | self.to 42 | } 43 | 44 | pub fn step_diff(&mut self, dtime: Time) -> Vector3 { 45 | let step = self.dir * dtime.n as f32; 46 | self.current_dist.n += step.magnitude(); 47 | self.current.v += step; 48 | if self.is_finished() { 49 | self.current = self.to; 50 | } 51 | step 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/pick.rs: -------------------------------------------------------------------------------- 1 | use cgmath::{self, SquareMatrix, EuclideanSpace}; 2 | use collision::{Plane, Ray, Intersect}; 3 | use core::position::{MapPos}; 4 | use core::game_state::{State}; 5 | use context::{Context}; 6 | use geom; 7 | use camera::Camera; 8 | use types::{WorldPos}; 9 | 10 | pub fn pick_world_pos(context: &Context, camera: &Camera) -> WorldPos { 11 | let im = camera.mat().invert() 12 | .expect("Can`t invert camera matrix"); 13 | let win_size = context.win_size(); 14 | let w = win_size.w as f32; 15 | let h = win_size.h as f32; 16 | let x = context.mouse().pos.v.x as f32; 17 | let y = context.mouse().pos.v.y as f32; 18 | let x = (2.0 * x) / w - 1.0; 19 | let y = 1.0 - (2.0 * y) / h; 20 | let p0_raw = im * cgmath::Vector4{x: x, y: y, z: 0.0, w: 1.0}; 21 | let p0 = (p0_raw / p0_raw.w).truncate(); 22 | let p1_raw = im * cgmath::Vector4{x: x, y: y, z: 1.0, w: 1.0}; 23 | let p1 = (p1_raw / p1_raw.w).truncate(); 24 | let plane = Plane::from_abcd(0.0, 0.0, 1.0, 0.0); 25 | let ray = Ray::new(cgmath::Point3::from_vec(p0), p1 - p0); 26 | let intersection_pos = (plane, ray).intersection() 27 | .expect("Can`t find mouse ray/plane intersection"); 28 | WorldPos{v: intersection_pos.to_vec()} 29 | } 30 | 31 | pub fn pick_tile( 32 | context: &Context, 33 | state: &State, 34 | camera: &Camera, 35 | ) -> Option { 36 | let world_pos = pick_world_pos(context, camera); 37 | let pos = geom::world_pos_to_map_pos(world_pos); 38 | if state.map().is_inboard(pos) { 39 | Some(pos) 40 | } else { 41 | None 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/text.rs: -------------------------------------------------------------------------------- 1 | use types::{Size2}; 2 | use rusttype::{Scale, PositionedGlyph, Font, point}; 3 | 4 | fn calc_text_width(glyphs: &[PositionedGlyph]) -> f32 { 5 | glyphs.last().unwrap().pixel_bounding_box().unwrap().max.x as f32 6 | } 7 | 8 | pub fn text_to_texture(font: &Font, height: f32, text: &str) -> (Size2, Vec) { 9 | let scale = Scale { x: height, y: height }; 10 | let v_metrics = font.v_metrics(scale); 11 | let offset = point(0.0, v_metrics.ascent); 12 | let glyphs: Vec<_> = font.layout(text, scale, offset).collect(); 13 | let pixel_height = height.ceil() as usize; 14 | let width = calc_text_width(&glyphs) as usize; 15 | let mut pixel_data = vec![0_u8; 4 * width * pixel_height]; 16 | let mapping_scale = 255.0; 17 | for g in glyphs { 18 | let bb = match g.pixel_bounding_box() { 19 | Some(bb) => bb, 20 | None => continue, 21 | }; 22 | g.draw(|x, y, v| { 23 | let v = (v * mapping_scale + 0.5) as u8; 24 | let x = x as i32 + bb.min.x; 25 | let y = y as i32 + bb.min.y; 26 | // There's still a possibility that the glyph clips the boundaries of the bitmap 27 | if v > 0 && x >= 0 && x < width as i32 && y >= 0 && y < pixel_height as i32 { 28 | let i = (x as usize + y as usize * width) * 4; 29 | pixel_data[i] = 255; 30 | pixel_data[i + 1] = 255; 31 | pixel_data[i + 2] = 255; 32 | pixel_data[i + 3] = v; 33 | } 34 | }); 35 | } 36 | let size = Size2{w: width as i32, h: pixel_height as i32}; 37 | (size, pixel_data) 38 | } 39 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | global: 3 | PROJECT_NAME: zoc 4 | MAKE: C:\MinGW\bin\mingw32-make.exe 5 | matrix: 6 | - TARGET: i686-pc-windows-gnu 7 | CHANNEL: stable 8 | - TARGET: x86_64-pc-windows-msvc 9 | CHANNEL: stable 10 | - TARGET: x86_64-pc-windows-gnu 11 | CHANNEL: stable 12 | - TARGET: x86_64-pc-windows-gnu 13 | CHANNEL: beta 14 | - TARGET: x86_64-pc-windows-gnu 15 | CHANNEL: nightly 16 | 17 | matrix: 18 | allow_failures: 19 | - CHANNEL: nightly 20 | 21 | install: 22 | - set PATH=C:\msys64\usr\bin;%PATH% 23 | - set PATH=C:\msys64\mingw32\bin;%PATH% 24 | - if "%TARGET%" == "x86_64-pc-windows-gnu" set PATH=C:\msys64\mingw64\bin;%PATH% 25 | - curl -sSf -o rustup-init.exe https://win.rustup.rs 26 | - rustup-init.exe --default-host %TARGET% --default-toolchain %CHANNEL% -y 27 | - set PATH=%PATH%;C:\Users\appveyor\.cargo\bin 28 | - rustc -Vv 29 | - cargo -V 30 | 31 | build: false 32 | 33 | test_script: 34 | - cmd: '%MAKE% && %MAKE% test' 35 | 36 | cache: 37 | - target 38 | - C:\Users\appveyor\.cargo\registry 39 | 40 | before_deploy: 41 | - mkdir staging 42 | - mkdir staging\assets 43 | - copy target\release\zoc.exe staging 44 | - xcopy assets staging\assets /E /I 45 | - cd staging 46 | - 7z a ../%PROJECT_NAME%-%TARGET%.zip * 47 | - appveyor PushArtifact ../%PROJECT_NAME%-%TARGET%.zip 48 | 49 | deploy: 50 | provider: GitHub 51 | artifact: /.*\.zip/ 52 | auth_token: 53 | secure: gZr9PnP3Sj6KVw4fd9HUosRkvMZMhkeC0dA1eIbWMmFJj3hANFfUTIS4DYhkaaJo 54 | on: 55 | CHANNEL: stable 56 | appveyor_repo_tag: true 57 | branch: 58 | - master 59 | -------------------------------------------------------------------------------- /src/mesh.rs: -------------------------------------------------------------------------------- 1 | use gfx; 2 | use gfx::traits::{FactoryExt}; 3 | use gfx_gl; 4 | use fs; 5 | use context::{Context}; 6 | use texture::{Texture, load_texture}; 7 | use pipeline::{Vertex}; 8 | 9 | #[derive(PartialOrd, Ord, PartialEq, Eq, Hash, Clone, Copy, Debug)] 10 | pub struct MeshId{pub id: i32} 11 | 12 | #[derive(Clone, Debug)] 13 | pub struct Mesh { 14 | slice: gfx::Slice, 15 | vertex_buffer: gfx::handle::Buffer, 16 | texture: Texture, 17 | is_wire: bool, 18 | } 19 | 20 | impl Mesh { 21 | pub fn new(context: &mut Context, vertices: &[Vertex], indices: &[u16], tex: Texture) -> Mesh { 22 | let (v, s) = context.factory_mut().create_vertex_buffer_with_slice(vertices, indices); 23 | Mesh { 24 | slice: s, 25 | vertex_buffer: v, 26 | texture: tex, 27 | is_wire: false, 28 | } 29 | } 30 | 31 | pub fn new_wireframe(context: &mut Context, vertices: &[Vertex], indices: &[u16]) -> Mesh { 32 | let (v, s) = context.factory_mut().create_vertex_buffer_with_slice(vertices, indices); 33 | let texture_data = fs::load("white.png").into_inner(); 34 | let texture = load_texture(context, &texture_data); 35 | Mesh { 36 | slice: s, 37 | vertex_buffer: v, 38 | texture: texture, 39 | is_wire: true, 40 | } 41 | } 42 | 43 | pub fn slice(&self) -> &gfx::Slice { 44 | &self.slice 45 | } 46 | 47 | pub fn vertex_buffer(&self) -> &gfx::handle::Buffer { 48 | &self.vertex_buffer 49 | } 50 | 51 | pub fn texture(&self) -> &Texture { 52 | &self.texture 53 | } 54 | 55 | pub fn is_wire(&self) -> bool { 56 | self.is_wire 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/unit_type_visual_info.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap}; 2 | use core::unit::{UnitTypeId}; 3 | use core::db::{Db}; 4 | use context::{Context}; 5 | use mesh::{MeshId}; 6 | use types::{Speed}; 7 | use mesh_manager::{MeshManager, load_object_mesh}; 8 | 9 | #[derive(Clone, Debug)] 10 | pub struct UnitTypeVisualInfo { 11 | pub mesh_id: MeshId, 12 | pub move_speed: Speed, 13 | } 14 | 15 | #[derive(Clone, Debug)] 16 | pub struct UnitTypeVisualInfoManager { 17 | map: HashMap, 18 | } 19 | 20 | impl UnitTypeVisualInfoManager { 21 | pub fn new() -> UnitTypeVisualInfoManager { 22 | UnitTypeVisualInfoManager { 23 | map: HashMap::new(), 24 | } 25 | } 26 | 27 | pub fn add_info(&mut self, unit_type_id: UnitTypeId, info: UnitTypeVisualInfo) { 28 | self.map.insert(unit_type_id, info); 29 | } 30 | 31 | pub fn get(&self, type_id: UnitTypeId) -> &UnitTypeVisualInfo { 32 | &self.map[&type_id] 33 | } 34 | } 35 | 36 | pub fn get_unit_type_visual_info( 37 | db: &Db, 38 | context: &mut Context, 39 | meshes: &mut MeshManager, 40 | ) -> UnitTypeVisualInfoManager { 41 | let mut manager = UnitTypeVisualInfoManager::new(); 42 | for &(unit_name, model_name, move_speed) in &[ 43 | ("soldier", "soldier", 2.0), 44 | ("smg", "submachine", 2.0), 45 | ("scout", "scout", 2.5), 46 | ("mortar", "mortar", 1.5), 47 | ("field_gun", "field_gun", 1.5), 48 | ("light_spg", "light_spg", 3.0), 49 | ("light_tank", "light_tank", 3.0), 50 | ("medium_tank", "medium_tank", 2.5), 51 | ("heavy_tank", "tank", 2.0), 52 | ("mammoth_tank", "mammoth", 1.5), 53 | ("truck", "truck", 3.0), 54 | ("jeep", "jeep", 3.5), 55 | ("helicopter", "helicopter", 3.0), 56 | ] { 57 | manager.add_info(db.unit_type_id(unit_name), UnitTypeVisualInfo { 58 | mesh_id: meshes.add(load_object_mesh(context, model_name)), 59 | move_speed: Speed{n: move_speed}, 60 | }); 61 | } 62 | manager 63 | } 64 | -------------------------------------------------------------------------------- /core/src/misc.rs: -------------------------------------------------------------------------------- 1 | use std::cmp; 2 | use std::sync::mpsc::{Receiver}; 3 | use rand::{thread_rng, Rng}; 4 | 5 | pub fn clamp(n: T, min: T, max: T) -> T 6 | where T: Copy + cmp::PartialOrd 7 | { 8 | assert!(min <= max); 9 | match n { 10 | n if n < min => min, 11 | n if n > max => max, 12 | n => n, 13 | } 14 | } 15 | 16 | pub fn get_shuffled_indices(v: &[T]) -> Vec { 17 | let mut indices: Vec<_> = (0..v.len()).collect(); 18 | thread_rng().shuffle(&mut indices); 19 | indices 20 | } 21 | 22 | pub fn rx_collect(rx: &Receiver) -> Vec { 23 | let mut v = Vec::new(); 24 | while let Ok(data) = rx.try_recv() { 25 | v.push(data); 26 | } 27 | v 28 | } 29 | 30 | pub fn opt_rx_collect(rx: &Option>) -> Vec { 31 | if let Some(ref rx) = *rx { 32 | rx_collect(rx) 33 | } else { 34 | Vec::new() 35 | } 36 | } 37 | 38 | #[cfg(test)] 39 | mod tests { 40 | use std::sync::mpsc::{channel}; 41 | use misc::{clamp, get_shuffled_indices, rx_collect, opt_rx_collect}; 42 | 43 | #[test] 44 | fn test_clamp() { 45 | assert_eq!(clamp(-1, 0, 10), 0); 46 | assert_eq!(clamp(11, 0, 10), 10); 47 | assert_eq!(clamp(0, 0, 10), 0); 48 | assert_eq!(clamp(10, 0, 10), 10); 49 | assert_eq!(clamp(5, 0, 10), 5); 50 | } 51 | 52 | #[test] 53 | fn test_shuffle_touches_all_fields() { 54 | let mut v = [false; 10]; 55 | let indices = get_shuffled_indices(&v); 56 | for i in indices { 57 | v[i] = true; 58 | } 59 | for n in &v { 60 | assert_eq!(*n, true); 61 | } 62 | } 63 | 64 | #[test] 65 | fn test_rx_collect() { 66 | let (tx, rx) = channel(); 67 | tx.send(1).unwrap(); 68 | tx.send(2).unwrap(); 69 | tx.send(3).unwrap(); 70 | assert_eq!(rx_collect(&rx), [1, 2, 3]); 71 | } 72 | 73 | #[test] 74 | fn test_opt_rx_collect() { 75 | let (tx, rx) = channel(); 76 | tx.send(1).unwrap(); 77 | tx.send(2).unwrap(); 78 | tx.send(3).unwrap(); 79 | assert_eq!(opt_rx_collect(&Some(rx)), [1, 2, 3]); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /core/src/attack.rs: -------------------------------------------------------------------------------- 1 | use rand::{thread_rng, Rng}; 2 | use db::{Db}; 3 | use game_state::{State}; 4 | use unit::{Unit}; 5 | use misc::{clamp}; 6 | use map::{Terrain}; 7 | 8 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] 9 | pub struct AttackPoints{pub n: i32} 10 | 11 | #[derive(PartialOrd, PartialEq, Eq, Hash, Clone, Copy, Debug)] 12 | pub struct HitChance{pub n: i32} 13 | 14 | pub fn hit_chance( 15 | db: &Db, 16 | state: &State, 17 | attacker: &Unit, 18 | defender: &Unit, 19 | ) -> HitChance { 20 | let attacker_type = db.unit_type(attacker.type_id); 21 | let defender_type = db.unit_type(defender.type_id); 22 | let weapon_type = db.weapon_type(attacker_type.weapon_type_id); 23 | let cover_bonus = cover_bonus(db, state, defender); 24 | let hit_test_v = -7 - cover_bonus + defender_type.size 25 | + weapon_type.accuracy + attacker_type.weapon_skill; 26 | let pierce_test_v = 10 + -defender_type.armor + weapon_type.ap; 27 | let wound_test_v = 5 -defender_type.toughness + weapon_type.damage; 28 | let hit_test_v = clamp(hit_test_v, 0, 10); 29 | let pierce_test_v = clamp(pierce_test_v, 0, 10); 30 | let wound_test_v = clamp(wound_test_v, 0, 10); 31 | let k = (hit_test_v * pierce_test_v * wound_test_v) / 10; 32 | HitChance{n: clamp(k, 0, 100)} 33 | } 34 | 35 | fn cover_bonus(db: &Db, state: &State, defender: &Unit) -> i32 { 36 | let defender_type = db.unit_type(defender.type_id); 37 | if defender_type.is_infantry { 38 | match *state.map().tile(defender.pos) { 39 | Terrain::Plain | Terrain::Water => 0, 40 | Terrain::Trees => 2, 41 | Terrain::City => 3, 42 | } 43 | } else { 44 | 0 45 | } 46 | } 47 | 48 | pub fn get_killed_count(db: &Db, state: &State, attacker: &Unit, defender: &Unit) -> i32 { 49 | let hit = attack_test(db, state, attacker, defender); 50 | if !hit { 51 | return 0; 52 | } 53 | let defender_type = db.unit_type(defender.type_id); 54 | if defender_type.is_infantry { 55 | clamp(thread_rng().gen_range(1, 5), 1, defender.count) 56 | } else { 57 | 1 58 | } 59 | } 60 | 61 | fn attack_test(db: &Db, state: &State, attacker: &Unit, defender: &Unit) -> bool { 62 | let k = hit_chance(db, state, attacker, defender).n; 63 | let r = thread_rng().gen_range(0, 100); 64 | r < k 65 | } 66 | -------------------------------------------------------------------------------- /core/src/unit.rs: -------------------------------------------------------------------------------- 1 | use position::{ExactPos}; 2 | use event::{ReactionFireMode}; 3 | use player::{PlayerId}; 4 | use map::{Distance}; 5 | use movement::{MovePoints}; 6 | use attack::{AttackPoints}; 7 | use game_state::{ReinforcementPoints}; 8 | 9 | #[derive(PartialOrd, Ord, PartialEq, Eq, Hash, Clone, Copy, Debug)] 10 | pub struct UnitId{pub id: i32} 11 | 12 | #[derive(PartialOrd, Ord, PartialEq, Eq, Hash, Clone, Copy, Debug)] 13 | pub struct UnitTypeId{pub id: i32} 14 | 15 | #[derive(Clone, Debug)] 16 | pub struct Unit { 17 | pub id: UnitId, 18 | pub pos: ExactPos, 19 | pub player_id: PlayerId, 20 | pub type_id: UnitTypeId, 21 | pub move_points: Option, 22 | pub attack_points: Option, 23 | pub reactive_attack_points: Option, 24 | pub reaction_fire_mode: ReactionFireMode, 25 | pub count: i32, 26 | pub morale: i32, 27 | pub passenger_id: Option, 28 | pub attached_unit_id: Option, 29 | pub is_alive: bool, 30 | pub is_loaded: bool, 31 | pub is_attached: bool, 32 | } 33 | 34 | #[derive(Clone, Debug)] 35 | pub struct WeaponType { 36 | pub name: String, 37 | pub damage: i32, 38 | pub ap: i32, 39 | pub accuracy: i32, 40 | pub max_distance: Distance, 41 | pub min_distance: Distance, 42 | pub max_air_distance: Option, 43 | pub is_inderect: bool, 44 | pub reaction_fire: bool, 45 | pub smoke: Option, 46 | } 47 | 48 | #[derive(Clone, Copy, Debug)] 49 | pub struct WeaponTypeId{pub id: i32} 50 | 51 | #[derive(Clone, Debug)] 52 | pub struct UnitType { 53 | pub name: String, 54 | pub count: i32, 55 | pub size: i32, 56 | pub armor: i32, 57 | pub toughness: i32, 58 | pub weapon_skill: i32, 59 | pub weapon_type_id: WeaponTypeId, 60 | pub move_points: MovePoints, 61 | pub attack_points: AttackPoints, 62 | pub reactive_attack_points: AttackPoints, 63 | pub los_range: Distance, 64 | pub cover_los_range: Distance, 65 | pub is_transporter: bool, 66 | pub is_big: bool, 67 | pub is_air: bool, 68 | pub is_infantry: bool, 69 | pub can_be_towed: bool, 70 | pub cost: ReinforcementPoints, 71 | } 72 | 73 | pub fn is_commandable(player_id: PlayerId, unit: &Unit) -> bool { 74 | unit.is_alive && unit.player_id == player_id 75 | && !is_loaded_or_attached(unit) 76 | } 77 | 78 | pub fn is_loaded_or_attached(unit: &Unit) -> bool { 79 | unit.is_loaded || unit.is_attached 80 | } 81 | -------------------------------------------------------------------------------- /src/end_turn_screen.rs: -------------------------------------------------------------------------------- 1 | use cgmath::{Vector2}; 2 | use glutin::{self, WindowEvent, MouseButton, VirtualKeyCode}; 3 | use glutin::ElementState::{Released}; 4 | use core::player::{PlayerId}; 5 | use screen::{Screen, ScreenCommand, EventStatus}; 6 | use context::{Context}; 7 | use gui::{ButtonManager, Button, is_tap}; 8 | use types::{ScreenPos, Time}; 9 | 10 | #[derive(Clone, Debug)] 11 | pub struct EndTurnScreen { 12 | button_manager: ButtonManager, 13 | } 14 | 15 | impl EndTurnScreen { 16 | pub fn new( 17 | context: &mut Context, 18 | player_id: PlayerId, 19 | ) -> EndTurnScreen { 20 | let mut button_manager = ButtonManager::new(); 21 | let pos = ScreenPos{v: Vector2{x: 10, y: 10}}; 22 | let str = format!("Pass the device to Player {}", player_id.id); 23 | // TODO: button -> label + center on screen 24 | let _ = button_manager.add_button(Button::new( 25 | context, &str, pos)); 26 | EndTurnScreen { 27 | button_manager: button_manager, 28 | } 29 | } 30 | 31 | fn handle_event_lmb_release(&mut self, context: &mut Context) { 32 | if is_tap(context) { 33 | context.add_command(ScreenCommand::PopScreen); 34 | } 35 | } 36 | 37 | fn handle_event_key_press(&mut self, context: &mut Context, key: VirtualKeyCode) { 38 | if key == glutin::VirtualKeyCode::Q 39 | || key == glutin::VirtualKeyCode::Escape 40 | { 41 | context.add_command(ScreenCommand::PopScreen); 42 | } 43 | } 44 | } 45 | 46 | impl Screen for EndTurnScreen { 47 | fn tick(&mut self, context: &mut Context, _: Time) { 48 | context.set_basic_color([0.0, 0.0, 0.0, 1.0]); 49 | self.button_manager.draw(context); 50 | } 51 | 52 | fn handle_event(&mut self, context: &mut Context, event: &WindowEvent) -> EventStatus { 53 | match *event { 54 | WindowEvent::MouseInput { state: Released, button: MouseButton::Left, .. } => { 55 | self.handle_event_lmb_release(context); 56 | }, 57 | WindowEvent::Touch(glutin::Touch{phase, ..}) => { 58 | if glutin::TouchPhase::Ended == phase { 59 | self.handle_event_lmb_release(context); 60 | } 61 | }, 62 | WindowEvent::KeyboardInput { input: glutin::KeyboardInput { state: Released, virtual_keycode: Some(key), .. }, .. } => { 63 | self.handle_event_key_press(context, key); 64 | }, 65 | _ => {}, 66 | } 67 | EventStatus::Handled 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /core/src/fov.rs: -------------------------------------------------------------------------------- 1 | /// Field of View 2 | 3 | use std::f32::consts::{PI}; 4 | use cgmath::{InnerSpace}; 5 | use game_state::{State}; 6 | use map::{Terrain, Distance, spiral_iter}; 7 | use geom; 8 | use position::{MapPos}; 9 | use object::{ObjectClass}; 10 | 11 | struct Shadow { 12 | left: f32, 13 | right: f32, 14 | } 15 | 16 | fn is_tile_visible(angle: f32, shadows: &[Shadow]) -> bool { 17 | for shadow in shadows { 18 | if shadow.left < angle && shadow.right > angle { 19 | return false; 20 | } 21 | } 22 | true 23 | } 24 | 25 | fn is_obstacle(state: &State, pos: MapPos) -> bool { 26 | match *state.map().tile(pos){ 27 | Terrain::Trees | Terrain::City => return true, 28 | Terrain::Plain | Terrain::Water => {}, 29 | } 30 | for object in state.objects_at(pos) { 31 | match object.class { 32 | ObjectClass::Building | 33 | ObjectClass::Smoke => return true, 34 | ObjectClass::ReinforcementSector | 35 | ObjectClass::Road => {}, 36 | } 37 | } 38 | false 39 | } 40 | 41 | // TODO: precalculate all 'atan2' and 'asin' stuff 42 | pub fn fov( 43 | state: &State, 44 | origin: MapPos, 45 | range: Distance, 46 | callback: &mut FnMut(MapPos), 47 | ) { 48 | callback(origin); 49 | let map = state.map(); 50 | let mut shadows = vec!(); 51 | let origin3d = geom::map_pos_to_world_pos(origin); 52 | for pos in spiral_iter(origin, range) { 53 | if !map.is_inboard(pos) { 54 | continue; 55 | } 56 | let pos3d = geom::map_pos_to_world_pos(pos); 57 | let diff = pos3d - origin3d; 58 | let distance = diff.magnitude(); 59 | let angle = diff.x.atan2(diff.y); // TODO: optimize 60 | if is_tile_visible(angle, &shadows) { 61 | callback(pos); 62 | } 63 | if is_obstacle(state, pos) { 64 | let obstacle_radius = geom::HEX_IN_RADIUS * 1.1; 65 | let a = (obstacle_radius / distance).asin(); 66 | let shadow = Shadow{left: angle - a, right: angle + a}; 67 | if shadow.right > PI { 68 | shadows.push(Shadow{left: -PI, right: shadow.right - PI * 2.0}); 69 | } 70 | shadows.push(shadow); 71 | } 72 | } 73 | } 74 | 75 | pub fn simple_fov( 76 | state: &State, 77 | origin: MapPos, 78 | range: Distance, 79 | callback: &mut FnMut(MapPos), 80 | ) { 81 | callback(origin); 82 | for pos in spiral_iter(origin, range) { 83 | if state.map().is_inboard(pos) { 84 | callback(pos); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /core/src/player.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashSet, VecDeque}; 2 | use event::{CoreEvent}; 3 | use unit::{UnitId}; 4 | use fow::{Fow}; 5 | use db::{Db}; 6 | use game_state::{State}; 7 | use check::{CommandError, check_command}; 8 | use event::{Command}; 9 | use filter; 10 | 11 | #[derive(PartialOrd, PartialEq, Eq, Hash, Clone, Copy, Debug)] 12 | pub struct PlayerId{pub id: i32} 13 | 14 | #[derive(PartialEq, Clone, Copy, Debug)] 15 | pub enum PlayerClass { 16 | Human, 17 | Ai, 18 | } 19 | 20 | #[derive(Clone, Copy, Debug)] 21 | pub struct Player { 22 | pub id: PlayerId, 23 | pub class: PlayerClass, 24 | } 25 | 26 | #[derive(Clone, Debug)] 27 | pub struct PlayerInfo { 28 | id: PlayerId, 29 | events: VecDeque, 30 | visible_enemies: HashSet, 31 | 32 | // This filed is optional because we need to temporary 33 | // put its Fow into Core's State for filtering events. 34 | // 35 | // See State::to_full, State:to_partial 36 | fow: Option, 37 | } 38 | 39 | impl PlayerInfo { 40 | pub fn new(state: &State, id: PlayerId) -> PlayerInfo { 41 | let fow = Fow::new(state, id); 42 | PlayerInfo { 43 | id: id, 44 | fow: Some(fow), 45 | events: VecDeque::new(), 46 | visible_enemies: HashSet::new(), 47 | } 48 | } 49 | 50 | pub fn filter_event(&mut self, state: &State, event: &CoreEvent) { 51 | let (filtered_events, active_unit_ids) = filter::filter_events( 52 | state, self.id, self.fow(), event); 53 | for filtered_event in filtered_events { 54 | self.fow_mut().apply_event(state, &filtered_event); 55 | self.events.push_back(filtered_event); 56 | let new_enemies = filter::get_visible_enemies( 57 | state, self.fow(), self.id); 58 | let show_hide_events = filter::show_or_hide_passive_enemies( 59 | state, &active_unit_ids, &self.visible_enemies, &new_enemies); 60 | self.events.extend(show_hide_events); 61 | self.visible_enemies = new_enemies; 62 | } 63 | } 64 | 65 | pub fn get_event(&mut self) -> Option { 66 | self.events.pop_front() 67 | } 68 | 69 | pub fn visible_enemies(&self) -> &HashSet { 70 | &self.visible_enemies 71 | } 72 | 73 | pub fn check_command( 74 | &mut self, 75 | db: &Db, 76 | state: &mut State, 77 | command: &Command, 78 | ) -> Result<(), CommandError> { 79 | state.to_partial(self.fow.take().unwrap()); 80 | let result = check_command(db, self.id, state, command); 81 | self.fow = Some(state.to_full()); 82 | result 83 | } 84 | 85 | pub fn fow(&self) -> &Fow { 86 | self.fow.as_ref().unwrap() 87 | } 88 | 89 | pub fn fow_mut(&mut self) -> &mut Fow { 90 | self.fow.as_mut().unwrap() 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /core/src/dir.rs: -------------------------------------------------------------------------------- 1 | use cgmath::{Vector2}; 2 | use position::{MapPos}; 3 | 4 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 5 | pub enum Dir { 6 | SouthEast, 7 | East, 8 | NorthEast, 9 | NorthWest, 10 | West, 11 | SouthWest, 12 | } 13 | 14 | const DIR_TO_POS_DIFF: [[Vector2; 6]; 2] = [ 15 | [ 16 | Vector2{x: 1, y: -1}, 17 | Vector2{x: 1, y: 0}, 18 | Vector2{x: 1, y: 1}, 19 | Vector2{x: 0, y: 1}, 20 | Vector2{x: -1, y: 0}, 21 | Vector2{x: 0, y: -1}, 22 | ], 23 | [ 24 | Vector2{x: 0, y: -1}, 25 | Vector2{x: 1, y: 0}, 26 | Vector2{x: 0, y: 1}, 27 | Vector2{x: -1, y: 1}, 28 | Vector2{x: -1, y: 0}, 29 | Vector2{x: -1, y: -1}, 30 | ] 31 | ]; 32 | 33 | impl Dir { 34 | pub fn from_int(n: i32) -> Dir { 35 | assert!(n >= 0 && n < 6); 36 | let dirs = [ 37 | Dir::SouthEast, 38 | Dir::East, 39 | Dir::NorthEast, 40 | Dir::NorthWest, 41 | Dir::West, 42 | Dir::SouthWest, 43 | ]; 44 | dirs[n as usize] 45 | } 46 | 47 | pub fn to_int(&self) -> i32 { 48 | match *self { 49 | Dir::SouthEast => 0, 50 | Dir::East => 1, 51 | Dir::NorthEast => 2, 52 | Dir::NorthWest => 3, 53 | Dir::West => 4, 54 | Dir::SouthWest => 5, 55 | } 56 | } 57 | 58 | pub fn get_dir_from_to(from: MapPos, to: MapPos) -> Dir { 59 | // assert!(from.distance(to) == 1); 60 | let diff = to.v - from.v; 61 | let is_odd_row = from.v.y % 2 != 0; 62 | let subtable_index = if is_odd_row { 1 } else { 0 }; 63 | for dir in dirs() { 64 | if diff == DIR_TO_POS_DIFF[subtable_index][dir.to_int() as usize] { 65 | return dir; 66 | } 67 | } 68 | panic!("impossible positions: {}, {}", from, to); 69 | } 70 | 71 | pub fn get_neighbour_pos(pos: MapPos, dir: Dir) -> MapPos { 72 | let is_odd_row = pos.v.y % 2 != 0; 73 | let subtable_index = if is_odd_row { 1 } else { 0 }; 74 | let direction_index = dir.to_int(); 75 | assert!(direction_index >= 0 && direction_index < 6); 76 | let difference = DIR_TO_POS_DIFF[subtable_index][direction_index as usize]; 77 | MapPos{v: pos.v + difference} 78 | } 79 | } 80 | 81 | #[derive(Clone, Debug)] 82 | pub struct DirIter { 83 | index: i32, 84 | } 85 | 86 | pub fn dirs() -> DirIter { 87 | DirIter{index: 0} 88 | } 89 | 90 | impl Iterator for DirIter { 91 | type Item = Dir; 92 | 93 | fn next(&mut self) -> Option { 94 | let next_dir = if self.index > 5 { 95 | None 96 | } else { 97 | Some(Dir::from_int(self.index)) 98 | }; 99 | self.index += 1; 100 | next_dir 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /core/src/print_info.rs: -------------------------------------------------------------------------------- 1 | use db::{Db}; 2 | use unit::{Unit}; 3 | use game_state::{State}; 4 | use map::{Terrain}; 5 | use position::{MapPos}; 6 | 7 | pub fn print_unit_info(db: &Db, unit: &Unit) { 8 | let unit_type = db.unit_type(unit.type_id); 9 | let weapon_type = db.weapon_type(unit_type.weapon_type_id); 10 | println!("unit:"); 11 | println!(" player_id: {}", unit.player_id.id); 12 | if let Some(move_points) = unit.move_points { 13 | println!(" move_points: {}", move_points.n); 14 | } else { 15 | println!(" move_points: ?"); 16 | } 17 | if let Some(attack_points) = unit.attack_points { 18 | println!(" attack_points: {}", attack_points.n); 19 | } else { 20 | println!(" attack_points: ?"); 21 | } 22 | if let Some(reactive_attack_points) = unit.reactive_attack_points { 23 | println!(" reactive_attack_points: {}", reactive_attack_points.n); 24 | } else { 25 | println!(" reactive_attack_points: ?"); 26 | } 27 | println!(" count: {}", unit.count); 28 | println!(" morale: {}", unit.morale); 29 | println!(" passenger_id: {:?}", unit.passenger_id); 30 | println!(" attached_unit_id: {:?}", unit.attached_unit_id); 31 | println!(" is_alive: {:?}", unit.is_alive); 32 | println!("type:"); 33 | println!(" name: {}", unit_type.name); 34 | println!(" is_infantry: {}", unit_type.is_infantry); 35 | println!(" count: {}", unit_type.count); 36 | println!(" size: {}", unit_type.size); 37 | println!(" armor: {}", unit_type.armor); 38 | println!(" toughness: {}", unit_type.toughness); 39 | println!(" weapon_skill: {}", unit_type.weapon_skill); 40 | println!(" mp: {}", unit_type.move_points.n); 41 | println!(" ap: {}", unit_type.attack_points.n); 42 | println!(" reactive_ap: {}", unit_type.reactive_attack_points.n); 43 | println!(" los_range: {}", unit_type.los_range.n); 44 | println!(" cover_los_range: {}", unit_type.cover_los_range.n); 45 | println!("weapon:"); 46 | println!(" name: {}", weapon_type.name); 47 | println!(" damage: {}", weapon_type.damage); 48 | println!(" ap: {}", weapon_type.ap); 49 | println!(" accuracy: {}", weapon_type.accuracy); 50 | println!(" min_distance: {}", weapon_type.min_distance.n); 51 | println!(" max_distance: {}", weapon_type.max_distance.n); 52 | println!(" smoke: {:?}", weapon_type.smoke); 53 | } 54 | 55 | pub fn print_terrain_info(state: &State, pos: MapPos) { 56 | match *state.map().tile(pos) { 57 | Terrain::City => println!("City"), 58 | Terrain::Trees => println!("Trees"), 59 | Terrain::Plain => println!("Plain"), 60 | Terrain::Water => println!("Water"), 61 | } 62 | } 63 | 64 | pub fn print_pos_info(db: &Db, state: &State, pos: MapPos) { 65 | print_terrain_info(state, pos); 66 | println!(""); 67 | for unit in state.units_at(pos) { 68 | print_unit_info(db, unit); 69 | println!(""); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /core/src/sector.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashSet}; 2 | use cgmath::{Vector2}; 3 | use db::{Db}; 4 | use game_state::{State}; 5 | use position::{MapPos}; 6 | use event::{CoreEvent}; 7 | use player::{PlayerId}; 8 | 9 | #[derive(PartialOrd, PartialEq, Eq, Hash, Clone, Copy, Debug)] 10 | pub struct SectorId{pub id: i32} 11 | 12 | #[derive(Clone, Debug)] 13 | pub struct Sector { 14 | pub owner_id: Option, 15 | pub positions: Vec, 16 | } 17 | 18 | impl Sector { 19 | pub fn center(&self) -> MapPos { 20 | let mut pos = Vector2{x: 0.0, y: 0.0}; 21 | for sector_pos in &self.positions { 22 | pos.x += sector_pos.v.x as f32; 23 | pos.y += sector_pos.v.y as f32; 24 | } 25 | pos /= self.positions.len() as f32; 26 | let pos = MapPos{v: Vector2{ 27 | x: (pos.x + 0.5) as i32, 28 | y: (pos.y + 0.5) as i32, 29 | }}; 30 | assert!(self.positions.contains(&pos)); 31 | pos 32 | } 33 | } 34 | 35 | pub fn check_sectors(db: &Db, state: &State) -> Vec { 36 | let mut events = Vec::new(); 37 | for (§or_id, sector) in state.sectors() { 38 | let mut claimers = HashSet::new(); 39 | for &pos in §or.positions { 40 | for unit in state.units_at(pos) { 41 | let unit_type = db.unit_type(unit.type_id); 42 | if !unit_type.is_air && unit.is_alive { 43 | claimers.insert(unit.player_id); 44 | } 45 | } 46 | } 47 | let owner_id = if claimers.len() != 1 { 48 | None 49 | } else { 50 | Some(claimers.into_iter().next().unwrap()) 51 | }; 52 | if sector.owner_id != owner_id { 53 | events.push(CoreEvent::SectorOwnerChanged { 54 | sector_id: sector_id, 55 | new_owner_id: owner_id, 56 | }); 57 | } 58 | } 59 | events 60 | } 61 | 62 | #[cfg(test)] 63 | mod tests { 64 | use cgmath::{Vector2}; 65 | use sector::{Sector}; 66 | use position::{MapPos}; 67 | 68 | #[test] 69 | fn test_center_1() { 70 | let real = Sector { 71 | positions: vec![ 72 | MapPos{v: Vector2{x: 5, y: 0}}, 73 | MapPos{v: Vector2{x: 6, y: 0}}, 74 | MapPos{v: Vector2{x: 5, y: 1}}, 75 | MapPos{v: Vector2{x: 6, y: 1}}, 76 | MapPos{v: Vector2{x: 7, y: 1}}, 77 | MapPos{v: Vector2{x: 5, y: 2}}, 78 | MapPos{v: Vector2{x: 6, y: 2}}, 79 | ], 80 | owner_id: None, 81 | }.center(); 82 | let expected = MapPos{v: Vector2{x: 6, y: 1}}; 83 | assert_eq!(expected, real); 84 | } 85 | 86 | #[test] 87 | fn test_center_2() { 88 | let real = Sector { 89 | positions: vec![ 90 | MapPos{v: Vector2{x: 6, y: 0}}, 91 | MapPos{v: Vector2{x: 6, y: 1}}, 92 | MapPos{v: Vector2{x: 6, y: 2}}, 93 | ], 94 | owner_id: None, 95 | }.center(); 96 | let expected = MapPos{v: Vector2{x: 6, y: 1}}; 97 | assert_eq!(expected, real); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/selection.rs: -------------------------------------------------------------------------------- 1 | use cgmath::{Rad}; 2 | use core::unit::{UnitId}; 3 | use core::game_state::{State}; 4 | use core::dir::{dirs}; 5 | use geom; 6 | use fs; 7 | use scene::{Scene, SceneNode, NodeId}; 8 | use context::{Context}; 9 | use texture::{load_texture}; 10 | use types::{WorldPos}; 11 | use mesh::{MeshId}; 12 | use mesh::{Mesh}; 13 | use pipeline::{Vertex}; 14 | 15 | #[derive(Clone, Debug)] 16 | pub struct SelectionManager { 17 | unit_id: Option, 18 | mesh_id: MeshId, 19 | selection_marker_node_id: Option, 20 | } 21 | 22 | impl SelectionManager { 23 | pub fn new(mesh_id: MeshId) -> SelectionManager { 24 | SelectionManager { 25 | unit_id: None, 26 | mesh_id: mesh_id, 27 | selection_marker_node_id: None, 28 | } 29 | } 30 | 31 | fn get_pos(&self, state: &State) -> WorldPos { 32 | let unit_id = self.unit_id 33 | .expect("Can`t get pos if no unit is selected"); 34 | let map_pos = state.unit(unit_id).pos; 35 | WorldPos{v: geom::lift(geom::exact_pos_to_world_pos(state, map_pos).v)} 36 | } 37 | 38 | pub fn create_selection_marker( 39 | &mut self, 40 | state: &State, 41 | scene: &mut Scene, 42 | unit_id: UnitId, 43 | ) { 44 | self.unit_id = Some(unit_id); 45 | if let Some(node_id) = self.selection_marker_node_id { 46 | if scene.nodes().get(&node_id).is_some() { 47 | scene.remove_node(node_id); 48 | } 49 | } 50 | let node = SceneNode { 51 | pos: self.get_pos(state), 52 | rot: Rad(0.0), 53 | mesh_id: Some(self.mesh_id), 54 | color: [1.0, 1.0, 1.0, 1.0], 55 | children: Vec::new(), 56 | }; 57 | self.selection_marker_node_id = Some(scene.add_node(node)); 58 | } 59 | 60 | pub fn deselect(&mut self, scene: &mut Scene) { 61 | self.unit_id = None; 62 | if let Some(node_id) = self.selection_marker_node_id { 63 | scene.remove_node(node_id); 64 | } 65 | self.selection_marker_node_id = None; 66 | } 67 | } 68 | 69 | pub fn get_selection_mesh(context: &mut Context) -> Mesh { 70 | let texture_data = fs::load("shell.png").into_inner(); 71 | let texture = load_texture(context, &texture_data); 72 | let mut vertices = Vec::new(); 73 | let mut indices = Vec::new(); 74 | let scale_1 = 0.6; 75 | let scale_2 = scale_1 + 0.05; 76 | let mut i = 0; 77 | for dir in dirs() { 78 | let dir_index = dir.to_int(); 79 | let vertex_1_1 = geom::index_to_hex_vertex_s(scale_1, dir_index); 80 | let vertex_1_2 = geom::index_to_hex_vertex_s(scale_2, dir_index); 81 | let vertex_2_1 = geom::index_to_hex_vertex_s(scale_1, dir_index + 1); 82 | let vertex_2_2 = geom::index_to_hex_vertex_s(scale_2, dir_index + 1); 83 | vertices.push(Vertex{pos: vertex_1_1.v.into(), uv: [0.0, 0.0]}); 84 | vertices.push(Vertex{pos: vertex_1_2.v.into(), uv: [0.0, 1.0]}); 85 | vertices.push(Vertex{pos: vertex_2_1.v.into(), uv: [1.0, 0.0]}); 86 | vertices.push(Vertex{pos: vertex_2_2.v.into(), uv: [1.0, 1.0]}); 87 | indices.extend_from_slice(&[i, i + 1, i + 2, i + 1, i + 2, i + 3]); 88 | i += 4; 89 | } 90 | Mesh::new(context, &vertices, &indices, texture) 91 | } 92 | -------------------------------------------------------------------------------- /src/player_info.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap}; 2 | use std::rc::{Rc}; 3 | use cgmath::{Vector2, Vector3}; 4 | use core::game_state::{State}; 5 | use core::movement::{Pathfinder}; 6 | use core::map::{Map}; 7 | use core::db::{Db}; 8 | use core::player::{PlayerId}; 9 | use core::options::{Options, GameType}; 10 | use core::position::{MapPos}; 11 | use context::{Context}; 12 | use types::{Size2, Time, WorldPos}; 13 | use scene::{Scene, NodeId}; 14 | use geom; 15 | use camera::Camera; 16 | 17 | fn get_initial_camera_pos(map_size: Size2) -> WorldPos { 18 | let pos = get_max_camera_pos(map_size); 19 | WorldPos{v: Vector3{x: pos.v.x / 2.0, y: pos.v.y / 2.0, z: 0.0}} 20 | } 21 | 22 | fn get_max_camera_pos(map_size: Size2) -> WorldPos { 23 | let map_pos = MapPos{v: Vector2{x: map_size.w, y: map_size.h - 1}}; 24 | let pos = geom::map_pos_to_world_pos(map_pos); 25 | WorldPos{v: Vector3{x: -pos.v.x, y: -pos.v.y, z: 0.0}} 26 | } 27 | 28 | #[derive(Clone, Debug)] 29 | pub struct FowInfo { 30 | pub map: Map>, 31 | pub vanishing_node_ids: HashMap, 32 | pub forthcoming_node_ids: HashMap, 33 | } 34 | 35 | impl FowInfo { 36 | pub fn new(map_size: Size2) -> FowInfo { 37 | FowInfo { 38 | map: Map::new(map_size), 39 | vanishing_node_ids: HashMap::new(), 40 | forthcoming_node_ids: HashMap::new(), 41 | } 42 | } 43 | } 44 | 45 | #[derive(Clone, Debug)] 46 | pub struct PlayerInfo { 47 | pub game_state: State, 48 | pub pathfinder: Pathfinder, 49 | pub scene: Scene, 50 | pub camera: Camera, 51 | pub fow_info: FowInfo, 52 | } 53 | 54 | #[derive(Clone, Debug)] 55 | pub struct PlayerInfoManager { 56 | pub info: HashMap, 57 | } 58 | 59 | impl PlayerInfoManager { 60 | pub fn new(db: Rc, context: &Context, options: &Options) -> PlayerInfoManager { 61 | let state = State::new_partial(db.clone(), options, PlayerId{id: 0}); 62 | let map_size = state.map().size(); 63 | let mut m = HashMap::new(); 64 | let mut camera = Camera::new(context.win_size()); 65 | camera.set_max_pos(get_max_camera_pos(map_size)); 66 | camera.set_pos(get_initial_camera_pos(map_size)); 67 | m.insert(PlayerId{id: 0}, PlayerInfo { 68 | game_state: state, 69 | pathfinder: Pathfinder::new(db.clone(), map_size), 70 | scene: Scene::new(), 71 | camera: camera.clone(), 72 | fow_info: FowInfo::new(map_size), 73 | }); 74 | if options.game_type == GameType::Hotseat { 75 | let state2 = State::new_partial(db.clone(), options, PlayerId{id: 1}); 76 | m.insert(PlayerId{id: 1}, PlayerInfo { 77 | game_state: state2, 78 | pathfinder: Pathfinder::new(db, map_size), 79 | scene: Scene::new(), 80 | camera: camera, 81 | fow_info: FowInfo::new(map_size), 82 | }); 83 | } 84 | PlayerInfoManager{info: m} 85 | } 86 | 87 | pub fn get(&self, player_id: PlayerId) -> &PlayerInfo { 88 | &self.info[&player_id] 89 | } 90 | 91 | pub fn get_mut(&mut self, player_id: PlayerId) -> &mut PlayerInfo { 92 | match self.info.get_mut(&player_id) { 93 | Some(i) => i, 94 | None => panic!("Can`t find player_info for id={}", player_id.id), 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /core/src/event.rs: -------------------------------------------------------------------------------- 1 | use unit::{Unit, UnitId, UnitTypeId}; 2 | use position::{ExactPos, MapPos}; 3 | use player::{PlayerId}; 4 | use sector::{SectorId}; 5 | use object::{ObjectId}; 6 | use movement::{MovePoints}; 7 | 8 | #[derive(Clone, Copy, PartialEq, Debug)] 9 | pub enum FireMode { 10 | Active, 11 | Reactive, 12 | } 13 | 14 | #[derive(Clone, Copy, PartialEq, Debug)] 15 | pub enum ReactionFireMode { 16 | Normal, 17 | HoldFire, 18 | } 19 | 20 | #[derive(Clone, Copy, PartialEq, Debug)] 21 | pub enum MoveMode { 22 | Fast, 23 | Hunt, 24 | } 25 | 26 | #[derive(PartialEq, Clone, Debug)] 27 | pub enum Command { 28 | Move{unit_id: UnitId, path: Vec, mode: MoveMode}, 29 | EndTurn, 30 | CreateUnit{pos: ExactPos, type_id: UnitTypeId}, 31 | AttackUnit{attacker_id: UnitId, defender_id: UnitId}, 32 | LoadUnit{transporter_id: UnitId, passenger_id: UnitId}, 33 | UnloadUnit{transporter_id: UnitId, passenger_id: UnitId, pos: ExactPos}, 34 | Attach{transporter_id: UnitId, attached_unit_id: UnitId}, 35 | Detach{transporter_id: UnitId, pos: ExactPos}, 36 | SetReactionFireMode{unit_id: UnitId, mode: ReactionFireMode}, 37 | Smoke{unit_id: UnitId, pos: MapPos}, 38 | } 39 | 40 | #[derive(Clone, Debug, PartialEq)] 41 | pub struct AttackInfo { 42 | pub attacker_id: Option, 43 | pub defender_id: UnitId, 44 | pub mode: FireMode, 45 | pub killed: i32, 46 | pub suppression: i32, 47 | pub remove_move_points: bool, 48 | pub is_ambush: bool, 49 | pub is_inderect: bool, 50 | pub leave_wrecks: bool, 51 | } 52 | 53 | #[derive(Clone, Debug)] 54 | pub enum CoreEvent { 55 | Move { 56 | unit_id: UnitId, 57 | from: ExactPos, 58 | to: ExactPos, 59 | mode: MoveMode, 60 | cost: MovePoints, 61 | }, 62 | EndTurn { 63 | old_id: PlayerId, 64 | new_id: PlayerId, 65 | }, 66 | CreateUnit { 67 | unit_info: Unit, 68 | }, 69 | AttackUnit { 70 | attack_info: AttackInfo, 71 | }, 72 | // Reveal is like ShowUnit but is generated directly by Core 73 | Reveal { 74 | unit_info: Unit, 75 | }, 76 | ShowUnit { 77 | unit_info: Unit, 78 | }, 79 | HideUnit { 80 | unit_id: UnitId, 81 | }, 82 | LoadUnit { 83 | transporter_id: Option, 84 | passenger_id: UnitId, 85 | from: ExactPos, 86 | to: ExactPos, 87 | }, 88 | UnloadUnit { 89 | unit_info: Unit, 90 | transporter_id: Option, 91 | from: ExactPos, 92 | to: ExactPos, 93 | }, 94 | Attach { 95 | transporter_id: UnitId, 96 | attached_unit_id: UnitId, 97 | from: ExactPos, 98 | to: ExactPos, 99 | }, 100 | Detach { 101 | transporter_id: UnitId, 102 | from: ExactPos, 103 | to: ExactPos, 104 | }, 105 | SetReactionFireMode { 106 | unit_id: UnitId, 107 | mode: ReactionFireMode, 108 | }, 109 | SectorOwnerChanged { 110 | sector_id: SectorId, 111 | new_owner_id: Option, 112 | }, 113 | VictoryPoint { 114 | player_id: PlayerId, 115 | pos: MapPos, 116 | count: i32, 117 | }, 118 | // TODO: CreateObject 119 | Smoke { 120 | id: ObjectId, 121 | pos: MapPos, 122 | unit_id: Option, 123 | }, 124 | // TODO: RemoveObject 125 | RemoveSmoke { 126 | id: ObjectId, 127 | }, 128 | } 129 | -------------------------------------------------------------------------------- /src/game_results_screen.rs: -------------------------------------------------------------------------------- 1 | use cgmath::{Vector2}; 2 | use glutin::{self, WindowEvent, MouseButton, KeyboardInput, VirtualKeyCode}; 3 | use glutin::ElementState::{Released}; 4 | use core::player::{PlayerId}; 5 | use core::game_state::{State, Score}; 6 | use screen::{Screen, ScreenCommand, EventStatus}; 7 | use context::{Context}; 8 | use gui::{ButtonManager, Button, is_tap}; 9 | use types::{ScreenPos, Time}; 10 | 11 | fn winner_id(state: &State) -> PlayerId { 12 | // TODO: `CoreEvent::GameEnd` event? 13 | let mut winner_id = PlayerId{id: 0}; 14 | let mut winner_score = Score{n: 0}; 15 | for (&id, &score) in state.score() { 16 | if score.n > winner_score.n { 17 | winner_id = id; 18 | winner_score = score; 19 | } 20 | } 21 | winner_id 22 | } 23 | 24 | #[derive(Clone, Debug)] 25 | pub struct GameResultsScreen { 26 | button_manager: ButtonManager, 27 | } 28 | 29 | impl GameResultsScreen { 30 | pub fn new(context: &mut Context, state: &State) -> GameResultsScreen { 31 | let mut button_manager = ButtonManager::new(); 32 | let wh = context.win_size().h; 33 | let mut pos = ScreenPos{v: Vector2{x: 10, y: wh -10}}; 34 | pos.v.y -= wh / 10; // TODO: magic num 35 | let winner_index = winner_id(state); 36 | let str = format!("Player {} wins!", winner_index.id); 37 | let title_button = Button::new(context, &str, pos); 38 | pos.v.y -= title_button.size().h; // TODO: autolayout 39 | let _ = button_manager.add_button(title_button); 40 | for (player_index, player_score) in state.score() { 41 | let str = format!("Player {}: {} VPs", player_index.id, player_score.n); 42 | let button = Button::new(context, &str, pos); 43 | pos.v.y -= button.size().h; 44 | let _ = button_manager.add_button(button); 45 | } 46 | // TODO: button -> label + center on screen 47 | GameResultsScreen { 48 | button_manager: button_manager, 49 | } 50 | } 51 | 52 | fn handle_event_lmb_release(&mut self, context: &mut Context) { 53 | if is_tap(context) { 54 | context.add_command(ScreenCommand::PopScreen); 55 | } 56 | } 57 | 58 | fn handle_event_key_press(&mut self, context: &mut Context, key: VirtualKeyCode) { 59 | if key == glutin::VirtualKeyCode::Q 60 | || key == glutin::VirtualKeyCode::Escape 61 | { 62 | context.add_command(ScreenCommand::PopScreen); 63 | } 64 | } 65 | } 66 | 67 | impl Screen for GameResultsScreen { 68 | fn tick(&mut self, context: &mut Context, _: Time) { 69 | context.set_basic_color([0.0, 0.0, 0.0, 1.0]); 70 | self.button_manager.draw(context); 71 | } 72 | 73 | fn handle_event(&mut self, context: &mut Context, event: &WindowEvent) -> EventStatus { 74 | match *event { 75 | WindowEvent::MouseInput{ state: Released, button: MouseButton::Left, .. } => { 76 | self.handle_event_lmb_release(context); 77 | }, 78 | WindowEvent::Touch(glutin::Touch{phase, ..}) => { 79 | if glutin::TouchPhase::Ended == phase { 80 | self.handle_event_lmb_release(context); 81 | } 82 | }, 83 | WindowEvent::KeyboardInput { input: KeyboardInput { state: Released, virtual_keycode: Some(key), .. }, .. } => { 84 | self.handle_event_key_press(context, key); 85 | }, 86 | _ => {}, 87 | } 88 | EventStatus::Handled 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/camera.rs: -------------------------------------------------------------------------------- 1 | use std::f32::consts::{PI}; 2 | use cgmath::{perspective, Rad, Matrix4, Matrix3, Vector3, Array, Angle}; 3 | use core::misc::{clamp}; 4 | use types::{WorldPos, Size2}; 5 | 6 | #[derive(Clone, Debug)] 7 | pub struct Camera { 8 | x_angle: Rad, 9 | z_angle: Rad, 10 | pos: WorldPos, 11 | max_pos: WorldPos, 12 | zoom: f32, 13 | projection_mat: Matrix4, 14 | } 15 | 16 | fn get_projection_mat(win_size: Size2) -> Matrix4 { 17 | let fov = Rad(PI / 4.0); 18 | let ratio = win_size.w as f32 / win_size.h as f32; 19 | let display_range_min = 0.1; 20 | let display_range_max = 100.0; 21 | perspective(fov, ratio, display_range_min, display_range_max) 22 | } 23 | 24 | impl Camera { 25 | pub fn new(win_size: Size2) -> Camera { 26 | Camera { 27 | x_angle: Rad(PI / 4.0), 28 | z_angle: Rad(PI / 4.0), 29 | pos: WorldPos{v: Vector3::from_value(0.0)}, 30 | max_pos: WorldPos{v: Vector3::from_value(0.0)}, 31 | zoom: 25.0, 32 | projection_mat: get_projection_mat(win_size), 33 | } 34 | } 35 | 36 | pub fn mat(&self) -> Matrix4 { 37 | let zoom_m = Matrix4::from_translation(Vector3{x: 0.0, y: 0.0, z: -self.zoom}); 38 | let x_angle_m = Matrix4::from(Matrix3::from_angle_x(-self.x_angle)); 39 | let z_angle_m = Matrix4::from(Matrix3::from_angle_z(-self.z_angle)); 40 | let tr_m = Matrix4::from_translation(self.pos.v); 41 | self.projection_mat * zoom_m * x_angle_m * z_angle_m * tr_m 42 | } 43 | 44 | pub fn add_horizontal_angle(&mut self, angle: Rad) { 45 | self.z_angle = self.z_angle + angle; 46 | while self.z_angle < Rad(0.0) { 47 | self.z_angle = self.z_angle + Rad(PI * 2.0); 48 | } 49 | while self.z_angle > Rad(PI * 2.0) { 50 | self.z_angle = self.z_angle - Rad(PI * 2.0); 51 | } 52 | } 53 | 54 | pub fn add_vertical_angle(&mut self, angle: Rad) { 55 | self.x_angle = self.x_angle + angle; 56 | let min = Rad(PI / 18.0); 57 | let max = Rad(PI / 4.0); 58 | self.x_angle = clamp(self.x_angle, min, max); 59 | } 60 | 61 | fn clamp_pos(&mut self) { 62 | self.pos.v.x = clamp(self.pos.v.x, self.max_pos.v.x, 0.0); 63 | self.pos.v.y = clamp(self.pos.v.y, self.max_pos.v.y, 0.0); 64 | } 65 | 66 | pub fn set_pos(&mut self, pos: WorldPos) { 67 | self.pos = pos; 68 | self.clamp_pos(); 69 | } 70 | 71 | pub fn set_max_pos(&mut self, max_pos: WorldPos) { 72 | self.max_pos = max_pos; 73 | } 74 | 75 | pub fn change_zoom(&mut self, ratio: f32) { 76 | self.zoom *= ratio; 77 | self.zoom = clamp(self.zoom, 10.0, 40.0); 78 | } 79 | 80 | pub fn get_z_angle(&self) -> Rad { 81 | self.z_angle 82 | } 83 | 84 | pub fn get_x_angle(&self) -> Rad { 85 | self.x_angle 86 | } 87 | 88 | pub fn move_in_direction(&mut self, direction: Rad, speed: f32) { 89 | let diff = self.z_angle - direction; 90 | let dx = diff.sin(); 91 | let dy = diff.cos(); 92 | // TODO: handle zoom 93 | // self.pos.v.x -= dy * speed * self.zoom; 94 | // self.pos.v.y -= dx * speed * self.zoom; 95 | self.pos.v.x -= dy * speed; 96 | self.pos.v.y -= dx * speed; 97 | self.clamp_pos(); 98 | } 99 | 100 | pub fn regenerate_projection_mat(&mut self, win_size: Size2) { 101 | self.projection_mat = get_projection_mat(win_size); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/geom.rs: -------------------------------------------------------------------------------- 1 | use std::f32::consts::{PI}; 2 | use cgmath::{Vector3, Vector2, Rad, Angle, InnerSpace}; 3 | use core::geom; 4 | use core::position::{ExactPos, MapPos, SlotId, get_slots_count}; 5 | use core::dir::{Dir}; 6 | use core::game_state::{State}; 7 | use core::map::{Distance, spiral_iter}; 8 | use types::{VertexCoord, WorldPos, WorldDistance}; 9 | 10 | pub use core::geom::{HEX_IN_RADIUS, HEX_EX_RADIUS}; 11 | 12 | pub const MIN_LIFT_HEIGHT: f32 = 0.01; 13 | 14 | pub fn vec3_z(z: f32) -> Vector3 { 15 | Vector3{x: 0.0, y: 0.0, z: z} 16 | } 17 | 18 | pub fn world_pos_to_map_pos(pos: WorldPos) -> MapPos { 19 | let origin = MapPos{v: Vector2 { 20 | x: (pos.v.x / (HEX_IN_RADIUS * 2.0)) as i32, 21 | y: (pos.v.y / (HEX_EX_RADIUS * 1.5)) as i32, 22 | }}; 23 | let origin_world_pos = map_pos_to_world_pos(origin); 24 | let mut closest_map_pos = origin; 25 | let mut min_dist = (origin_world_pos.v - pos.v).magnitude(); 26 | for map_pos in spiral_iter(origin, Distance{n: 1}) { 27 | let world_pos = map_pos_to_world_pos(map_pos); 28 | let d = (world_pos.v - pos.v).magnitude(); 29 | if d < min_dist { 30 | min_dist = d; 31 | closest_map_pos = map_pos; 32 | } 33 | } 34 | closest_map_pos 35 | } 36 | 37 | pub fn map_pos_to_world_pos(p: MapPos) -> WorldPos { 38 | let v = geom::map_pos_to_world_pos(p).extend(0.0); 39 | WorldPos{v: v} 40 | } 41 | 42 | pub fn exact_pos_to_world_pos(state: &State, p: ExactPos) -> WorldPos { 43 | let v = geom::map_pos_to_world_pos(p.map_pos).extend(0.0); 44 | let n = get_slots_count(state.map(), p.map_pos); 45 | match p.slot_id { 46 | SlotId::TwoTiles(dir) => { 47 | // TODO: employ index_to_circle_vertex_rnd 48 | let p2 = Dir::get_neighbour_pos(p.map_pos, dir); 49 | let v2 = geom::map_pos_to_world_pos(p2).extend(0.0); 50 | WorldPos{v: (v + v2) / 2.0} 51 | } 52 | SlotId::WholeTile => { 53 | WorldPos{v: v + index_to_circle_vertex_rnd(n, 0, p.map_pos).v * 0.2} 54 | } 55 | SlotId::Air => { 56 | let v = v + vec3_z(2.0); 57 | WorldPos{v: v + index_to_circle_vertex_rnd(n, 0, p.map_pos).v * 0.2} // TODO 58 | } 59 | SlotId::Id(id) => { 60 | WorldPos{v: v + index_to_circle_vertex_rnd(n, id as i32, p.map_pos).v * 0.5} 61 | } 62 | } 63 | } 64 | 65 | pub fn lift(v: Vector3) -> Vector3 { 66 | let mut v = v; 67 | v.z += MIN_LIFT_HEIGHT; 68 | v 69 | } 70 | 71 | pub fn index_to_circle_vertex_rnd(count: i32, i: i32, pos: MapPos) -> VertexCoord { 72 | let n = 2.0 * PI * (i as f32) / (count as f32); 73 | let n = n + ((pos.v.x as f32 + pos.v.y as f32) * 7.0) % 4.0; // TODO: remove magic numbers 74 | let v = Vector3{x: n.cos(), y: n.sin(), z: 0.0}; 75 | VertexCoord{v: v * if count == 1 { 0.1 } else { HEX_EX_RADIUS }} 76 | } 77 | 78 | pub fn index_to_circle_vertex(count: i32, i: i32) -> VertexCoord { 79 | let n = PI / 2.0 + 2.0 * PI * (i as f32) / (count as f32); 80 | VertexCoord { 81 | v: Vector3{x: n.cos(), y: n.sin(), z: 0.0} * HEX_EX_RADIUS 82 | } 83 | } 84 | 85 | pub fn index_to_hex_vertex(i: i32) -> VertexCoord { 86 | index_to_circle_vertex(6, i) 87 | } 88 | 89 | pub fn index_to_hex_vertex_s(scale: f32, i: i32) -> VertexCoord { 90 | let v = index_to_hex_vertex(i).v * scale; 91 | VertexCoord{v: v} 92 | } 93 | 94 | pub fn dist(a: WorldPos, b: WorldPos) -> WorldDistance { 95 | let dx = (b.v.x - a.v.x).abs(); 96 | let dy = (b.v.y - a.v.y).abs(); 97 | let dz = (b.v.z - a.v.z).abs(); 98 | WorldDistance{n: ((dx.powi(2) + dy.powi(2) + dz.powi(2)) as f32).sqrt()} 99 | } 100 | 101 | pub fn get_rot_angle(a: WorldPos, b: WorldPos) -> Rad { 102 | let diff = b.v - a.v; 103 | let angle = diff.x.atan2(diff.y); 104 | Rad(-angle).normalize() 105 | } 106 | 107 | #[cfg(test)] 108 | mod tests { 109 | use std::f32::consts::{PI}; 110 | use cgmath::{Vector3}; 111 | use types::{WorldPos}; 112 | use super::{get_rot_angle, index_to_circle_vertex}; 113 | 114 | const EPS: f32 = 0.001; 115 | 116 | #[test] 117 | fn test_get_rot_angle_30_deg() { 118 | let count = 12; 119 | for i in 0 .. count { 120 | let a = WorldPos{v: Vector3{x: 0.0, y: 0.0, z: 0.0}}; 121 | let b = WorldPos{v: index_to_circle_vertex(count, i).v}; 122 | let expected_angle = i as f32 * (PI * 2.0) / (count as f32); 123 | let angle = get_rot_angle(a, b); 124 | let diff = (expected_angle - angle.0).abs(); 125 | assert!(diff < EPS, "{} != {}", expected_angle, angle.0); 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | 2 | Zone of Control 3 | =============== 4 | 5 | |license|_ 6 | |loc|_ 7 | |travis-ci|_ 8 | |appveyor-ci|_ 9 | 10 | 11 | The project is discontinued 12 | --------------------------- 13 | 14 | Sorry, friends. ZoC is discontinued. See https://ozkriff.github.io/2017-08-17--devlog.html 15 | 16 | 17 | Downloads 18 | --------- 19 | 20 | Precompiled binaries for linux, win and osx: https://github.com/ozkriff/zoc/releases 21 | 22 | 23 | Overview 24 | -------- 25 | 26 | ZoC is a turn-based hexagonal strategy game written in Rust_. 27 | 28 | Core game features are: 29 | 30 | - advanced fog of war 31 | - slot system (single tile fits multiple units) 32 | - reaction fire (xcom-like) 33 | - morale and suppression 34 | 35 | .. image:: http://i.imgur.com/TYoAVj6.png 36 | 37 | .. image:: http://i.imgur.com/DxfBok2.png 38 | 39 | .. image:: http://i.imgur.com/V4ZPCrT.png 40 | 41 | Player's objective is to capture and hold control zones for certain number of turns. 42 | 43 | Terrain types: 44 | 45 | - Plain 46 | - Trees 47 | - Water 48 | - Road/Bridge 49 | - City 50 | 51 | Unit types: 52 | 53 | - Infantry - weak, but can use terrain like Trees or City to get a defence bonus and hide from enemies; can be transported by trucks. Types: 54 | 55 | - rifleman - basic infantry type, 4 soldiers in a squad; 56 | - smg - more deadly on short distances, less deadly on full range, 3 soldiers in a squad; 57 | - scout - weak, but have advances visibility range and can better detect hidden enemies, 2 soldiers in a squad; 58 | - mortar - defenceless, but can shoot smokescreen rounds, slow; 59 | - field gun - effective against vehicles, slow and can't be transported inside of track, but can be _towed_; 60 | 61 | - Vehicles - can't hide in terrain, can't occupy buildings. Can't see hidden infantry. Leave a wreck when destroyed. Can take in a tow vehicle or wrecks lighter than themselves. Types: 62 | 63 | - jeep - fast and effective against infantry and helicopters; 64 | - truck - can transport infantry; 65 | - light tank 66 | - light self-propelled gun - has an armor of a light tank, but a gun of medium tank; 67 | - medium tank 68 | - heavy tank 69 | - mammoth tank 70 | 71 | - Aircrafts - can fly above all terrain features; it's line of sight isn't blocked by terrain. Only one type was implemented: 72 | - Helicopter 73 | 74 | Morale/Suppression system: 75 | 76 | - every unit initially have 100 morale points and restore 10 points every turn 77 | - morale is reduced by half a a damage chance (hit chance / armor protection) when a unit is attacked even if attack missed; 78 | - if a soldier of the squad is killed additional suppression is added 79 | - if a unit's morale falls below 50, then it's suppressed and can't attack anymore 80 | 81 | ------ 82 | 83 | Videos: 84 | 85 | - Some playtest (recorded in 2019, but uses a game build from 2017): https://youtu.be/3_ZPtwnMQVU 86 | - AI, reaction fire and sectors (2016.06.08): https://youtu.be/hI6YmZeuZ3s 87 | - transporter, roads (2016.08.07): https://youtu.be/_0_U-h1KCAE 88 | - smoke, water and bridges (2016.08.20): https://youtu.be/WJHkuWwAb7A 89 | 90 | 91 | Assets 92 | ------ 93 | 94 | Basic game assets are stored in a separate repo: 95 | https://github.com/ozkriff/zoc_assets 96 | 97 | Run ``make assets`` (or ``git clone https://github.com/ozkriff/zoc_assets assets``) to download them. 98 | 99 | 100 | Building 101 | -------- 102 | 103 | ``make`` or ``cargo build``. 104 | 105 | 106 | Running 107 | ------- 108 | 109 | ``make run`` or ``cargo run`` or ``./target/zoc``. 110 | 111 | 112 | Android 113 | ------- 114 | 115 | For instructions on setting up your environment see 116 | https://github.com/tomaka/android-rs-glue#setting-up-your-environment. 117 | 118 | Then just: ``make android_run`` - this will build .apk, install and run it. 119 | 120 | 121 | License 122 | ------- 123 | 124 | ZoC is distributed under the terms of both the MIT license and the Apache License (Version 2.0). 125 | 126 | See `LICENSE-APACHE`_ and `LICENSE-MIT`_ for details. 127 | 128 | 129 | .. |license| image:: https://img.shields.io/badge/license-MIT_or_Apache_2.0-blue.svg 130 | .. |loc| image:: https://tokei.rs/b1/github/ozkriff/zoc 131 | .. |travis-ci| image:: https://travis-ci.org/ozkriff/zoc.svg?branch=master 132 | .. |appveyor-ci| image:: https://ci.appveyor.com/api/projects/status/49kqaol7dlt2xrec/branch/master?svg=true 133 | .. _`This Month in ZoC`: https://users.rust-lang.org/t/this-month-in-zone-of-control/6993 134 | .. _Rust: https://rust-lang.org 135 | .. _LICENSE-MIT: LICENSE-MIT 136 | .. _LICENSE-APACHE: LICENSE-APACHE 137 | .. _loc: https://github.com/Aaronepower/tokei 138 | .. _travis-ci: https://travis-ci.org/ozkriff/zoc 139 | .. _appveyor-ci: https://ci.appveyor.com/project/ozkriff/zoc 140 | -------------------------------------------------------------------------------- /src/visualizer.rs: -------------------------------------------------------------------------------- 1 | use std::{process, thread, time}; 2 | use std::sync::mpsc::{channel, Receiver}; 3 | use std::fs::{metadata}; 4 | use glutin::Event; 5 | use screen::{Screen, ScreenCommand, EventStatus}; 6 | use context::{Context}; 7 | use main_menu_screen::{MainMenuScreen}; 8 | use types::{Time}; 9 | 10 | #[cfg(not(target_os = "android"))] 11 | fn check_assets_dir() { 12 | if let Err(e) = metadata("assets") { 13 | println!("Can`t find 'assets' dir: {}", e); 14 | println!("Note: see 'Assets' section of README.rst"); 15 | process::exit(1); 16 | } 17 | } 18 | 19 | #[cfg(target_os = "android")] 20 | fn check_assets_dir() {} 21 | 22 | pub struct Visualizer { 23 | screens: Vec>, 24 | popups: Vec>, 25 | should_close: bool, 26 | last_time: Time, 27 | context: Context, 28 | rx: Receiver, 29 | } 30 | 31 | impl Visualizer { 32 | pub fn new() -> Visualizer { 33 | check_assets_dir(); 34 | let (tx, rx) = channel(); 35 | let mut context = Context::new(tx); 36 | let screens = vec![ 37 | Box::new(MainMenuScreen::new(&mut context)) as Box, 38 | ]; 39 | let last_time = context.current_time(); 40 | Visualizer { 41 | screens: screens, 42 | popups: Vec::new(), 43 | should_close: false, 44 | last_time: last_time, 45 | context: context, 46 | rx: rx, 47 | } 48 | } 49 | 50 | pub fn tick(&mut self) { 51 | let max_fps = 60; 52 | let max_frame_time = time::Duration::from_millis(1000 / max_fps); 53 | let start_frame_time = time::Instant::now(); 54 | self.draw(); 55 | self.handle_events(); 56 | self.handle_commands(); 57 | let delta_time = start_frame_time.elapsed(); 58 | if max_frame_time > delta_time { 59 | thread::sleep(max_frame_time - delta_time); 60 | } 61 | } 62 | 63 | fn draw(&mut self) { 64 | let dtime = self.update_time(); 65 | self.context.clear(); 66 | { 67 | let screen = self.screens.last_mut().unwrap(); 68 | screen.tick(&mut self.context, dtime); 69 | } 70 | for popup in &mut self.popups { 71 | popup.tick(&mut self.context, dtime); 72 | } 73 | self.context.flush(); 74 | } 75 | 76 | fn handle_events(&mut self) { 77 | let events = self.context.poll_events(); 78 | for event in &events { 79 | let event = match event { 80 | Event::WindowEvent { ref event, ..} => event, 81 | Event::DeviceEvent { .. } => continue, 82 | Event::Suspended { .. } | Event::Awakened { .. } => unimplemented!("{:?}", event), 83 | }; 84 | self.context.handle_event_pre(event); 85 | let mut event_status = EventStatus::NotHandled; 86 | for i in (0 .. self.popups.len()).rev() { 87 | event_status = self.popups[i].handle_event( 88 | &mut self.context, event); 89 | if event_status == EventStatus::Handled { 90 | break; 91 | } 92 | } 93 | if event_status == EventStatus::NotHandled { 94 | let screen = self.screens.last_mut().unwrap(); 95 | screen.handle_event(&mut self.context, event); 96 | } 97 | self.context.handle_event_post(event); 98 | } 99 | } 100 | 101 | fn handle_commands(&mut self) { 102 | while let Ok(command) = self.rx.try_recv() { 103 | match command { 104 | ScreenCommand::PushScreen(screen) => { 105 | self.screens.push(screen); 106 | }, 107 | ScreenCommand::PushPopup(popup) => { 108 | self.popups.push(popup); 109 | }, 110 | ScreenCommand::PopScreen => { 111 | self.screens.pop().unwrap(); 112 | if self.screens.is_empty() { 113 | self.should_close = true; 114 | } 115 | self.popups.clear(); 116 | }, 117 | ScreenCommand::PopPopup => { 118 | assert!(self.popups.len() > 0); 119 | let _ = self.popups.pop(); 120 | }, 121 | } 122 | } 123 | } 124 | 125 | pub fn is_running(&self) -> bool { 126 | !self.should_close && !self.context.should_close() 127 | } 128 | 129 | fn update_time(&mut self) -> Time { 130 | let time = self.context.current_time(); 131 | let dtime = Time{n: time.n - self.last_time.n}; 132 | self.last_time = time; 133 | dtime 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/gui.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap}; 2 | use cgmath::{Vector3, Matrix4, ortho}; 3 | use context::{Context}; 4 | use texture::{load_texture_raw}; 5 | use types::{Size2, ScreenPos}; 6 | use text; 7 | use mesh::{Mesh}; 8 | use pipeline::{Vertex}; 9 | 10 | /// Check if this was a tap or swipe 11 | pub fn is_tap(context: &Context) -> bool { 12 | let mouse = context.mouse(); 13 | let diff = mouse.pos.v - mouse.last_press_pos.v; 14 | let tolerance = 20; // TODO: read from config file 15 | diff.x.abs() < tolerance && diff.y.abs() < tolerance 16 | } 17 | 18 | pub fn basic_text_size(context: &Context) -> f32 { 19 | // TODO: use different value for android 20 | let lines_per_screen_h = 14.0; 21 | (context.win_size().h as f32) / lines_per_screen_h 22 | } 23 | 24 | pub fn small_text_size(context: &Context) -> f32 { 25 | basic_text_size(context) / 2.0 26 | } 27 | 28 | pub fn get_2d_screen_matrix(win_size: Size2) -> Matrix4 { 29 | let left = 0.0; 30 | let right = win_size.w as f32; 31 | let bottom = 0.0; 32 | let top = win_size.h as f32; 33 | let near = -1.0; 34 | let far = 1.0; 35 | ortho(left, right, bottom, top, near, far) 36 | } 37 | 38 | #[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)] 39 | pub struct ButtonId {pub id: i32} 40 | 41 | #[derive(Clone, Debug)] 42 | pub struct Button { 43 | pos: ScreenPos, 44 | size: Size2, 45 | mesh: Mesh, 46 | } 47 | 48 | impl Button { 49 | pub fn new(context: &mut Context, label: &str, pos: ScreenPos) -> Button { 50 | let text_size = basic_text_size(context); 51 | Button::new_with_size(context, label, text_size, pos) 52 | } 53 | 54 | pub fn new_small(context: &mut Context, label: &str, pos: ScreenPos) -> Button { 55 | let text_size = small_text_size(context); 56 | Button::new_with_size(context, label, text_size, pos) 57 | } 58 | 59 | pub fn new_with_size(context: &mut Context, label: &str, size: f32, pos: ScreenPos) -> Button { 60 | let (texture_size, texture_data) = text::text_to_texture(context.font(), size, label); 61 | let texture = load_texture_raw(context.factory_mut(), texture_size, &texture_data); 62 | let h = texture_size.h as f32; 63 | let w = texture_size.w as f32; 64 | let vertices = &[ 65 | Vertex{pos: [0.0, 0.0, 0.0], uv: [0.0, 1.0]}, 66 | Vertex{pos: [0.0, h, 0.0], uv: [0.0, 0.0]}, 67 | Vertex{pos: [w, 0.0, 0.0], uv: [1.0, 1.0]}, 68 | Vertex{pos: [w, h, 0.0], uv: [1.0, 0.0]}, 69 | ]; 70 | let indices = &[0, 1, 2, 1, 2, 3]; 71 | let mesh = Mesh::new(context, vertices, indices, texture); 72 | Button { 73 | pos: pos, 74 | size: texture_size, 75 | mesh: mesh, 76 | } 77 | } 78 | 79 | pub fn draw(&self, context: &mut Context) { 80 | context.draw_mesh(&self.mesh); 81 | } 82 | 83 | pub fn pos(&self) -> ScreenPos { 84 | self.pos 85 | } 86 | 87 | pub fn set_pos(&mut self, pos: ScreenPos) { 88 | self.pos = pos; 89 | } 90 | 91 | pub fn size(&self) -> Size2 { 92 | self.size 93 | } 94 | } 95 | 96 | #[derive(Clone, Debug)] 97 | pub struct ButtonManager { 98 | buttons: HashMap, 99 | last_id: ButtonId, 100 | } 101 | 102 | impl ButtonManager { 103 | pub fn new() -> ButtonManager { 104 | ButtonManager { 105 | buttons: HashMap::new(), 106 | last_id: ButtonId{id: 0}, 107 | } 108 | } 109 | 110 | pub fn buttons(&self) -> &HashMap { 111 | &self.buttons 112 | } 113 | 114 | pub fn buttons_mut(&mut self) -> &mut HashMap { 115 | &mut self.buttons 116 | } 117 | 118 | pub fn add_button(&mut self, button: Button) -> ButtonId { 119 | let id = self.last_id; 120 | self.buttons.insert(id, button); 121 | self.last_id.id += 1; 122 | id 123 | } 124 | 125 | pub fn remove_button(&mut self, id: ButtonId) { 126 | self.buttons.remove(&id).unwrap(); 127 | } 128 | 129 | pub fn get_clicked_button_id(&self, context: &Context) -> Option { 130 | let x = context.mouse().pos.v.x; 131 | let y = context.win_size().h - context.mouse().pos.v.y; 132 | for (&id, button) in self.buttons() { 133 | if x >= button.pos().v.x 134 | && x <= button.pos().v.x + button.size().w 135 | && y >= button.pos().v.y 136 | && y <= button.pos().v.y + button.size().h 137 | { 138 | return Some(id); 139 | } 140 | } 141 | None 142 | } 143 | 144 | pub fn draw(&self, context: &mut Context) { 145 | let proj_mat = get_2d_screen_matrix(context.win_size()); 146 | for button in self.buttons().values() { 147 | let tr_mat = Matrix4::from_translation(Vector3 { 148 | x: button.pos().v.x as f32, 149 | y: button.pos().v.y as f32, 150 | z: 0.0, 151 | }); 152 | context.set_mvp(proj_mat * tr_mat); 153 | button.draw(context); 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/map_text.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, VecDeque}; 2 | use cgmath::{Matrix4, Matrix3}; 3 | use core::position::{MapPos}; 4 | use types::{Time, Speed}; 5 | use camera::Camera; 6 | use geom; 7 | use move_helper::{MoveHelper}; 8 | use context::{Context}; 9 | use texture::{load_texture_raw}; 10 | use mesh::{Mesh}; 11 | use text; 12 | use pipeline::{Vertex}; 13 | 14 | #[derive(Clone, Debug)] 15 | struct ShowTextCommand { 16 | pos: MapPos, 17 | text: String, 18 | } 19 | 20 | #[derive(Clone, Debug)] 21 | struct MapText { 22 | move_helper: MoveHelper, 23 | mesh: Mesh, 24 | pos: MapPos, 25 | } 26 | 27 | #[derive(Clone, Debug)] 28 | pub struct MapTextManager { 29 | commands: VecDeque, 30 | visible_labels_list: HashMap, 31 | last_label_id: i32, // TODO: think about better way of deleting old labels 32 | } 33 | 34 | impl MapTextManager { 35 | pub fn new() -> Self { 36 | MapTextManager { 37 | commands: VecDeque::new(), 38 | visible_labels_list: HashMap::new(), 39 | last_label_id: 0, 40 | } 41 | } 42 | 43 | pub fn add_text(&mut self, pos: MapPos, text: &str) { 44 | self.commands.push_back(ShowTextCommand { 45 | pos: pos, 46 | text: text.to_owned(), 47 | }); 48 | } 49 | 50 | fn can_show_text_here(&self, pos: MapPos) -> bool { 51 | let min_progress = 0.3; 52 | for map_text in self.visible_labels_list.values() { 53 | let progress = map_text.move_helper.progress(); 54 | if map_text.pos == pos && progress < min_progress { 55 | return false; 56 | } 57 | } 58 | true 59 | } 60 | 61 | pub fn do_commands(&mut self, context: &mut Context) { 62 | let mut postponed_commands = Vec::new(); 63 | while !self.commands.is_empty() { 64 | let command = self.commands.pop_front() 65 | .expect("MapTextManager: Can`t get next command"); 66 | if !self.can_show_text_here(command.pos) { 67 | postponed_commands.push(command); 68 | continue; 69 | } 70 | let mut from = geom::map_pos_to_world_pos(command.pos); 71 | from.v.z += 0.5; 72 | let mut to = from; 73 | to.v.z += 2.0; 74 | let mesh = { 75 | let (size, texture_data) = text::text_to_texture(context.font(), 80.0, &command.text); 76 | let texture = load_texture_raw(context.factory_mut(), size, &texture_data); 77 | let scale_factor = 200.0; // TODO: take camera zoom into account 78 | let h_2 = (size.h as f32 / scale_factor) / 2.0; 79 | let w_2 = (size.w as f32 / scale_factor) / 2.0; 80 | let vertices = &[ 81 | Vertex{pos: [-w_2, -h_2, 0.0], uv: [0.0, 1.0]}, 82 | Vertex{pos: [-w_2, h_2, 0.0], uv: [0.0, 0.0]}, 83 | Vertex{pos: [w_2, -h_2, 0.0], uv: [1.0, 1.0]}, 84 | Vertex{pos: [w_2, h_2, 0.0], uv: [1.0, 0.0]}, 85 | ]; 86 | let indices = &[0, 1, 2, 1, 2, 3]; 87 | Mesh::new(context, vertices, indices, texture) 88 | }; 89 | let move_speed = Speed{n: 1.0}; 90 | self.visible_labels_list.insert(self.last_label_id, MapText { 91 | pos: command.pos, 92 | mesh: mesh, 93 | move_helper: MoveHelper::new(from, to, move_speed), 94 | }); 95 | self.last_label_id += 1; 96 | } 97 | self.commands.extend(postponed_commands); 98 | } 99 | 100 | fn delete_old(&mut self) { 101 | let mut bad_keys = Vec::new(); 102 | for (&key, map_text) in &self.visible_labels_list { 103 | if map_text.move_helper.is_finished() { 104 | bad_keys.push(key); 105 | } 106 | } 107 | for key in &bad_keys { 108 | self.visible_labels_list.remove(key); 109 | } 110 | } 111 | 112 | pub fn draw( 113 | &mut self, 114 | context: &mut Context, 115 | camera: &Camera, 116 | dtime: Time, 117 | ) { 118 | self.do_commands(context); 119 | let rot_z_mat = Matrix4::from(Matrix3::from_angle_z(camera.get_z_angle())); 120 | let rot_x_mat = Matrix4::from(Matrix3::from_angle_x(camera.get_x_angle())); 121 | for map_text in self.visible_labels_list.values_mut() { 122 | // TODO: use https://github.com/orhanbalci/rust-easing 123 | let t = 0.8; 124 | let p = map_text.move_helper.progress(); 125 | let alpha = if p > t { 126 | (1.0 - p) / (1.0 - t) 127 | } else { 128 | 1.0 129 | }; 130 | context.set_basic_color([0.0, 0.0, 0.0, alpha]); 131 | let pos = map_text.move_helper.step(dtime); 132 | let tr_mat = Matrix4::from_translation(pos.v); 133 | let mvp = camera.mat() * tr_mat * rot_z_mat * rot_x_mat; 134 | context.set_mvp(mvp); 135 | context.draw_mesh(&map_text.mesh); 136 | } 137 | self.delete_old(); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/obj.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap}; 2 | use std::collections::hash_map::{Entry}; 3 | use std::fmt::{Debug}; 4 | use std::io::{BufRead}; 5 | use std::path::{Path}; 6 | use std::str::{SplitWhitespace, Split, FromStr}; 7 | use fs; 8 | use pipeline::{Vertex}; 9 | 10 | type Face = [[u16; 3]; 3]; 11 | 12 | #[derive(Clone, Debug)] 13 | struct Line { 14 | vertex: [u16; 2], 15 | } 16 | 17 | type Uv = [f32; 2]; 18 | 19 | type Pos = [f32; 3]; 20 | 21 | #[derive(Clone, Debug)] 22 | pub struct Model { 23 | faces: Vec, 24 | lines: Vec, 25 | uvs: Vec, 26 | positions: Vec, 27 | } 28 | 29 | fn parse_word(words: &mut SplitWhitespace) -> T 30 | where T::Err: Debug 31 | { 32 | let str = words.next().expect("Can not read next word"); 33 | str.parse().expect("Can not parse word") 34 | } 35 | 36 | fn parse_charsplit(words: &mut Split) -> T 37 | where T::Err: Debug 38 | { 39 | let str = words.next().expect("Can not read next word"); 40 | str.parse().expect("Can not parse word") 41 | } 42 | 43 | impl Model { 44 | pub fn new>(path: P) -> Model { 45 | let mut obj = Model { 46 | positions: Vec::new(), 47 | uvs: Vec::new(), 48 | faces: Vec::new(), 49 | lines: Vec::new(), 50 | }; 51 | obj.read(path); 52 | obj 53 | } 54 | 55 | fn read_v(words: &mut SplitWhitespace) -> Pos { 56 | // TODO: flip models 57 | [parse_word(words), -parse_word::(words), parse_word(words)] 58 | } 59 | 60 | fn read_vt(words: &mut SplitWhitespace) -> Uv { 61 | [ 62 | parse_word(words), 63 | 1.0 - parse_word::(words), // flip 64 | ] 65 | } 66 | 67 | fn read_f(words: &mut SplitWhitespace) -> Face { 68 | let mut f = [[0; 3]; 3]; 69 | for (i, group) in words.by_ref().enumerate() { 70 | let w = &mut group.split('/'); 71 | f[i] = [ 72 | parse_charsplit(w), 73 | parse_charsplit(w), 74 | parse_charsplit(w), 75 | ]; 76 | } 77 | f 78 | } 79 | 80 | fn read_l(words: &mut SplitWhitespace) -> Line { 81 | Line { 82 | vertex: [ 83 | parse_word(words), 84 | parse_word(words), 85 | ], 86 | } 87 | } 88 | 89 | fn read_line(&mut self, line: &str) { 90 | let mut words = line.split_whitespace(); 91 | fn is_correct_tag(tag: &str) -> bool { 92 | !tag.is_empty() && !tag.starts_with('#') 93 | } 94 | match words.next() { 95 | Some(tag) if is_correct_tag(tag) => { 96 | let w = &mut words; 97 | match tag { 98 | "v" => self.positions.push(Model::read_v(w)), 99 | "vt" => self.uvs.push(Model::read_vt(w)), 100 | "f" => self.faces.push(Model::read_f(w)), 101 | "l" => self.lines.push(Model::read_l(w)), 102 | "vn" | 103 | "s" | 104 | "#" => {}, 105 | unexpected_tag => { 106 | println!("obj: unexpected tag: {}", unexpected_tag); 107 | } 108 | } 109 | } 110 | _ => {}, 111 | }; 112 | } 113 | 114 | fn read>(&mut self, path: P) { 115 | for line in fs::load(path).lines() { 116 | match line { 117 | Ok(line) => self.read_line(&line), 118 | Err(msg) => panic!("Obj: read error: {}", msg), 119 | } 120 | } 121 | } 122 | 123 | pub fn is_wire(&self) -> bool { 124 | !self.lines.is_empty() 125 | } 126 | } 127 | 128 | pub fn build(model: &Model) -> (Vec, Vec) { 129 | let mut vertices = Vec::new(); 130 | let mut indices = Vec::new(); 131 | let mut components_map: HashMap<(u16, u16), u16> = HashMap::new(); 132 | for face in &model.faces { 133 | for face_vertex in face { 134 | let pos_id = face_vertex[0] - 1; 135 | let uv_id = face_vertex[1] - 1; 136 | let key = (pos_id, uv_id); 137 | let id = match components_map.entry(key) { 138 | Entry::Vacant(vacant) => { 139 | let id = vertices.len() as u16; 140 | vertices.push(Vertex { 141 | pos: model.positions[pos_id as usize], 142 | uv: model.uvs[uv_id as usize], 143 | }); 144 | vacant.insert(id); 145 | id 146 | } 147 | Entry::Occupied(occ) => *occ.get() 148 | }; 149 | indices.push(id); 150 | } 151 | } 152 | for line in &model.lines { 153 | for &line_vertex in &line.vertex { 154 | let pos_id = line_vertex as usize - 1; 155 | vertices.push(Vertex { 156 | pos: model.positions[pos_id], 157 | uv: [0.0, 0.0], 158 | }); 159 | indices.push(vertices.len() as u16 - 1); 160 | } 161 | } 162 | (vertices, indices) 163 | } 164 | -------------------------------------------------------------------------------- /src/scene.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet, BTreeMap}; 2 | use std::cmp::{Ord, Ordering}; 3 | use cgmath::{Rad}; 4 | use core::object::{ObjectId}; 5 | use core::unit::{UnitId}; 6 | use core::sector::{SectorId}; 7 | use types::{WorldPos}; 8 | use mesh::{MeshId}; 9 | 10 | #[derive(PartialOrd, Ord, PartialEq, Eq, Hash, Clone, Copy, Debug)] 11 | pub struct NodeId{pub id: i32} 12 | 13 | // TODO: Builder constructor 14 | #[derive(Clone, Debug)] 15 | pub struct SceneNode { 16 | pub pos: WorldPos, 17 | pub rot: Rad, 18 | pub mesh_id: Option, 19 | pub color: [f32; 4], 20 | pub children: Vec, 21 | } 22 | 23 | #[derive(PartialEq, PartialOrd, Clone, Copy, Debug)] 24 | pub struct Z(f32); 25 | 26 | impl Eq for Z {} 27 | 28 | impl Ord for Z { 29 | fn cmp(&self, other: &Self) -> Ordering { 30 | self.partial_cmp(other).unwrap_or(Ordering::Equal) 31 | } 32 | } 33 | 34 | #[derive(Clone, Debug)] 35 | pub struct Scene { 36 | unit_id_to_node_id_map: HashMap, 37 | sector_id_to_node_id_map: HashMap, 38 | object_id_to_node_id_map: HashMap>, 39 | nodes: HashMap, 40 | transparent_node_ids: BTreeMap>, 41 | next_id: NodeId, 42 | } 43 | 44 | impl Scene { 45 | pub fn new() -> Scene { 46 | Scene { 47 | unit_id_to_node_id_map: HashMap::new(), 48 | sector_id_to_node_id_map: HashMap::new(), 49 | object_id_to_node_id_map: HashMap::new(), 50 | nodes: HashMap::new(), 51 | transparent_node_ids: BTreeMap::new(), 52 | next_id: NodeId{id: 0}, 53 | } 54 | } 55 | 56 | pub fn unit_id_to_node_id_opt(&self, unit_id: UnitId) -> Option { 57 | self.unit_id_to_node_id_map.get(&unit_id).cloned() 58 | } 59 | 60 | pub fn unit_id_to_node_id(&self, unit_id: UnitId) -> NodeId { 61 | self.unit_id_to_node_id_map[&unit_id] 62 | } 63 | 64 | pub fn sector_id_to_node_id(&self, sector_id: SectorId) -> NodeId { 65 | self.sector_id_to_node_id_map[§or_id] 66 | } 67 | 68 | pub fn object_id_to_node_id(&self, object_id: ObjectId) -> &HashSet { 69 | &self.object_id_to_node_id_map[&object_id] 70 | } 71 | 72 | pub fn remove_node(&mut self, node_id: NodeId) { 73 | self.nodes.remove(&node_id).unwrap(); 74 | for layer in self.transparent_node_ids.values_mut() { 75 | layer.remove(&node_id); 76 | } 77 | } 78 | 79 | pub fn add_node(&mut self, node: SceneNode) -> NodeId { 80 | let node_id = self.next_id; 81 | self.next_id.id += 1; 82 | assert!(!self.nodes.contains_key(&node_id)); 83 | if node.color[3] < 1.0 { 84 | let z = Z(node.pos.v.z); 85 | self.transparent_node_ids.entry(z).or_insert_with(HashSet::new); 86 | let layer = self.transparent_node_ids.get_mut(&z).unwrap(); 87 | layer.insert(node_id); 88 | } 89 | self.nodes.insert(node_id, node); 90 | node_id 91 | } 92 | 93 | pub fn remove_unit(&mut self, unit_id: UnitId) { 94 | assert!(self.unit_id_to_node_id_map.contains_key(&unit_id)); 95 | let node_id = self.unit_id_to_node_id(unit_id); 96 | self.remove_node(node_id); 97 | self.unit_id_to_node_id_map.remove(&unit_id).unwrap(); 98 | } 99 | 100 | pub fn remove_object(&mut self, object_id: ObjectId) { 101 | assert!(self.object_id_to_node_id_map.contains_key(&object_id)); 102 | let node_ids = self.object_id_to_node_id(object_id).clone(); 103 | for node_id in node_ids { 104 | self.remove_node(node_id); 105 | } 106 | self.object_id_to_node_id_map.remove(&object_id).unwrap(); 107 | } 108 | 109 | pub fn add_unit(&mut self, unit_id: UnitId, node: SceneNode) -> NodeId { 110 | let node_id = self.add_node(node); 111 | assert!(!self.unit_id_to_node_id_map.contains_key(&unit_id)); 112 | self.unit_id_to_node_id_map.insert(unit_id, node_id); 113 | node_id 114 | } 115 | 116 | pub fn add_sector(&mut self, sector_id: SectorId, node: SceneNode) -> NodeId { 117 | let node_id = self.add_node(node); 118 | assert!(!self.sector_id_to_node_id_map.contains_key(§or_id)); 119 | self.sector_id_to_node_id_map.insert(sector_id, node_id); 120 | node_id 121 | } 122 | 123 | pub fn add_object(&mut self, object_id: ObjectId, node: SceneNode) -> NodeId { 124 | let node_id = self.add_node(node); 125 | self.object_id_to_node_id_map.entry(object_id).or_insert_with(HashSet::new); 126 | let node_ids = self.object_id_to_node_id_map.get_mut(&object_id).unwrap(); 127 | node_ids.insert(node_id); 128 | node_id 129 | } 130 | 131 | pub fn nodes(&self) -> &HashMap { 132 | &self.nodes 133 | } 134 | 135 | pub fn transparent_node_ids(&self) -> &BTreeMap> { 136 | &self.transparent_node_ids 137 | } 138 | 139 | pub fn node(&self, node_id: NodeId) -> &SceneNode { 140 | &self.nodes[&node_id] 141 | } 142 | 143 | pub fn node_mut(&mut self, node_id: NodeId) -> &mut SceneNode { 144 | self.nodes.get_mut(&node_id).expect("Bad node id") 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/reinforcements_popup.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc::{Sender}; 2 | use std::collections::{HashMap}; 3 | use glutin::{self, WindowEvent, MouseButton, VirtualKeyCode}; 4 | use glutin::ElementState::{Released}; 5 | use core::player::{PlayerId}; 6 | use core::position::{MapPos, ExactPos, get_free_exact_pos}; 7 | use core::game_state::{State}; 8 | use core::unit::{UnitTypeId}; 9 | use core::db::{Db}; 10 | use types::{Time, ScreenPos}; 11 | use screen::{Screen, ScreenCommand, EventStatus}; 12 | use context::{Context}; 13 | use gui::{ButtonManager, Button, ButtonId, is_tap, basic_text_size}; 14 | 15 | #[derive(PartialEq, Clone, Debug)] 16 | pub struct Options { 17 | unit_types: Vec<(UnitTypeId, ExactPos)>, 18 | } 19 | 20 | impl Options { 21 | pub fn new() -> Options { 22 | Options { 23 | unit_types: Vec::new(), 24 | } 25 | } 26 | } 27 | 28 | pub fn get_options( 29 | db: &Db, 30 | state: &State, 31 | player_id: PlayerId, 32 | pos: MapPos, 33 | ) -> Options { 34 | let mut options = Options::new(); 35 | let reinforcement_points = state.reinforcement_points()[&player_id]; 36 | for (i, unit_type) in db.unit_types().iter().enumerate() { 37 | let unit_type_id = UnitTypeId{id: i as i32}; 38 | let exact_pos = match get_free_exact_pos(state, unit_type, pos) { 39 | Some(exact_pos) => exact_pos, 40 | None => continue, 41 | }; 42 | if unit_type.cost > reinforcement_points { 43 | continue; 44 | } 45 | options.unit_types.push((unit_type_id, exact_pos)); 46 | } 47 | options 48 | } 49 | 50 | #[derive(Clone, Debug)] 51 | pub struct ReinforcementsPopup { 52 | game_screen_tx: Sender<(UnitTypeId, ExactPos)>, 53 | button_manager: ButtonManager, 54 | button_ids: HashMap, 55 | } 56 | 57 | impl ReinforcementsPopup { 58 | pub fn new( 59 | db: &Db, 60 | context: &mut Context, 61 | screen_pos: ScreenPos, 62 | options: Options, 63 | tx: Sender<(UnitTypeId, ExactPos)>, 64 | ) -> ReinforcementsPopup { 65 | let mut button_manager = ButtonManager::new(); 66 | let mut button_ids = HashMap::new(); 67 | let mut pos = screen_pos; 68 | let text_size = basic_text_size(context); 69 | pos.v.y -= text_size as i32; 70 | let vstep = (text_size * 0.8) as i32; 71 | for &(type_id, exact_pos) in &options.unit_types { 72 | let unit_type = db.unit_type(type_id); 73 | let text = &format!("[{}: {}$]", unit_type.name, unit_type.cost.n); 74 | let button_id = button_manager.add_button( 75 | Button::new(context, text, pos)); 76 | button_ids.insert(button_id, (type_id, exact_pos)); 77 | pos.v.y -= vstep; 78 | } 79 | assert!(button_ids.len() > 0); 80 | ReinforcementsPopup { 81 | game_screen_tx: tx, 82 | button_manager: button_manager, 83 | button_ids: button_ids, 84 | } 85 | } 86 | 87 | fn handle_event_lmb_release(&mut self, context: &mut Context) { 88 | if !is_tap(context) { 89 | return; 90 | } 91 | if let Some(button_id) = self.button_manager.get_clicked_button_id(context) { 92 | self.handle_event_button_press(context, button_id); 93 | } else { 94 | context.add_command(ScreenCommand::PopPopup); 95 | } 96 | } 97 | 98 | fn handle_event_button_press( 99 | &mut self, 100 | context: &mut Context, 101 | button_id: ButtonId 102 | ) { 103 | if let Some(&unit_info) = self.button_ids.get(&button_id) { 104 | self.game_screen_tx.send(unit_info).unwrap(); 105 | context.add_command(ScreenCommand::PopPopup); 106 | return; 107 | } else { 108 | panic!("Bad button id: {}", button_id.id); 109 | } 110 | } 111 | 112 | fn handle_event_key_press(&mut self, context: &mut Context, key: VirtualKeyCode) { 113 | match key { 114 | glutin::VirtualKeyCode::Q 115 | | glutin::VirtualKeyCode::Escape => 116 | { 117 | context.add_command(ScreenCommand::PopPopup); 118 | }, 119 | _ => {}, 120 | } 121 | } 122 | } 123 | 124 | impl Screen for ReinforcementsPopup { 125 | fn tick(&mut self, context: &mut Context, _: Time) { 126 | context.set_basic_color([0.0, 0.0, 0.0, 1.0]); 127 | self.button_manager.draw(context); 128 | } 129 | 130 | fn handle_event( 131 | &mut self, 132 | context: &mut Context, 133 | event: &glutin::WindowEvent, 134 | ) -> EventStatus { 135 | let mut event_status = EventStatus::Handled; 136 | match *event { 137 | WindowEvent::CursorMoved{..} => {}, 138 | WindowEvent::MouseInput{ state: Released, button: MouseButton::Left, .. } => { 139 | self.handle_event_lmb_release(context); 140 | }, 141 | WindowEvent::Touch(glutin::Touch{phase, ..}) => { 142 | if phase == glutin::TouchPhase::Ended { 143 | self.handle_event_lmb_release(context); 144 | } 145 | }, 146 | WindowEvent::KeyboardInput { input: glutin::KeyboardInput { state: Released, virtual_keycode: Some(key), .. }, .. } => { 147 | self.handle_event_key_press(context, key); 148 | }, 149 | _ => event_status = EventStatus::NotHandled, 150 | } 151 | event_status 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/mesh_manager.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap}; 2 | use context::{Context}; 3 | use core::game_state::{State}; 4 | use core::sector::{SectorId}; 5 | use texture::{load_texture}; 6 | use mesh::{Mesh, MeshId}; 7 | use selection::{get_selection_mesh}; 8 | use fs; 9 | use obj; 10 | use gen; 11 | 12 | #[derive(Clone, Debug)] 13 | pub struct MeshIdManager { 14 | pub big_building_mesh_id: MeshId, 15 | pub building_mesh_id: MeshId, 16 | pub big_building_mesh_w_id: MeshId, 17 | pub building_mesh_w_id: MeshId, 18 | pub road_mesh_id: MeshId, 19 | pub trees_mesh_id: MeshId, 20 | pub shell_mesh_id: MeshId, 21 | pub marker_mesh_id: MeshId, 22 | pub walkable_mesh_id: MeshId, 23 | pub targets_mesh_id: MeshId, 24 | pub map_mesh_id: MeshId, 25 | pub water_mesh_id: MeshId, 26 | pub selection_marker_mesh_id: MeshId, 27 | pub smoke_mesh_id: MeshId, 28 | pub fow_tile_mesh_id: MeshId, 29 | pub reinforcement_sector_tile_mesh_id: MeshId, 30 | pub sector_mesh_ids: HashMap, 31 | } 32 | 33 | impl MeshIdManager { 34 | pub fn new( 35 | context: &mut Context, 36 | meshes: &mut MeshManager, 37 | state: &State, 38 | ) -> MeshIdManager { 39 | let smoke_tex = load_texture(context, &fs::load("smoke.png").into_inner()); 40 | let floor_tex = load_texture(context, &fs::load("hex.png").into_inner()); 41 | let reinforcement_sector_tex = load_texture( 42 | context, &fs::load("reinforcement_sector.png").into_inner()); 43 | let chess_grid_tex = load_texture(context, &fs::load("chess_grid.png").into_inner()); 44 | let map_mesh_id = meshes.add(gen::generate_map_mesh( 45 | context, state, floor_tex.clone())); 46 | let water_mesh_id = meshes.add(gen::generate_water_mesh( 47 | context, state, floor_tex.clone())); 48 | let mut sector_mesh_ids = HashMap::new(); 49 | for (&id, sector) in state.sectors() { 50 | let mesh_id = meshes.add(gen::generate_sector_mesh( 51 | context, sector, chess_grid_tex.clone())); 52 | sector_mesh_ids.insert(id, mesh_id); 53 | } 54 | let selection_marker_mesh_id = meshes.add(get_selection_mesh(context)); 55 | let smoke_mesh_id = meshes.add(gen::get_one_tile_mesh(context, smoke_tex)); 56 | let fow_tile_mesh_id = meshes.add(gen::get_one_tile_mesh(context, floor_tex)); 57 | let reinforcement_sector_tile_mesh_id = meshes.add( 58 | gen::get_one_tile_mesh(context, reinforcement_sector_tex)); 59 | let big_building_mesh_id = meshes.add( 60 | load_object_mesh(context, "big_building")); 61 | let building_mesh_id = meshes.add( 62 | load_object_mesh(context, "building")); 63 | let big_building_mesh_w_id = meshes.add( 64 | load_object_mesh(context, "big_building_wire")); 65 | let building_mesh_w_id = meshes.add( 66 | load_object_mesh(context, "building_wire")); 67 | let trees_mesh_id = meshes.add(load_object_mesh(context, "trees")); 68 | let shell_mesh_id = meshes.add(gen::get_shell_mesh(context)); 69 | let road_mesh_id = meshes.add(gen::get_road_mesh(context)); 70 | let marker_mesh_id = meshes.add(gen::get_marker(context, "white.png")); 71 | let walkable_mesh_id = meshes.add(gen::empty_mesh(context)); 72 | let targets_mesh_id = meshes.add(gen::empty_mesh(context)); 73 | MeshIdManager { 74 | big_building_mesh_id: big_building_mesh_id, 75 | building_mesh_id: building_mesh_id, 76 | big_building_mesh_w_id: big_building_mesh_w_id, 77 | building_mesh_w_id: building_mesh_w_id, 78 | trees_mesh_id: trees_mesh_id, 79 | road_mesh_id: road_mesh_id, 80 | shell_mesh_id: shell_mesh_id, 81 | marker_mesh_id: marker_mesh_id, 82 | walkable_mesh_id: walkable_mesh_id, 83 | targets_mesh_id: targets_mesh_id, 84 | map_mesh_id: map_mesh_id, 85 | water_mesh_id: water_mesh_id, 86 | selection_marker_mesh_id: selection_marker_mesh_id, 87 | smoke_mesh_id: smoke_mesh_id, 88 | fow_tile_mesh_id: fow_tile_mesh_id, 89 | reinforcement_sector_tile_mesh_id: reinforcement_sector_tile_mesh_id, 90 | sector_mesh_ids: sector_mesh_ids, 91 | } 92 | } 93 | } 94 | 95 | pub fn load_object_mesh(context: &mut Context, name: &str) -> Mesh { 96 | let model = obj::Model::new(&format!("{}.obj", name)); 97 | let (vertices, indices) = obj::build(&model); 98 | if model.is_wire() { 99 | Mesh::new_wireframe(context, &vertices, &indices) 100 | } else { 101 | let texture_data = fs::load(format!("{}.png", name)).into_inner(); 102 | let texture = load_texture(context, &texture_data); 103 | Mesh::new(context, &vertices, &indices, texture) 104 | } 105 | } 106 | 107 | #[derive(Clone, Debug)] 108 | pub struct MeshManager { 109 | meshes: Vec, 110 | } 111 | 112 | impl MeshManager { 113 | pub fn new() -> MeshManager { 114 | MeshManager { 115 | meshes: Vec::new(), 116 | } 117 | } 118 | 119 | pub fn add(&mut self, mesh: Mesh) -> MeshId { 120 | self.meshes.push(mesh); 121 | MeshId{id: (self.meshes.len() as i32) - 1} 122 | } 123 | 124 | pub fn set(&mut self, id: MeshId, mesh: Mesh) { 125 | let index = id.id as usize; 126 | self.meshes[index] = mesh; 127 | } 128 | 129 | pub fn get(&self, id: MeshId) -> &Mesh { 130 | let index = id.id as usize; 131 | &self.meshes[index] 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/main_menu_screen.rs: -------------------------------------------------------------------------------- 1 | use cgmath::{Vector2}; 2 | use glutin::{self, WindowEvent, MouseButton, VirtualKeyCode}; 3 | use glutin::ElementState::{Released}; 4 | use screen::{Screen, ScreenCommand, EventStatus}; 5 | use tactical_screen::{TacticalScreen}; 6 | use core::options::{Options, GameType}; 7 | use context::{Context}; 8 | use gui::{ButtonManager, Button, ButtonId, is_tap}; 9 | use types::{ScreenPos, Time}; 10 | 11 | #[derive(Clone, Debug)] 12 | pub struct MainMenuScreen { 13 | button_start_hotseat_id: ButtonId, 14 | button_start_vs_ai_id: ButtonId, 15 | button_map_id: ButtonId, 16 | button_manager: ButtonManager, 17 | map_names: Vec<&'static str>, 18 | selected_map_index: usize, 19 | } 20 | 21 | impl MainMenuScreen { 22 | pub fn new(context: &mut Context) -> MainMenuScreen { 23 | let map_names = vec![ 24 | "map01", 25 | "map02", 26 | "map03", 27 | "map04", 28 | "map05", 29 | "map_fov_bug_test", 30 | ]; 31 | let selected_map_index = 0; 32 | let mut button_manager = ButtonManager::new(); 33 | // TODO: Use relative coords in ScreenPos - x: [0.0, 1.0], y: [0.0, 1.0] 34 | // TODO: Add analog of Qt::Alignment 35 | let mut button_pos = ScreenPos{v: Vector2{x: 10, y: 10}}; 36 | let button_start_hotseat_id = button_manager.add_button(Button::new( 37 | context, 38 | "[start hotseat]", 39 | button_pos, 40 | )); 41 | // TODO: Add something like QLayout 42 | let vstep = button_manager.buttons()[&button_start_hotseat_id].size().h; 43 | button_pos.v.y += vstep; 44 | let button_start_vs_ai_id = button_manager.add_button(Button::new( 45 | context, 46 | "[start human vs ai]", 47 | button_pos, 48 | )); 49 | button_pos.v.y += vstep * 2; 50 | let button_map_id = button_manager.add_button(Button::new( 51 | context, 52 | &format!("[map: {}]", map_names[selected_map_index]), 53 | button_pos, 54 | )); 55 | MainMenuScreen { 56 | button_manager: button_manager, 57 | button_start_hotseat_id: button_start_hotseat_id, 58 | button_start_vs_ai_id: button_start_vs_ai_id, 59 | button_map_id: button_map_id, 60 | map_names: map_names, 61 | selected_map_index: selected_map_index, 62 | } 63 | } 64 | 65 | fn handle_event_lmb_release(&mut self, context: &mut Context) { 66 | if !is_tap(context) { 67 | return; 68 | } 69 | if let Some(button_id) = self.button_manager.get_clicked_button_id(context) { 70 | self.handle_event_button_press(context, button_id); 71 | } 72 | } 73 | 74 | fn handle_event_button_press( 75 | &mut self, 76 | context: &mut Context, 77 | button_id: ButtonId 78 | ) { 79 | let map_name = self.map_names[self.selected_map_index].to_string(); 80 | let mut core_options = Options { 81 | game_type: GameType::Hotseat, 82 | map_name: map_name, 83 | players_count: 2, 84 | }; 85 | if button_id == self.button_start_hotseat_id { 86 | let tactical_screen = Box::new( 87 | TacticalScreen::new(context, &core_options)); 88 | context.add_command(ScreenCommand::PushScreen(tactical_screen)); 89 | } else if button_id == self.button_start_vs_ai_id { 90 | core_options.game_type = GameType::SingleVsAi; 91 | let tactical_screen = Box::new( 92 | TacticalScreen::new(context, &core_options)); 93 | context.add_command(ScreenCommand::PushScreen(tactical_screen)); 94 | } else if button_id == self.button_map_id { 95 | self.selected_map_index += 1; 96 | if self.selected_map_index == self.map_names.len() { 97 | self.selected_map_index = 0; 98 | } 99 | let text = &format!("[map: {}]", self.map_names[self.selected_map_index]); 100 | let pos = self.button_manager.buttons()[&self.button_map_id].pos(); 101 | let button_map = Button::new(context, text, pos); 102 | self.button_manager.remove_button(self.button_map_id); 103 | self.button_map_id = self.button_manager.add_button(button_map); 104 | } else { 105 | panic!("Bad button id: {}", button_id.id); 106 | } 107 | } 108 | 109 | fn handle_event_key_press(&mut self, context: &mut Context, key: VirtualKeyCode) { 110 | match key { 111 | glutin::VirtualKeyCode::Q 112 | | glutin::VirtualKeyCode::Escape => 113 | { 114 | context.add_command(ScreenCommand::PopScreen); 115 | }, 116 | _ => {}, 117 | } 118 | } 119 | } 120 | 121 | impl Screen for MainMenuScreen { 122 | fn tick(&mut self, context: &mut Context, _: Time) { 123 | context.clear(); 124 | context.set_basic_color([0.0, 0.0, 0.0, 1.0]); 125 | self.button_manager.draw(context); 126 | } 127 | 128 | fn handle_event(&mut self, context: &mut Context, event: &WindowEvent) -> EventStatus { 129 | match *event { 130 | WindowEvent::MouseInput { state: Released, button: MouseButton::Left, .. } => { 131 | self.handle_event_lmb_release(context); 132 | }, 133 | WindowEvent::Touch(glutin::Touch{phase, ..}) => { 134 | if phase == glutin::TouchPhase::Ended { 135 | self.handle_event_lmb_release(context); 136 | } 137 | }, 138 | WindowEvent::KeyboardInput { input: glutin::KeyboardInput { state: Released, virtual_keycode: Some(key), .. }, .. } => { 139 | self.handle_event_key_press(context, key); 140 | }, 141 | _ => {}, 142 | } 143 | EventStatus::Handled 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /core/src/fow.rs: -------------------------------------------------------------------------------- 1 | use std::default::{Default}; 2 | use std::rc::{Rc}; 3 | use game_state::{State}; 4 | use map::{Map, Terrain, distance}; 5 | use fov::{fov, simple_fov}; 6 | use db::{Db}; 7 | use unit::{Unit, UnitType}; 8 | use position::{MapPos, ExactPos, SlotId}; 9 | use event::{CoreEvent}; 10 | use player::{PlayerId}; 11 | use object::{ObjectClass}; 12 | 13 | #[derive(Clone, Copy, PartialEq, PartialOrd, Debug)] 14 | pub enum TileVisibility { 15 | No, 16 | // Bad, 17 | Normal, 18 | Excellent, 19 | } 20 | 21 | impl Default for TileVisibility { 22 | fn default() -> Self { TileVisibility::No } 23 | } 24 | 25 | fn calc_visibility( 26 | state: &State, 27 | unit_type: &UnitType, 28 | origin: MapPos, 29 | pos: MapPos, 30 | ) -> TileVisibility { 31 | let distance = distance(origin, pos); 32 | if distance > unit_type.los_range { 33 | return TileVisibility::No; 34 | } 35 | if !unit_type.is_air && distance <= unit_type.cover_los_range { 36 | return TileVisibility::Excellent; 37 | } 38 | let mut vis = match *state.map().tile(pos) { 39 | Terrain::City | Terrain::Trees => TileVisibility::Normal, 40 | Terrain::Plain | Terrain::Water => TileVisibility::Excellent, 41 | }; 42 | for object in state.objects_at(pos) { 43 | match object.class { 44 | // TODO: Remove Terrain::City and Terrain::Trees, use Smoke-like objects in logic 45 | ObjectClass::Building | ObjectClass::Smoke => { 46 | vis = TileVisibility::Normal; 47 | } 48 | ObjectClass::Road | 49 | ObjectClass::ReinforcementSector => {}, 50 | } 51 | } 52 | vis 53 | } 54 | 55 | /// Fog of War 56 | #[derive(Clone, Debug)] 57 | pub struct Fow { 58 | map: Map, 59 | air_map: Map, 60 | player_id: PlayerId, 61 | db: Rc, 62 | } 63 | 64 | impl Fow { 65 | pub fn new(state: &State, player_id: PlayerId) -> Fow { 66 | let db = state.db().clone(); 67 | let map_size = state.map().size(); 68 | let mut fow = Fow { 69 | map: Map::new(map_size), 70 | air_map: Map::new(map_size), 71 | player_id: player_id, 72 | db: db, 73 | }; 74 | fow.reset(state); 75 | fow 76 | } 77 | 78 | pub fn is_ground_tile_visible(&self, pos: MapPos) -> bool { 79 | match *self.map.tile(pos) { 80 | TileVisibility::Excellent | 81 | TileVisibility::Normal => true, 82 | TileVisibility::No => false, 83 | } 84 | } 85 | 86 | pub fn is_visible(&self, unit: &Unit) -> bool { 87 | self.is_visible_at(unit, unit.pos) 88 | } 89 | 90 | pub fn is_visible_at(&self, unit: &Unit, pos: ExactPos) -> bool { 91 | if pos.slot_id == SlotId::Air { 92 | *self.air_map.tile(pos.map_pos) != TileVisibility::No 93 | } else { 94 | let unit_type = self.db.unit_type(unit.type_id); 95 | match *self.map.tile(pos.map_pos) { 96 | TileVisibility::Excellent => true, 97 | TileVisibility::Normal => !unit_type.is_infantry, 98 | TileVisibility::No => false, 99 | } 100 | } 101 | } 102 | 103 | fn fov_unit(&mut self, state: &State, unit: &Unit) { 104 | assert!(unit.is_alive); 105 | let origin = unit.pos.map_pos; 106 | let unit_type = self.db.unit_type(unit.type_id); 107 | let range = unit_type.los_range; 108 | let ground_fow = &mut self.map; 109 | let ground_cb = &mut |pos| { 110 | let vis = calc_visibility(state, unit_type, origin, pos); 111 | if vis > *ground_fow.tile_mut(pos) { 112 | *ground_fow.tile_mut(pos) = vis; 113 | } 114 | }; 115 | if unit.pos.slot_id == SlotId::Air { 116 | simple_fov(state, origin, range, ground_cb); 117 | } else { 118 | fov(state, origin, range, ground_cb); 119 | } 120 | let air_fow = &mut self.air_map; 121 | simple_fov(state, origin, range, &mut |pos| { 122 | *air_fow.tile_mut(pos) = TileVisibility::Excellent; 123 | }); 124 | } 125 | 126 | fn clear(&mut self) { 127 | for pos in self.map.get_iter() { 128 | *self.map.tile_mut(pos) = TileVisibility::No; 129 | *self.air_map.tile_mut(pos) = TileVisibility::No; 130 | } 131 | } 132 | 133 | fn reset(&mut self, state: &State) { 134 | self.clear(); 135 | for (_, unit) in state.units() { 136 | if unit.player_id == self.player_id && unit.is_alive { 137 | self.fov_unit(state, unit); 138 | } 139 | } 140 | for object in state.objects().values() { 141 | if object.class != ObjectClass::ReinforcementSector 142 | || object.owner_id != Some(self.player_id) 143 | { 144 | continue; 145 | } 146 | *self.map.tile_mut(object.pos) = TileVisibility::Excellent; 147 | *self.air_map.tile_mut(object.pos) = TileVisibility::Excellent; 148 | } 149 | } 150 | 151 | pub fn apply_event( 152 | &mut self, 153 | state: &State, 154 | event: &CoreEvent, 155 | ) { 156 | match *event { 157 | CoreEvent::Move{unit_id, ..} => { 158 | let unit = state.unit(unit_id); 159 | if unit.player_id == self.player_id { 160 | self.fov_unit(state, unit); 161 | } 162 | }, 163 | CoreEvent::EndTurn{new_id, ..} => { 164 | if self.player_id == new_id { 165 | self.reset(state); 166 | } 167 | }, 168 | CoreEvent::CreateUnit{ref unit_info} => { 169 | let unit = state.unit(unit_info.id); 170 | if self.player_id == unit_info.player_id { 171 | self.fov_unit(state, unit); 172 | } 173 | }, 174 | CoreEvent::AttackUnit{ref attack_info} => { 175 | if let Some(attacker_id) = attack_info.attacker_id { 176 | if !attack_info.is_ambush { 177 | let pos = state.unit(attacker_id).pos; 178 | // TODO: do not give away all units in this tile! 179 | *self.map.tile_mut(pos) = TileVisibility::Excellent; 180 | } 181 | } 182 | }, 183 | CoreEvent::UnloadUnit{ref unit_info, ..} => { 184 | if self.player_id == unit_info.player_id { 185 | let unit = state.unit(unit_info.id); 186 | self.fov_unit(state, unit); 187 | } 188 | }, 189 | CoreEvent::Detach{transporter_id, ..} => { 190 | let transporter = state.unit(transporter_id); 191 | if self.player_id == transporter.player_id { 192 | self.fov_unit(state, transporter); 193 | } 194 | }, 195 | CoreEvent::Reveal{..} | 196 | CoreEvent::ShowUnit{..} | 197 | CoreEvent::HideUnit{..} | 198 | CoreEvent::LoadUnit{..} | 199 | CoreEvent::Attach{..} | 200 | CoreEvent::SetReactionFireMode{..} | 201 | CoreEvent::SectorOwnerChanged{..} | 202 | CoreEvent::Smoke{..} | 203 | CoreEvent::RemoveSmoke{..} | 204 | CoreEvent::VictoryPoint{..} => {}, 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/gen.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path}; 2 | use cgmath::{Vector2, Array}; 3 | use core::event::{Command}; 4 | use core::player::{PlayerId}; 5 | use core::unit::{UnitId}; 6 | use core::position::{MapPos, ExactPos}; 7 | use core::sector::{Sector}; 8 | use core::db::{Db}; 9 | use core::movement::{Pathfinder, MovePoints}; 10 | use core::map::{Terrain}; 11 | use core::game_state::{State}; 12 | use core::check::{check_command}; 13 | use context::{Context}; 14 | use texture::{Texture, load_texture}; 15 | use mesh::{Mesh}; 16 | use pipeline::{Vertex}; 17 | use core::dir::{Dir, dirs}; 18 | use geom; 19 | use fs; 20 | 21 | pub fn get_player_color(player_id: PlayerId) -> [f32; 4] { 22 | match player_id.id { 23 | 0 => [0.1, 0.1, 1.0, 1.0], 24 | 1 => [0.0, 0.8, 0.0, 1.0], 25 | n => panic!("Wrong player id: {}", n), 26 | } 27 | } 28 | 29 | pub fn generate_tiles_mesh>( 30 | context: &mut Context, 31 | tex: Texture, 32 | positions: I 33 | ) -> Mesh { 34 | let mut vertices = Vec::new(); 35 | let mut indices = Vec::new(); 36 | let mut i = 0; 37 | for tile_pos in positions { 38 | let pos = geom::map_pos_to_world_pos(tile_pos); 39 | for dir in dirs() { 40 | let vertex = geom::index_to_hex_vertex(dir.to_int()); 41 | let uv = vertex.v.truncate() / (geom::HEX_EX_RADIUS * 2.0) 42 | + Vector2::from_value(0.5); 43 | vertices.push(Vertex { 44 | pos: (pos.v + vertex.v).into(), 45 | uv: uv.into(), 46 | }); 47 | } 48 | indices.extend_from_slice(&[ 49 | i, i + 1, i + 2, 50 | i, i + 2, i + 3, 51 | i, i + 3, i + 5, 52 | i + 3, i + 4, i + 5, 53 | ]); 54 | i += 6; 55 | } 56 | Mesh::new(context, &vertices, &indices, tex) 57 | } 58 | 59 | pub fn generate_sector_mesh(context: &mut Context, sector: &Sector, tex: Texture) -> Mesh { 60 | generate_tiles_mesh(context, tex, sector.positions.to_vec()) 61 | } 62 | 63 | pub fn generate_map_mesh(context: &mut Context, state: &State, tex: Texture) -> Mesh { 64 | let mut normal_positions = Vec::new(); 65 | for tile_pos in state.map().get_iter() { 66 | if *state.map().tile(tile_pos) != Terrain::Water { 67 | normal_positions.push(tile_pos); 68 | } 69 | } 70 | generate_tiles_mesh(context, tex, normal_positions) 71 | } 72 | 73 | pub fn generate_water_mesh(context: &mut Context, state: &State, tex: Texture) -> Mesh { 74 | let mut normal_positions = Vec::new(); 75 | for pos in state.map().get_iter() { 76 | if *state.map().tile(pos) == Terrain::Water { 77 | normal_positions.push(pos); 78 | } 79 | } 80 | generate_tiles_mesh(context, tex, normal_positions) 81 | } 82 | 83 | pub fn empty_mesh(context: &mut Context) -> Mesh { 84 | Mesh::new_wireframe(context, &[], &[]) 85 | } 86 | 87 | pub fn build_walkable_mesh( 88 | context: &mut Context, 89 | pf: &Pathfinder, 90 | state: &State, 91 | move_points: MovePoints, 92 | ) -> Mesh { 93 | let map = state.map(); 94 | let mut vertices = Vec::new(); 95 | let mut indices = Vec::new(); 96 | let mut i = 0; 97 | for tile_pos in map.get_iter() { 98 | if pf.get_map().tile(tile_pos).cost().n > move_points.n { 99 | continue; 100 | } 101 | if let Some(parent_dir) = pf.get_map().tile(tile_pos).parent() { 102 | let tile_pos_to = Dir::get_neighbour_pos(tile_pos, parent_dir); 103 | let exact_pos = ExactPos { 104 | map_pos: tile_pos, 105 | slot_id: pf.get_map().tile(tile_pos).slot_id(), 106 | }; 107 | let exact_pos_to = ExactPos { 108 | map_pos: tile_pos_to, 109 | slot_id: pf.get_map().tile(tile_pos_to).slot_id(), 110 | }; 111 | let mut world_pos_from = geom::exact_pos_to_world_pos(state, exact_pos); 112 | world_pos_from.v.z = 0.0; 113 | let mut world_pos_to = geom::exact_pos_to_world_pos(state, exact_pos_to); 114 | world_pos_to.v.z = 0.0; 115 | vertices.push(Vertex { 116 | pos: geom::lift(world_pos_from.v).into(), 117 | uv: [0.5, 0.5], 118 | }); 119 | vertices.push(Vertex { 120 | pos: geom::lift(world_pos_to.v).into(), 121 | uv: [0.5, 0.5], 122 | }); 123 | indices.extend_from_slice(&[i, i + 1]); 124 | i += 2; 125 | } 126 | } 127 | Mesh::new_wireframe(context, &vertices, &indices) 128 | } 129 | 130 | pub fn build_targets_mesh(db: &Db, context: &mut Context, state: &State, unit_id: UnitId) -> Mesh { 131 | let mut vertices = Vec::new(); 132 | let mut indices = Vec::new(); 133 | let unit = state.unit(unit_id); 134 | let mut i = 0; 135 | for (&enemy_id, enemy) in state.units() { 136 | if unit.player_id == enemy.player_id { 137 | continue; 138 | } 139 | let command = Command::AttackUnit { 140 | attacker_id: unit_id, 141 | defender_id: enemy_id, 142 | }; 143 | if !check_command(db, unit.player_id, state, &command).is_ok() { 144 | continue; 145 | } 146 | let world_pos_from = geom::exact_pos_to_world_pos(state, unit.pos); 147 | let world_pos_to = geom::exact_pos_to_world_pos(state, enemy.pos); 148 | vertices.push(Vertex { 149 | pos: geom::lift(world_pos_from.v).into(), 150 | uv: [0.5, 0.5], 151 | }); 152 | vertices.push(Vertex { 153 | pos: geom::lift(world_pos_to.v).into(), 154 | uv: [0.5, 0.5], 155 | }); 156 | indices.extend_from_slice(&[i, i + 1]); 157 | i += 2; 158 | } 159 | Mesh::new_wireframe(context, &vertices, &indices) 160 | } 161 | 162 | pub fn get_shell_mesh(context: &mut Context) -> Mesh { 163 | let w = 0.05; 164 | let l = w * 3.0; 165 | let h = 0.1; 166 | let vertices = [ 167 | Vertex{pos: [-w, -l, h], uv: [0.0, 0.0]}, 168 | Vertex{pos: [-w, l, h], uv: [0.0, 1.0]}, 169 | Vertex{pos: [w, l, h], uv: [1.0, 0.0]}, 170 | Vertex{pos: [w, -l, h], uv: [1.0, 0.0]}, 171 | ]; 172 | let indices = [0, 1, 2, 2, 3, 0]; 173 | let texture_data = fs::load("shell.png").into_inner(); 174 | let texture = load_texture(context, &texture_data); 175 | Mesh::new(context, &vertices, &indices, texture) 176 | } 177 | 178 | pub fn get_road_mesh(context: &mut Context) -> Mesh { 179 | let w = geom::HEX_EX_RADIUS * 0.3; 180 | let l = geom::HEX_EX_RADIUS; 181 | let h = geom::MIN_LIFT_HEIGHT / 2.0; 182 | let vertices = [ 183 | Vertex{pos: [-w, -l, h], uv: [0.0, 0.0]}, 184 | Vertex{pos: [-w, l, h], uv: [0.0, 1.0]}, 185 | Vertex{pos: [w, l, h], uv: [1.0, 1.0]}, 186 | Vertex{pos: [w, -l, h], uv: [1.0, 0.0]}, 187 | ]; 188 | let indices = [0, 1, 2, 2, 3, 0]; 189 | let texture_data = fs::load("road.png").into_inner(); 190 | let texture = load_texture(context, &texture_data); 191 | Mesh::new(context, &vertices, &indices, texture) 192 | } 193 | 194 | pub fn get_marker>(context: &mut Context, tex_path: P) -> Mesh { 195 | let n = 0.2; 196 | let vertices = [ 197 | Vertex{pos: [-n, 0.0, 0.1], uv: [0.0, 0.0]}, 198 | Vertex{pos: [0.0, n * 1.4, 0.1], uv: [1.0, 0.0]}, 199 | Vertex{pos: [n, 0.0, 0.1], uv: [0.5, 0.5]}, 200 | ]; 201 | let indices = [0, 1, 2]; 202 | let texture_data = fs::load(tex_path).into_inner(); 203 | let texture = load_texture(context, &texture_data); 204 | Mesh::new(context, &vertices, &indices, texture) 205 | } 206 | 207 | pub fn get_one_tile_mesh(context: &mut Context, texture: Texture) -> Mesh { 208 | let mut vertices = Vec::new(); 209 | for dir in dirs() { 210 | let vertex = geom::index_to_hex_vertex(dir.to_int()); 211 | let uv = vertex.v.truncate() / (geom::HEX_EX_RADIUS * 2.0) 212 | + Vector2::from_value(0.5); 213 | vertices.push(Vertex { 214 | pos: vertex.v.into(), 215 | uv: uv.into(), 216 | }); 217 | } 218 | let indices = [ 219 | 0, 1, 2, 220 | 0, 2, 3, 221 | 0, 3, 5, 222 | 3, 4, 5, 223 | ]; 224 | Mesh::new(context, &vertices, &indices, texture) 225 | } 226 | -------------------------------------------------------------------------------- /core/src/map.rs: -------------------------------------------------------------------------------- 1 | use std::default::{Default}; 2 | use std::iter::{repeat}; 3 | use cgmath::{Vector2, Array}; 4 | use types::{Size2}; 5 | use dir::{Dir, DirIter, dirs}; 6 | use position::{MapPos}; 7 | 8 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] 9 | pub struct Distance{pub n: i32} 10 | 11 | #[derive(Clone, Copy, PartialEq, Eq, Debug)] 12 | pub enum Terrain { 13 | Plain, 14 | Trees, 15 | City, 16 | Water, 17 | } 18 | 19 | impl Default for Terrain { 20 | fn default() -> Terrain { Terrain::Plain } 21 | } 22 | 23 | #[derive(Clone, Debug)] 24 | pub struct Map { 25 | tiles: Vec, 26 | size: Size2, 27 | } 28 | 29 | impl Map { 30 | pub fn new(size: Size2) -> Map { 31 | let tiles_count = (size.w * size.h) as usize; 32 | let tiles = repeat(Default::default()).take(tiles_count).collect(); 33 | Map { 34 | tiles: tiles, 35 | size: size, 36 | } 37 | } 38 | 39 | pub fn size(&self) -> Size2 { 40 | self.size 41 | } 42 | 43 | pub fn tile_mut>(&mut self, pos: P) -> &mut T { 44 | let pos = pos.into(); 45 | assert!(self.is_inboard(pos)); 46 | let index = self.size.w * pos.v.y + pos.v.x; 47 | &mut self.tiles[index as usize] 48 | } 49 | 50 | pub fn tile>(&self, pos: P) -> &T { 51 | let pos = pos.into(); 52 | assert!(self.is_inboard(pos)); 53 | let index = self.size.w * pos.v.y + pos.v.x; 54 | &self.tiles[index as usize] 55 | } 56 | 57 | pub fn is_inboard>(&self, pos: P) -> bool { 58 | let pos = pos.into(); 59 | let x = pos.v.x; 60 | let y = pos.v.y; 61 | x >= 0 && y >= 0 && x < self.size.w && y < self.size.h 62 | } 63 | 64 | pub fn get_iter(&self) -> MapPosIter { 65 | MapPosIter::new(self.size()) 66 | } 67 | } 68 | 69 | #[derive(Clone, Debug)] 70 | pub struct MapPosIter { 71 | cursor: MapPos, 72 | map_size: Size2, 73 | } 74 | 75 | impl MapPosIter { 76 | fn new(map_size: Size2) -> MapPosIter { 77 | MapPosIter { 78 | cursor: MapPos{v: Vector2::from_value(0)}, 79 | map_size: map_size, 80 | } 81 | } 82 | } 83 | 84 | impl Iterator for MapPosIter { 85 | type Item = MapPos; 86 | 87 | fn next(&mut self) -> Option { 88 | let current_pos = if self.cursor.v.y >= self.map_size.h { 89 | None 90 | } else { 91 | Some(self.cursor) 92 | }; 93 | self.cursor.v.x += 1; 94 | if self.cursor.v.x >= self.map_size.w { 95 | self.cursor.v.x = 0; 96 | self.cursor.v.y += 1; 97 | } 98 | current_pos 99 | } 100 | } 101 | 102 | #[derive(Clone, Debug)] 103 | pub struct RingIter { 104 | cursor: MapPos, 105 | segment_index: i32, 106 | dir_iter: DirIter, 107 | radius: Distance, 108 | dir: Dir, 109 | } 110 | 111 | pub fn ring_iter(pos: MapPos, radius: Distance) -> RingIter { 112 | let mut pos = pos; 113 | pos.v.x -= radius.n; 114 | let mut dir_iter = dirs(); 115 | let dir = dir_iter.next() 116 | .expect("Can`t get first direction"); 117 | assert_eq!(dir, Dir::SouthEast); 118 | RingIter { 119 | cursor: pos, 120 | radius: radius, 121 | segment_index: 0, 122 | dir_iter: dir_iter, 123 | dir: dir, 124 | } 125 | } 126 | 127 | impl RingIter { 128 | fn simple_step(&mut self) -> Option { 129 | self.cursor = Dir::get_neighbour_pos( 130 | self.cursor, self.dir); 131 | self.segment_index += 1; 132 | Some(self.cursor) 133 | } 134 | 135 | fn rotate(&mut self, dir: Dir) -> Option { 136 | self.segment_index = 0; 137 | self.cursor = Dir::get_neighbour_pos(self.cursor, self.dir); 138 | self.dir = dir; 139 | Some(self.cursor) 140 | } 141 | } 142 | 143 | impl Iterator for RingIter { 144 | type Item = MapPos; 145 | 146 | fn next(&mut self) -> Option { 147 | if self.segment_index >= self.radius.n - 1 { 148 | if let Some(dir) = self.dir_iter.next() { 149 | self.rotate(dir) 150 | } else if self.segment_index == self.radius.n { 151 | None 152 | } else { 153 | // last pos 154 | self.simple_step() 155 | } 156 | } else { 157 | self.simple_step() 158 | } 159 | } 160 | } 161 | 162 | #[derive(Clone, Debug)] 163 | pub struct SpiralIter { 164 | ring_iter: RingIter, 165 | radius: Distance, 166 | last_radius: Distance, 167 | origin: MapPos, 168 | } 169 | 170 | pub fn spiral_iter(pos: MapPos, radius: Distance) -> SpiralIter { 171 | assert!(radius.n >= 1); 172 | SpiralIter { 173 | ring_iter: ring_iter(pos, Distance{n: 1}), 174 | radius: Distance{n: 1}, 175 | last_radius: radius, 176 | origin: pos, 177 | } 178 | } 179 | 180 | impl Iterator for SpiralIter { 181 | type Item = MapPos; 182 | 183 | fn next(&mut self) -> Option { 184 | let pos = self.ring_iter.next(); 185 | if pos.is_some() { 186 | pos 187 | } else { 188 | self.radius.n += 1; 189 | if self.radius > self.last_radius { 190 | None 191 | } else { 192 | self.ring_iter = ring_iter( 193 | self.origin, self.radius); 194 | self.ring_iter.next() 195 | } 196 | } 197 | } 198 | } 199 | 200 | pub fn distance(from: MapPos, to: MapPos) -> Distance { 201 | let to = to.v; 202 | let from = from.v; 203 | let dx = (to.x + to.y / 2) - (from.x + from.y / 2); 204 | let dy = to.y - from.y; 205 | Distance{n: (dx.abs() + dy.abs() + (dx - dy).abs()) / 2} 206 | } 207 | 208 | #[cfg(test)] 209 | mod tests { 210 | use cgmath::{Vector2}; 211 | use map::{MapPos, Distance, ring_iter, spiral_iter}; 212 | 213 | #[test] 214 | fn test_ring_1() { 215 | let radius = Distance{n: 1}; 216 | let start_pos = MapPos{v: Vector2{x: 0, y: 0}}; 217 | let expected = [ 218 | (0, -1), (1, -1), (1, 0), (1, 1), (0, 1), (-1, 0) ]; 219 | let mut expected = expected.iter(); 220 | for p in ring_iter(start_pos, radius) { 221 | let expected = expected.next().expect( 222 | "Can not get next element from expected vector"); 223 | assert_eq!(*expected, (p.v.x, p.v.y)); 224 | } 225 | assert!(expected.next().is_none()); 226 | } 227 | 228 | #[test] 229 | fn test_ring_2() { 230 | let radius = Distance{n: 2}; 231 | let start_pos = MapPos{v: Vector2{x: 0, y: 0}}; 232 | let expected = [ 233 | (-1, -1), 234 | (-1, -2), 235 | (0, -2), 236 | (1, -2), 237 | (2, -1), 238 | (2, 0), 239 | (2, 1), 240 | (1, 2), 241 | (0, 2), 242 | (-1, 2), 243 | (-1, 1), 244 | (-2, 0), 245 | ]; 246 | let mut expected = expected.iter(); 247 | for p in ring_iter(start_pos, radius) { 248 | let expected = expected.next().expect( 249 | "Can not get next element from expected vector"); 250 | assert_eq!(*expected, (p.v.x, p.v.y)); 251 | } 252 | assert!(expected.next().is_none()); 253 | } 254 | 255 | #[test] 256 | fn test_spiral_1() { 257 | let radius = Distance{n: 2}; 258 | let start_pos = MapPos{v: Vector2{x: 0, y: 0}}; 259 | let expected = [ 260 | // ring 1 261 | (0, -1), 262 | (1, -1), 263 | (1, 0), 264 | (1, 1), 265 | (0, 1), 266 | (-1, 0), 267 | // ring 2 268 | (-1, -1), 269 | (-1, -2), 270 | (0, -2), 271 | (1, -2), 272 | (2, -1), 273 | (2, 0), 274 | (2, 1), 275 | (1, 2), 276 | (0, 2), 277 | (-1, 2), 278 | (-1, 1), 279 | (-2, 0), 280 | ]; 281 | let mut expected = expected.iter(); 282 | for p in spiral_iter(start_pos, radius) { 283 | let expected = expected.next().expect( 284 | "Can not get next element from expected vector"); 285 | assert_eq!(*expected, (p.v.x, p.v.y)); 286 | } 287 | assert!(expected.next().is_none()); 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /core/src/position.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt}; 2 | use std::collections::{HashMap}; 3 | use cgmath::{Vector2}; 4 | use dir::{Dir}; 5 | use game_state::{State, ObjectsAtIter}; 6 | use map::{Map, Terrain}; 7 | use unit::{self, UnitId, Unit, UnitType}; 8 | use object::{Object, ObjectId, ObjectClass}; 9 | use player::{PlayerId}; 10 | 11 | #[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)] 12 | pub struct MapPos{pub v: Vector2} 13 | 14 | impl fmt::Display for MapPos { 15 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 16 | write!(f, "MapPos({}, {})", self.v.x, self.v.y) 17 | } 18 | } 19 | 20 | #[derive(PartialEq, Clone, Copy, Debug)] 21 | pub enum SlotId { 22 | Id(u8), 23 | WholeTile, 24 | TwoTiles(Dir), 25 | Air, 26 | } 27 | 28 | #[derive(PartialEq, Clone, Copy, Debug)] 29 | pub struct ExactPos { 30 | pub map_pos: MapPos, 31 | pub slot_id: SlotId, 32 | } 33 | 34 | #[derive(Clone, Copy, Debug)] 35 | pub struct ExactPosIter { 36 | p: ExactPos, 37 | i: u8, 38 | } 39 | 40 | impl ExactPos { 41 | pub fn map_pos_iter(self) -> ExactPosIter { 42 | ExactPosIter { 43 | p: self, 44 | i: 0, 45 | } 46 | } 47 | } 48 | 49 | impl Iterator for ExactPosIter { 50 | type Item = MapPos; 51 | 52 | fn next(&mut self) -> Option { 53 | let next_pos = match self.p.slot_id { 54 | SlotId::Air | SlotId::Id(_) | SlotId::WholeTile => { 55 | if self.i == 0 { 56 | Some(self.p.map_pos) 57 | } else { 58 | None 59 | } 60 | } 61 | SlotId::TwoTiles(dir) => { 62 | if self.i == 0 { 63 | Some(self.p.map_pos) 64 | } else if self.i == 1 { 65 | Some(Dir::get_neighbour_pos(self.p.map_pos, dir)) 66 | } else { 67 | None 68 | } 69 | } 70 | }; 71 | self.i += 1; 72 | next_pos 73 | } 74 | } 75 | 76 | // TODO: return iterator? 77 | impl From for MapPos { 78 | fn from(pos: ExactPos) -> MapPos { 79 | pos.map_pos 80 | } 81 | } 82 | 83 | pub fn is_unit_in_object(unit: &Unit, object: &Object) -> bool { 84 | if unit.pos == object.pos { 85 | return true; 86 | } 87 | let is_object_big = object.pos.slot_id == SlotId::WholeTile; 88 | is_object_big && unit.pos.map_pos == object.pos.map_pos 89 | } 90 | 91 | // TODO: simplify/optimize 92 | pub fn find_next_player_unit_id( 93 | state: &State, 94 | player_id: PlayerId, 95 | unit_id: UnitId, 96 | ) -> UnitId { 97 | let mut i = state.units().cycle().filter( 98 | |&(_, unit)| unit::is_commandable(player_id, unit)); 99 | while let Some((&id, _)) = i.next() { 100 | if id == unit_id { 101 | let (&id, _) = i.next().unwrap(); 102 | return id; 103 | } 104 | } 105 | unreachable!() 106 | } 107 | 108 | // TODO: simplify/optimize 109 | pub fn find_prev_player_unit_id( 110 | state: &State, 111 | player_id: PlayerId, 112 | unit_id: UnitId, 113 | ) -> UnitId { 114 | let mut i = state.units().cycle().filter( 115 | |&(_, unit)| unit::is_commandable(player_id, unit)).peekable(); 116 | while let Some((&id, _)) = i.next() { 117 | let &(&next_id, _) = i.peek().unwrap(); 118 | if next_id == unit_id { 119 | return id; 120 | } 121 | } 122 | unreachable!() 123 | } 124 | 125 | pub fn get_unit_ids_at(state: &State, pos: MapPos) -> Vec { 126 | let mut ids = Vec::new(); 127 | for unit in state.units_at(pos) { 128 | if !unit::is_loaded_or_attached(unit) { 129 | ids.push(unit.id) 130 | } 131 | } 132 | ids 133 | } 134 | 135 | pub fn objects_at(objects: &HashMap, pos: MapPos) -> ObjectsAtIter { 136 | ObjectsAtIter::new(objects, pos) 137 | } 138 | 139 | pub fn get_free_slot_for_building( 140 | map: &Map, 141 | objects: &HashMap, 142 | pos: MapPos, 143 | ) -> Option { 144 | let mut slots = [false, false, false]; 145 | for object in objects_at(objects, pos) { 146 | if let SlotId::Id(slot_id) = object.pos.slot_id { 147 | slots[slot_id as usize] = true; 148 | } else { 149 | return None; 150 | } 151 | } 152 | let slots_count = get_slots_count(map, pos) as usize; 153 | for (i, slot) in slots.iter().enumerate().take(slots_count) { 154 | if !slot { 155 | return Some(SlotId::Id(i as u8)); 156 | } 157 | } 158 | None 159 | } 160 | 161 | pub fn get_free_exact_pos( 162 | state: &State, 163 | unit_type: &UnitType, 164 | pos: MapPos, 165 | ) -> Option { 166 | let slot_ids = [ 167 | SlotId::Id(0), 168 | SlotId::Id(1), 169 | SlotId::Id(2), 170 | SlotId::WholeTile, 171 | SlotId::Air, 172 | ]; 173 | for &slot_id in &slot_ids { 174 | let exact_pos = ExactPos{map_pos: pos, slot_id: slot_id}; 175 | if can_place_unit(state, unit_type, exact_pos) { 176 | return Some(exact_pos); 177 | } 178 | } 179 | None 180 | } 181 | 182 | pub fn get_slots_count(map: &Map, pos: MapPos) -> i32 { 183 | match *map.tile(pos) { 184 | Terrain::Water => 1, 185 | Terrain::City | 186 | Terrain::Plain | 187 | Terrain::Trees => 3, 188 | } 189 | } 190 | 191 | fn can_place_air_unit( 192 | state: &State, 193 | unit_type: &UnitType, 194 | pos: ExactPos, 195 | ) -> bool { 196 | assert!(unit_type.is_air); 197 | if pos.slot_id != SlotId::Air { 198 | return false; 199 | } 200 | for unit in state.units_at(pos.map_pos) { 201 | if unit.pos.slot_id == SlotId::Air { 202 | return false; 203 | } 204 | } 205 | true 206 | } 207 | 208 | fn can_place_big_ground_unit( 209 | state: &State, 210 | unit_type: &UnitType, 211 | pos: ExactPos, 212 | ) -> bool { 213 | // TODO: forbid placing on bridge tile 214 | assert!(unit_type.is_big); 215 | if pos.slot_id != SlotId::WholeTile { 216 | return false; 217 | } 218 | for object in state.objects_at(pos.map_pos) { 219 | if object.class == ObjectClass::Building { 220 | return false; 221 | } 222 | } 223 | // check if there're any other ground units 224 | for unit in state.units_at(pos.map_pos) { 225 | if unit.pos.slot_id != SlotId::Air { 226 | return false; 227 | } 228 | } 229 | true 230 | } 231 | 232 | fn can_place_small_ground_vehicle_unit( 233 | state: &State, 234 | unit_type: &UnitType, 235 | pos: ExactPos, 236 | ) -> bool { 237 | assert!(!unit_type.is_infantry); 238 | // TODO: add assert that it's actually a small ground vehicle 239 | let objects_at = state.objects_at(pos.map_pos); 240 | for object in objects_at { 241 | match object.pos.slot_id { 242 | SlotId::Id(_) => { 243 | if object.pos == pos { 244 | return false; 245 | } 246 | }, 247 | SlotId::WholeTile => { 248 | if object.class == ObjectClass::Building { 249 | return false; 250 | } 251 | } 252 | SlotId::TwoTiles(_) | SlotId::Air => {}, 253 | } 254 | } 255 | true 256 | } 257 | 258 | fn can_place_small_ground_unit( 259 | state: &State, 260 | unit_type: &UnitType, 261 | pos: ExactPos, 262 | ) -> bool { 263 | match pos.slot_id { 264 | SlotId::Id(_) => {}, 265 | _ => return false, // TODO: convert this match to assert? 266 | } 267 | let slots_count = get_slots_count(state.map(), pos.map_pos); 268 | let units_at = state.units_at(pos.map_pos); 269 | let ground_units_count = units_at.clone() 270 | .filter(|unit| unit.pos.slot_id != SlotId::Air) 271 | .count(); 272 | if slots_count == 1 && ground_units_count > 0 { 273 | return false; 274 | } 275 | if !unit_type.is_infantry 276 | && !can_place_small_ground_vehicle_unit(state, unit_type, pos) 277 | { 278 | return false; 279 | } 280 | for unit in units_at { 281 | if unit.pos == pos || unit.pos.slot_id == SlotId::WholeTile { 282 | return false; 283 | } 284 | } 285 | true 286 | } 287 | 288 | fn can_place_ground_unit( 289 | state: &State, 290 | unit_type: &UnitType, 291 | pos: ExactPos, 292 | ) -> bool { 293 | // TODO: forbid placing on water tiles without bridge 294 | // TODO: check max move points 295 | if pos.slot_id == SlotId::Air { 296 | return false; 297 | } 298 | if unit_type.is_big { 299 | can_place_big_ground_unit(state, unit_type, pos) 300 | } else { 301 | can_place_small_ground_unit(state, unit_type, pos) 302 | } 303 | } 304 | 305 | pub fn can_place_unit( 306 | state: &State, 307 | unit_type: &UnitType, 308 | pos: ExactPos, 309 | ) -> bool { 310 | if unit_type.is_air { 311 | can_place_air_unit(state, unit_type, pos) 312 | } else { 313 | can_place_ground_unit(state, unit_type, pos) 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /core/src/ai.rs: -------------------------------------------------------------------------------- 1 | use std::rc::{Rc}; 2 | use rand::{thread_rng, Rng}; 3 | use game_state::{State}; 4 | use map::{distance}; 5 | use movement::{self, MovePoints, Pathfinder, path_cost, truncate_path}; 6 | use dir::{Dir, dirs}; 7 | use unit::{Unit, UnitTypeId}; 8 | use db::{Db}; 9 | use misc::{get_shuffled_indices}; 10 | use check::{check_command}; 11 | use position::{ExactPos, MapPos, get_free_exact_pos}; 12 | use object::{ObjectClass, Object}; 13 | use event::{CoreEvent, Command, MoveMode}; 14 | use player::{PlayerId}; 15 | use options::{Options}; 16 | 17 | #[derive(Clone, Debug)] 18 | pub struct Ai { 19 | id: PlayerId, 20 | state: State, 21 | pathfinder: Pathfinder, 22 | db: Rc, 23 | } 24 | 25 | impl Ai { 26 | pub fn new(db: Rc, options: &Options, id: PlayerId) -> Ai { 27 | let state = State::new_partial(db.clone(), options, id); 28 | let map_size = state.map().size(); 29 | Ai { 30 | id: id, 31 | state: state, 32 | pathfinder: Pathfinder::new(db.clone(), map_size), 33 | db: db, 34 | } 35 | } 36 | 37 | pub fn apply_event(&mut self, event: &CoreEvent) { 38 | self.state.apply_event(event); 39 | } 40 | 41 | fn get_best_pos(&self, unit: &Unit) -> Option { 42 | let mut best_pos = None; 43 | let mut best_cost = movement::max_cost(); 44 | for (_, enemy) in self.state.units() { 45 | if enemy.player_id == self.id || !enemy.is_alive { 46 | continue; 47 | } 48 | for dir in dirs() { 49 | let pos = Dir::get_neighbour_pos(enemy.pos.map_pos, dir); 50 | if !self.state.map().is_inboard(pos) { 51 | continue; 52 | } 53 | if let Some((cost, pos)) = self.estimate_path(unit, pos) { 54 | if best_cost.n > cost.n { 55 | best_cost = cost; 56 | best_pos = Some(pos); 57 | } 58 | } 59 | } 60 | } 61 | for sector in self.state.sectors().values() { 62 | if sector.owner_id == Some(self.id) { 63 | continue; 64 | } 65 | for &pos in §or.positions { 66 | if unit.pos.map_pos == pos { 67 | return None; 68 | } 69 | if let Some((cost, pos)) = self.estimate_path(unit, pos) { 70 | if best_cost.n > cost.n { 71 | best_cost = cost; 72 | best_pos = Some(pos); 73 | } 74 | } 75 | } 76 | } 77 | best_pos 78 | } 79 | 80 | fn estimate_path( 81 | &self, 82 | unit: &Unit, 83 | destination: MapPos, 84 | ) -> Option<(MovePoints, ExactPos)> { 85 | let exact_destination = match get_free_exact_pos( 86 | &self.state, 87 | self.db.unit_type(unit.type_id), 88 | destination, 89 | ) { 90 | Some(pos) => pos, 91 | None => return None, 92 | }; 93 | let path = match self.pathfinder.get_path(exact_destination) { 94 | Some(path) => path, 95 | None => return None, 96 | }; 97 | let cost = path_cost(&self.db, &self.state, unit, &path); 98 | Some((cost, exact_destination)) 99 | } 100 | 101 | fn is_close_to_enemies(&self, unit: &Unit) -> bool { 102 | for (_, target) in self.state.units() { 103 | if target.player_id == self.id { 104 | continue; 105 | } 106 | let target_type = &self.db.unit_type(target.type_id); 107 | let attacker_type = &self.db.unit_type(unit.type_id); 108 | let weapon_type = &self.db.weapon_type(attacker_type.weapon_type_id); 109 | let distance = distance(unit.pos.map_pos, target.pos.map_pos); 110 | let max_distance = if target_type.is_air { 111 | match weapon_type.max_air_distance { 112 | Some(max_air_distance) => max_air_distance, 113 | None => continue, // can not attack air unit, skipping. 114 | } 115 | } else { 116 | weapon_type.max_distance 117 | }; 118 | if distance <= max_distance { 119 | return true; 120 | } 121 | } 122 | false 123 | } 124 | 125 | pub fn try_get_attack_command(&self) -> Option { 126 | for (_, unit) in self.state.units() { 127 | if unit.player_id != self.id { 128 | continue; 129 | } 130 | if unit.attack_points.unwrap().n <= 0 { 131 | continue; 132 | } 133 | for (_, target) in self.state.units() { 134 | if target.player_id == self.id { 135 | continue; 136 | } 137 | let command = Command::AttackUnit { 138 | attacker_id: unit.id, 139 | defender_id: target.id, 140 | }; 141 | if check_command(&self.db, self.id, &self.state, &command).is_ok() { 142 | return Some(command); 143 | } 144 | } 145 | } 146 | None 147 | } 148 | 149 | pub fn try_get_move_command(&mut self) -> Option { 150 | for (_, unit) in self.state.units() { 151 | if unit.player_id != self.id { 152 | continue; 153 | } 154 | if self.is_close_to_enemies(unit) { 155 | continue; 156 | } 157 | self.pathfinder.fill_map(&self.state, unit); 158 | let destination = match self.get_best_pos(unit) { 159 | Some(destination) => destination, 160 | None => continue, 161 | }; 162 | let path = match self.pathfinder.get_path(destination) { 163 | Some(path) => path, 164 | None => continue, 165 | }; 166 | let path = match truncate_path(&self.db, &self.state, &path, unit) { 167 | Some(path) => path, 168 | None => continue, 169 | }; 170 | let cost = path_cost(&self.db, &self.state, unit, &path); 171 | let move_points = unit.move_points.unwrap(); 172 | if move_points.n < cost.n { 173 | continue; 174 | } 175 | let command = Command::Move { 176 | unit_id: unit.id, 177 | path: path, 178 | mode: MoveMode::Fast, 179 | }; 180 | if check_command(&self.db, self.id, &self.state, &command).is_err() { 181 | continue; 182 | } 183 | return Some(command); 184 | } 185 | None 186 | } 187 | 188 | fn get_shuffled_reinforcement_sectors(&self, player_id: PlayerId) -> Vec<&Object> { 189 | let mut reinforcement_sectors = Vec::new(); 190 | for object in self.state.objects().values() { 191 | let owner_id = match object.owner_id { 192 | Some(id) => id, 193 | None => continue, 194 | }; 195 | if owner_id != player_id { 196 | continue; 197 | } 198 | if object.class != ObjectClass::ReinforcementSector { 199 | continue; 200 | } 201 | reinforcement_sectors.push(object); 202 | } 203 | thread_rng().shuffle(&mut reinforcement_sectors); 204 | reinforcement_sectors 205 | } 206 | 207 | pub fn try_get_create_unit_command(&self) -> Option { 208 | let reinforcement_sectors = self.get_shuffled_reinforcement_sectors(self.id); 209 | let reinforcement_points = self.state.reinforcement_points()[&self.id]; 210 | for type_index in get_shuffled_indices(self.db.unit_types()) { 211 | let unit_type_id = UnitTypeId{id: type_index as i32}; 212 | let unit_type = self.db.unit_type(unit_type_id); 213 | if unit_type.cost > reinforcement_points { 214 | continue; 215 | } 216 | for sector in &reinforcement_sectors { 217 | let exact_pos = match get_free_exact_pos( 218 | &self.state, 219 | unit_type, 220 | sector.pos.map_pos, 221 | ) { 222 | Some(pos) => pos, 223 | None => continue, 224 | }; 225 | let command = Command::CreateUnit { 226 | type_id: unit_type_id, 227 | pos: exact_pos, 228 | }; 229 | if check_command(&self.db, self.id, &self.state, &command).is_err() { 230 | continue; 231 | } 232 | return Some(command); 233 | } 234 | } 235 | None 236 | } 237 | 238 | pub fn get_command(&mut self) -> Command { 239 | if let Some(cmd) = self.try_get_attack_command() { 240 | cmd 241 | } else if let Some(cmd) = self.try_get_move_command() { 242 | cmd 243 | } else if let Some(cmd) = self.try_get_create_unit_command() { 244 | cmd 245 | } else { 246 | Command::EndTurn 247 | } 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /core/src/movement.rs: -------------------------------------------------------------------------------- 1 | use std::default::{Default}; 2 | use std::rc::{Rc}; 3 | use types::{Size2}; 4 | use db::{Db}; 5 | use unit::{Unit}; 6 | use map::{Map, Terrain}; 7 | use game_state::{State}; 8 | use dir::{Dir, dirs}; 9 | use position::{ExactPos, SlotId, get_free_exact_pos}; 10 | use object::{ObjectClass}; 11 | use event::{MoveMode}; 12 | 13 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] 14 | pub struct MovePoints{pub n: i32} 15 | 16 | pub fn move_cost_modifier(mode: MoveMode) -> i32 { 17 | match mode { 18 | MoveMode::Fast => 1, 19 | MoveMode::Hunt => 2, 20 | } 21 | } 22 | 23 | #[derive(Clone, Debug)] 24 | pub struct Tile { 25 | cost: MovePoints, 26 | parent: Option, 27 | slot_id: SlotId, 28 | } 29 | 30 | impl Tile { 31 | pub fn parent(&self) -> Option { self.parent } 32 | pub fn cost(&self) -> MovePoints { self.cost } 33 | pub fn slot_id(&self) -> SlotId { self.slot_id } 34 | } 35 | 36 | impl Default for Tile { 37 | fn default() -> Tile { 38 | Tile { 39 | cost: MovePoints{n: 0}, 40 | parent: None, 41 | slot_id: SlotId::WholeTile, 42 | } 43 | } 44 | } 45 | 46 | pub fn truncate_path(db: &Db, state: &State, path: &[ExactPos], unit: &Unit) -> Option> { 47 | let mut new_path = Vec::new(); 48 | let mut cost = MovePoints{n: 0}; 49 | new_path.push(path[0]); 50 | let move_points = unit.move_points.unwrap(); 51 | for window in path.windows(2) { 52 | let from = window[0]; 53 | let to = window[1]; 54 | cost.n += tile_cost(db, state, unit, from, to).n; 55 | if cost.n > move_points.n { 56 | break; 57 | } 58 | new_path.push(to); 59 | } 60 | if new_path.len() < 2 { 61 | None 62 | } else { 63 | Some(new_path) 64 | } 65 | } 66 | 67 | pub fn path_cost(db: &Db, state: &State, unit: &Unit, path: &[ExactPos]) 68 | -> MovePoints 69 | { 70 | let mut cost = MovePoints{n: 0}; 71 | for window in path.windows(2) { 72 | let from = window[0]; 73 | let to = window[1]; 74 | cost.n += tile_cost(db, state, unit, from, to).n; 75 | } 76 | cost 77 | } 78 | 79 | // TODO: const (see https://github.com/rust-lang/rust/issues/24111 ) 80 | pub fn max_cost() -> MovePoints { 81 | MovePoints{n: i32::max_value()} 82 | } 83 | 84 | // TODO: increase cost for attached units 85 | pub fn tile_cost(db: &Db, state: &State, unit: &Unit, from: ExactPos, pos: ExactPos) 86 | -> MovePoints 87 | { 88 | let map_pos = pos.map_pos; 89 | let objects_at = state.objects_at(map_pos); 90 | let units_at = state.units_at(map_pos); 91 | let mut unit_cost = 0; 92 | let mut object_cost = 0; 93 | let unit_type = db.unit_type(unit.type_id); 94 | if unit_type.is_air { 95 | return MovePoints{n: 2}; 96 | } 97 | 'unit_loop: for unit in units_at { 98 | for object in objects_at.clone() { 99 | match object.pos.slot_id { 100 | SlotId::Id(_) => if unit.pos == object.pos { 101 | assert!(db.unit_type(unit.type_id).is_infantry); 102 | break 'unit_loop; 103 | }, 104 | SlotId::TwoTiles(_) | SlotId::WholeTile => { 105 | break 'unit_loop; 106 | }, 107 | SlotId::Air => {}, 108 | } 109 | } 110 | unit_cost += 1; 111 | } 112 | let tile = state.map().tile(pos); 113 | let mut terrain_cost = if unit_type.is_infantry { 114 | match *tile { 115 | Terrain::Plain | Terrain::City => 4, 116 | Terrain::Trees => 5, 117 | Terrain::Water => 99, 118 | } 119 | } else { 120 | match *tile { 121 | Terrain::Plain | Terrain::City => 4, 122 | Terrain::Trees => 8, 123 | Terrain::Water => 99, 124 | } 125 | }; 126 | for object in objects_at.clone() { 127 | if object.class != ObjectClass::Road { 128 | continue; 129 | } 130 | let mut i = object.pos.map_pos_iter(); 131 | let road_from = i.next().unwrap(); 132 | let road_to = i.next().unwrap(); 133 | assert!(road_from != road_to); 134 | let is_road_pos_ok = road_from == from.map_pos && road_to == pos.map_pos; 135 | let is_road_pos_rev_ok = road_to == from.map_pos && road_from == pos.map_pos; 136 | if (is_road_pos_ok || is_road_pos_rev_ok) && !unit_type.is_big { 137 | // TODO: ultrahardcoded value :( 138 | terrain_cost = if unit_type.is_infantry { 4 } else { 2 }; 139 | } 140 | } 141 | for object in objects_at { 142 | let cost = if unit_type.is_infantry { 143 | match object.class { 144 | ObjectClass::Building => 1, 145 | ObjectClass::ReinforcementSector | 146 | ObjectClass::Road | 147 | ObjectClass::Smoke => 0, 148 | } 149 | } else { 150 | match object.class { 151 | ObjectClass::Building => 2, 152 | ObjectClass::ReinforcementSector | 153 | ObjectClass::Road | 154 | ObjectClass::Smoke => 0, 155 | } 156 | }; 157 | object_cost += cost; 158 | } 159 | MovePoints{n: terrain_cost + object_cost + unit_cost} 160 | } 161 | 162 | #[derive(Clone, Debug)] 163 | pub struct Pathfinder { 164 | queue: Vec, 165 | map: Map, 166 | db: Rc, 167 | } 168 | 169 | impl Pathfinder { 170 | pub fn new(db: Rc, map_size: Size2) -> Pathfinder { 171 | Pathfinder { 172 | queue: Vec::new(), 173 | map: Map::new(map_size), 174 | db: db, 175 | } 176 | } 177 | 178 | pub fn get_map(&self) -> &Map { 179 | &self.map 180 | } 181 | 182 | fn process_neighbour_pos( 183 | &mut self, 184 | state: &State, 185 | unit: &Unit, 186 | original_pos: ExactPos, 187 | neighbour_pos: ExactPos 188 | ) { 189 | let old_cost = self.map.tile(original_pos).cost; 190 | let tile_cost = tile_cost(&self.db, state, unit, original_pos, neighbour_pos); 191 | let tile = self.map.tile_mut(neighbour_pos); 192 | let new_cost = MovePoints{n: old_cost.n + tile_cost.n}; 193 | if tile.cost.n > new_cost.n { 194 | tile.cost = new_cost; 195 | tile.parent = Some(Dir::get_dir_from_to( 196 | neighbour_pos.map_pos, original_pos.map_pos)); 197 | tile.slot_id = neighbour_pos.slot_id; 198 | self.queue.push(neighbour_pos); 199 | } 200 | } 201 | 202 | fn clean_map(&mut self) { 203 | for pos in self.map.get_iter() { 204 | let tile = self.map.tile_mut(pos); 205 | tile.cost = max_cost(); 206 | tile.parent = None; 207 | tile.slot_id = SlotId::WholeTile; 208 | } 209 | } 210 | 211 | fn try_to_push_neighbours( 212 | &mut self, 213 | state: &State, 214 | unit: &Unit, 215 | pos: ExactPos, 216 | ) { 217 | assert!(self.map.is_inboard(pos)); 218 | for dir in dirs() { 219 | let neighbour_pos = Dir::get_neighbour_pos(pos.map_pos, dir); 220 | if self.map.is_inboard(neighbour_pos) { 221 | let exact_neighbour_pos = match get_free_exact_pos( 222 | state, self.db.unit_type(unit.type_id), neighbour_pos 223 | ) { 224 | Some(pos) => pos, 225 | None => continue, 226 | }; 227 | self.process_neighbour_pos( 228 | state, unit, pos, exact_neighbour_pos); 229 | } 230 | } 231 | } 232 | 233 | fn push_start_pos_to_queue(&mut self, start_pos: ExactPos) { 234 | let start_tile = self.map.tile_mut(start_pos); 235 | start_tile.cost = MovePoints{n: 0}; 236 | start_tile.parent = None; 237 | start_tile.slot_id = start_pos.slot_id; 238 | self.queue.push(start_pos); 239 | } 240 | 241 | pub fn fill_map(&mut self, state: &State, unit: &Unit) { 242 | assert!(self.queue.len() == 0); 243 | self.clean_map(); 244 | self.push_start_pos_to_queue(unit.pos); 245 | while !self.queue.is_empty() { 246 | let pos = self.queue.remove(0); 247 | self.try_to_push_neighbours(state, unit, pos); 248 | } 249 | } 250 | 251 | /* 252 | pub fn is_reachable(&self, pos: ExactPos) -> bool { 253 | self.map.tile(pos).cost.n != max_cost().n 254 | } 255 | */ 256 | 257 | pub fn get_path(&self, destination: ExactPos) -> Option> { 258 | let mut path = vec![destination]; 259 | let mut pos = destination; 260 | if self.map.tile(pos).cost.n == max_cost().n { 261 | return None; 262 | } 263 | while self.map.tile(pos).cost.n != 0 { 264 | assert!(self.map.is_inboard(pos)); 265 | let parent_dir = match self.map.tile(pos).parent() { 266 | Some(dir) => dir, 267 | None => return None, 268 | }; 269 | let neighbour_map_pos = Dir::get_neighbour_pos(pos.map_pos, parent_dir); 270 | pos = ExactPos { 271 | map_pos: neighbour_map_pos, 272 | slot_id: self.map.tile(neighbour_map_pos).slot_id, 273 | }; 274 | path.push(pos); 275 | } 276 | path.reverse(); 277 | if path.is_empty() { 278 | None 279 | } else { 280 | Some(path) 281 | } 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /src/context.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc::{Sender}; 2 | use std::time; 3 | use cgmath::{Vector2, Matrix4, SquareMatrix, Array}; 4 | use glutin::{self, ContextTrait, Api, WindowEvent, MouseButton, GlRequest}; 5 | use glutin::ElementState::{Pressed, Released}; 6 | use rusttype; 7 | use gfx::traits::{FactoryExt, Device}; 8 | use gfx::handle::{Program}; 9 | use gfx; 10 | use gfx_gl; 11 | use gfx_glutin; 12 | use screen::{ScreenCommand}; 13 | use types::{Size2, ScreenPos, Time}; 14 | use texture::{load_texture_raw}; 15 | use pipeline::{pipe}; 16 | use fs; 17 | use mesh::{Mesh}; 18 | 19 | fn duration_to_time(duration: time::Duration) -> Time { 20 | let seconds = duration.as_secs() as f32; 21 | let nanoseconds = duration.subsec_nanos() as f32; 22 | Time{n: seconds + nanoseconds / 1_000_000_000.0} 23 | } 24 | 25 | fn shader_version_string(api: Api) -> String { 26 | match api { 27 | Api::OpenGl => "#version 120\n".into(), 28 | Api::OpenGlEs | Api::WebGl => "#version 100\n".into(), 29 | } 30 | } 31 | 32 | fn vertex_shader(api: Api) -> String { 33 | shader_version_string(api) + &fs::load_as_string("shader/v.glsl") 34 | } 35 | 36 | fn fragment_shader(api: Api) -> String { 37 | let mut text = shader_version_string(api); 38 | if api == Api::OpenGlEs || api == Api::WebGl { 39 | text += "precision mediump float;\n"; 40 | } 41 | text + &fs::load_as_string("shader/f.glsl") 42 | } 43 | 44 | fn new_shader( 45 | context: &glutin::Context, 46 | factory: &mut gfx_gl::Factory, 47 | ) -> Program { 48 | let api = context.get_api(); 49 | factory.link_program( 50 | vertex_shader(api).as_bytes(), 51 | fragment_shader(api).as_bytes(), 52 | ).unwrap() 53 | } 54 | 55 | fn new_pso( 56 | factory: &mut gfx_gl::Factory, 57 | program: &Program, 58 | primitive: gfx::Primitive, 59 | ) -> gfx::PipelineState { 60 | let rasterizer = gfx::state::Rasterizer::new_fill(); 61 | let pso = factory.create_pipeline_from_program( 62 | program, primitive, rasterizer, pipe::new()); 63 | pso.unwrap() 64 | } 65 | 66 | // TODO: read font name from config 67 | fn new_font() -> rusttype::Font<'static> { 68 | let font_data = fs::load("DroidSerif-Regular.ttf").into_inner(); 69 | let collection = rusttype::FontCollection::from_bytes(font_data); 70 | collection.into_font().unwrap() 71 | } 72 | 73 | fn get_win_size(window: &glutin::Window) -> Size2 { 74 | let size = window.get_inner_size().expect("Can`t get window size"); 75 | Size2{w: size.width as i32, h: size.height as i32} 76 | } 77 | 78 | #[derive(Clone, Debug)] 79 | pub struct MouseState { 80 | pub is_left_button_pressed: bool, 81 | pub is_right_button_pressed: bool, 82 | pub last_press_pos: ScreenPos, 83 | pub pos: ScreenPos, 84 | } 85 | 86 | // TODO: use gfx-rs generics, not gfx_gl types 87 | pub struct Context { 88 | win_size: Size2, 89 | mouse: MouseState, 90 | should_close: bool, 91 | commands_tx: Sender, 92 | window: glutin::WindowedContext, 93 | clear_color: [f32; 4], 94 | device: gfx_gl::Device, 95 | encoder: gfx::Encoder, 96 | pso: gfx::PipelineState, 97 | pso_wire: gfx::PipelineState, 98 | factory: gfx_gl::Factory, 99 | font: rusttype::Font<'static>, 100 | data: pipe::Data, 101 | start_time: time::Instant, 102 | events_loop: glutin::EventsLoop, 103 | } 104 | 105 | impl Context { 106 | pub fn new(tx: Sender) -> Context { 107 | let gl_version = GlRequest::GlThenGles { 108 | opengles_version: (2, 0), 109 | opengl_version: (2, 1), 110 | }; 111 | let window_builder = glutin::WindowBuilder::new() 112 | .with_title("Zone of Control".to_string()); 113 | let context_builder = glutin::ContextBuilder::new() 114 | .with_gl(gl_version) 115 | .with_pixel_format(24, 8); 116 | let events_loop = glutin::EventsLoop::new(); 117 | let (window, device, mut factory, main_color, main_depth) 118 | = gfx_glutin::init(window_builder, context_builder, &events_loop).unwrap(); 119 | let encoder = factory.create_command_buffer().into(); 120 | let program = new_shader(window.context(), &mut factory); 121 | let pso = new_pso(&mut factory, &program, gfx::Primitive::TriangleList); 122 | let pso_wire = new_pso(&mut factory, &program, gfx::Primitive::LineList); 123 | let sampler = factory.create_sampler_linear(); 124 | let win_size = get_win_size(&window); 125 | // fake mesh for pipeline initialization 126 | let vb = factory.create_vertex_buffer(&[]); 127 | let fake_texture = load_texture_raw(&mut factory, Size2{w: 2, h: 2}, &[0; 4]); 128 | let data = pipe::Data { 129 | basic_color: [1.0, 1.0, 1.0, 1.0], 130 | vbuf: vb, 131 | texture: (fake_texture, sampler), 132 | out: main_color, 133 | out_depth: main_depth, 134 | mvp: Matrix4::identity().into(), 135 | }; 136 | Context { 137 | data: data, 138 | win_size: win_size, 139 | clear_color: [0.7, 0.7, 0.7, 1.0], 140 | window: window, 141 | device: device, 142 | factory: factory, 143 | encoder: encoder, 144 | pso: pso, 145 | pso_wire: pso_wire, 146 | should_close: false, 147 | commands_tx: tx, 148 | font: new_font(), 149 | mouse: MouseState { 150 | is_left_button_pressed: false, 151 | is_right_button_pressed: false, 152 | last_press_pos: ScreenPos{v: Vector2::from_value(0)}, 153 | pos: ScreenPos{v: Vector2::from_value(0)}, 154 | }, 155 | start_time: time::Instant::now(), 156 | events_loop, 157 | } 158 | } 159 | 160 | pub fn clear(&mut self) { 161 | self.encoder.clear(&self.data.out, self.clear_color); 162 | self.encoder.clear_depth(&self.data.out_depth, 1.0); 163 | } 164 | 165 | pub fn current_time(&self) -> Time { 166 | duration_to_time(time::Instant::now() - self.start_time) 167 | } 168 | 169 | pub fn should_close(&self) -> bool { 170 | self.should_close 171 | } 172 | 173 | pub fn flush(&mut self) { 174 | self.encoder.flush(&mut self.device); 175 | self.window.swap_buffers().expect("Can`t swap buffers"); 176 | self.device.cleanup(); 177 | } 178 | 179 | pub fn poll_events(&mut self) -> Vec { 180 | let mut events = Vec::new(); 181 | self.events_loop.poll_events(|e| events.push(e)); 182 | events 183 | } 184 | 185 | pub fn font(&self) -> &rusttype::Font { 186 | &self.font 187 | } 188 | 189 | pub fn win_size(&self) -> Size2 { 190 | self.win_size 191 | } 192 | 193 | pub fn factory_mut(&mut self) -> &mut gfx_gl::Factory { 194 | &mut self.factory 195 | } 196 | 197 | pub fn set_mvp(&mut self, mvp: Matrix4) { 198 | self.data.mvp = mvp.into(); 199 | } 200 | 201 | pub fn set_basic_color(&mut self, color: [f32; 4]) { 202 | self.data.basic_color = color; 203 | } 204 | 205 | pub fn mouse(&self) -> &MouseState { 206 | &self.mouse 207 | } 208 | 209 | pub fn draw_mesh(&mut self, mesh: &Mesh) { 210 | self.data.texture.0 = mesh.texture().clone(); 211 | self.data.vbuf = mesh.vertex_buffer().clone(); 212 | let pso = if mesh.is_wire() { 213 | &self.pso_wire 214 | } else { 215 | &self.pso 216 | }; 217 | self.encoder.draw(mesh.slice(), pso, &self.data); 218 | } 219 | 220 | pub fn add_command(&mut self, command: ScreenCommand) { 221 | self.commands_tx.send(command) 222 | .expect("Can't send command to Visualizer"); 223 | } 224 | 225 | pub fn handle_event_pre(&mut self, event: &WindowEvent) { 226 | match *event { 227 | WindowEvent::CloseRequested | WindowEvent::Destroyed => { 228 | self.should_close = true; 229 | }, 230 | WindowEvent::MouseInput { state: Pressed, button: MouseButton::Left, ..} => { 231 | self.mouse.is_left_button_pressed = true; 232 | self.mouse.last_press_pos = self.mouse.pos; 233 | }, 234 | WindowEvent::MouseInput{ state: Released, button: MouseButton::Left, ..} => { 235 | self.mouse.is_left_button_pressed = false; 236 | }, 237 | WindowEvent::MouseInput{ state: Pressed, button: MouseButton::Right, ..} => { 238 | self.mouse.is_right_button_pressed = true; 239 | }, 240 | WindowEvent::MouseInput{ state: Released, button: MouseButton::Right, ..} => { 241 | self.mouse.is_right_button_pressed = false; 242 | }, 243 | WindowEvent::Resized(size) => { 244 | if size.width as i32 == 0 || size.height as i32 == 0 { 245 | return 246 | } 247 | self.win_size = Size2{w: size.width as i32, h: size.height as i32}; 248 | gfx_glutin::update_views( 249 | &self.window, 250 | &mut self.data.out, 251 | &mut self.data.out_depth, 252 | ); 253 | }, 254 | _ => {}, 255 | } 256 | } 257 | 258 | pub fn handle_event_post(&mut self, event: &WindowEvent) { 259 | match *event { 260 | WindowEvent::CursorMoved{ position: pos, .. } => { 261 | let pos = ScreenPos{v: Vector2{x: pos.x as i32, y: pos.y as i32}}; 262 | self.mouse.pos = pos; 263 | }, 264 | WindowEvent::Touch(glutin::Touch{location: pos, phase, ..}) => { 265 | let pos = ScreenPos{v: Vector2{x: pos.x as i32, y: pos.y as i32}}; 266 | match phase { 267 | glutin::TouchPhase::Moved => { 268 | self.mouse.pos = pos; 269 | }, 270 | glutin::TouchPhase::Started => { 271 | self.mouse.pos = pos; 272 | self.mouse.last_press_pos = pos; 273 | self.mouse.is_left_button_pressed = true; 274 | }, 275 | glutin::TouchPhase::Ended => { 276 | self.mouse.pos = pos; 277 | self.mouse.is_left_button_pressed = false; 278 | }, 279 | glutin::TouchPhase::Cancelled => { 280 | unimplemented!(); 281 | }, 282 | } 283 | }, 284 | _ => {}, 285 | } 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /core/src/filter.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashSet}; 2 | use game_state::{State}; 3 | use fow::{Fow}; 4 | use unit::{Unit, UnitId}; 5 | use event::{CoreEvent, MoveMode, AttackInfo}; 6 | use player::{PlayerId}; 7 | use movement::{MovePoints}; 8 | 9 | fn filtered_unit(unit: &Unit) -> Unit { 10 | Unit { 11 | move_points: None, 12 | attack_points: None, 13 | reactive_attack_points: None, 14 | passenger_id: None, 15 | .. unit.clone() 16 | } 17 | } 18 | 19 | pub fn get_visible_enemies( 20 | state: &State, 21 | fow: &Fow, 22 | player_id: PlayerId, 23 | ) -> HashSet { 24 | let mut visible_enemies = HashSet::new(); 25 | for (&id, unit) in state.units() { 26 | if unit.player_id != player_id 27 | && fow.is_visible(unit) 28 | { 29 | visible_enemies.insert(id); 30 | } 31 | } 32 | visible_enemies 33 | } 34 | 35 | pub fn show_or_hide_passive_enemies( 36 | state: &State, 37 | active_unit_ids: &HashSet, 38 | old: &HashSet, 39 | new: &HashSet, 40 | ) -> Vec { 41 | let mut events = Vec::new(); 42 | let located_units = new.difference(old); 43 | for &id in located_units { 44 | if active_unit_ids.contains(&id) { 45 | continue; 46 | } 47 | let unit = state.unit_opt(id).expect("Can`t find unit"); 48 | events.push(CoreEvent::ShowUnit { 49 | unit_info: filtered_unit(unit), 50 | }); 51 | } 52 | let lost_units = old.difference(new); 53 | for &id in lost_units { 54 | if active_unit_ids.contains(&id) { 55 | continue; 56 | } 57 | events.push(CoreEvent::HideUnit{unit_id: id}); 58 | } 59 | events 60 | } 61 | 62 | pub fn filter_events( 63 | state: &State, 64 | player_id: PlayerId, 65 | fow: &Fow, 66 | event: &CoreEvent, 67 | ) -> (Vec, HashSet) { 68 | assert!(!state.is_partial()); 69 | let mut active_unit_ids = HashSet::new(); 70 | let mut events = vec![]; 71 | match *event { 72 | CoreEvent::Move{unit_id, from, to, ..} => { 73 | let unit = state.unit(unit_id); 74 | if unit.player_id == player_id { 75 | events.push(event.clone()); 76 | } else { 77 | let prev_vis = fow.is_visible_at(unit, from); 78 | let next_vis = fow.is_visible_at(unit, to); 79 | if !prev_vis && next_vis { 80 | events.push(CoreEvent::ShowUnit { 81 | unit_info: Unit { 82 | pos: from, 83 | .. filtered_unit(unit) 84 | }, 85 | }); 86 | if let Some(attached_unit_id) = unit.attached_unit_id { 87 | active_unit_ids.insert(attached_unit_id); 88 | let attached_unit = state.unit(attached_unit_id); 89 | events.push(CoreEvent::ShowUnit { 90 | unit_info: Unit { 91 | pos: from, 92 | .. filtered_unit(attached_unit) 93 | }, 94 | }); 95 | } 96 | } 97 | if prev_vis || next_vis { 98 | events.push(event.clone()); 99 | } 100 | if prev_vis && !next_vis { 101 | events.push(CoreEvent::HideUnit { 102 | unit_id: unit.id, 103 | }); 104 | } 105 | active_unit_ids.insert(unit_id); 106 | } 107 | }, 108 | CoreEvent::CreateUnit{ref unit_info} => { 109 | let unit = state.unit(unit_info.id); 110 | if player_id == unit_info.player_id 111 | || fow.is_visible_at(unit, unit_info.pos) 112 | { 113 | events.push(event.clone()); 114 | active_unit_ids.insert(unit_info.id); 115 | } 116 | }, 117 | CoreEvent::AttackUnit{ref attack_info} => { 118 | let attacker_id = attack_info.attacker_id 119 | .expect("Core must know about everything"); 120 | let attacker = state.unit(attacker_id); 121 | if player_id != attacker.player_id && !attack_info.is_ambush { 122 | // show attacker if this is not ambush 123 | let attacker = state.unit(attacker_id); 124 | if !fow.is_visible(attacker) { 125 | events.push(CoreEvent::ShowUnit { 126 | unit_info: filtered_unit(attacker), 127 | }); 128 | } 129 | active_unit_ids.insert(attacker_id); 130 | } 131 | active_unit_ids.insert(attack_info.defender_id); // if defender is killed 132 | let is_attacker_visible = player_id == attacker.player_id 133 | || !attack_info.is_ambush; 134 | let attack_info = AttackInfo { 135 | attacker_id: if is_attacker_visible { 136 | Some(attacker_id) 137 | } else { 138 | None 139 | }, 140 | .. attack_info.clone() 141 | }; 142 | events.push(CoreEvent::AttackUnit{attack_info: attack_info}); 143 | }, 144 | CoreEvent::Reveal{ref unit_info} => { 145 | if unit_info.player_id != player_id { 146 | events.push(CoreEvent::ShowUnit { 147 | unit_info: filtered_unit(unit_info), 148 | }); 149 | } 150 | }, 151 | CoreEvent::ShowUnit{..} | 152 | CoreEvent::HideUnit{..} => panic!(), 153 | CoreEvent::LoadUnit{passenger_id, from, to, transporter_id} => { 154 | let passenger = state.unit(passenger_id); 155 | let transporter = state.unit(transporter_id.unwrap()); 156 | let is_transporter_vis = fow.is_visible(transporter); 157 | let is_passenger_vis = fow.is_visible_at(passenger, from); 158 | if passenger.player_id == player_id { 159 | events.push(event.clone()); 160 | } else if is_passenger_vis || is_transporter_vis { 161 | if !fow.is_visible_at(passenger, from) { 162 | events.push(CoreEvent::ShowUnit { 163 | unit_info: Unit { 164 | pos: from, 165 | .. filtered_unit(passenger) 166 | }, 167 | }); 168 | } 169 | let filtered_transporter_id = if is_transporter_vis { 170 | transporter_id 171 | } else { 172 | None 173 | }; 174 | events.push(CoreEvent::LoadUnit { 175 | transporter_id: filtered_transporter_id, 176 | passenger_id: passenger_id, 177 | from: from, 178 | to: to, 179 | }); 180 | active_unit_ids.insert(passenger_id); 181 | } 182 | }, 183 | CoreEvent::UnloadUnit{ref unit_info, transporter_id, from, to} => { 184 | active_unit_ids.insert(unit_info.id); 185 | let passenger = state.unit(unit_info.id); 186 | let transporter = state.unit(transporter_id.unwrap()); 187 | let is_transporter_vis = fow.is_visible_at(transporter, from); 188 | let is_passenger_vis = fow.is_visible_at(passenger, to); 189 | if passenger.player_id == player_id { 190 | events.push(event.clone()); 191 | } else if is_passenger_vis || is_transporter_vis { 192 | let filtered_transporter_id = if is_transporter_vis { 193 | transporter_id 194 | } else { 195 | None 196 | }; 197 | events.push(CoreEvent::UnloadUnit { 198 | transporter_id: filtered_transporter_id, 199 | unit_info: Unit { 200 | move_points: None, 201 | attack_points: None, 202 | reactive_attack_points: None, 203 | passenger_id: None, 204 | .. unit_info.clone() 205 | }, 206 | from: from, 207 | to: to, 208 | }); 209 | if !is_passenger_vis { 210 | events.push(CoreEvent::HideUnit { 211 | unit_id: passenger.id, 212 | }); 213 | } 214 | } 215 | }, 216 | CoreEvent::Attach{transporter_id, attached_unit_id, from, to} => { 217 | let transporter = state.unit(transporter_id); 218 | if transporter.player_id == player_id { 219 | events.push(event.clone()) 220 | } else { 221 | active_unit_ids.insert(transporter_id); 222 | let attached_unit = state.unit(attached_unit_id); 223 | let is_attached_unit_vis = fow.is_visible_at(attached_unit, to); 224 | let is_transporter_vis = fow.is_visible_at(transporter, from); 225 | if is_attached_unit_vis { 226 | if !is_transporter_vis { 227 | events.push(CoreEvent::ShowUnit { 228 | unit_info: Unit { 229 | pos: from, 230 | attached_unit_id: None, 231 | .. filtered_unit(transporter) 232 | }, 233 | }); 234 | } 235 | events.push(event.clone()) 236 | } else if is_transporter_vis { 237 | events.push(CoreEvent::Move { 238 | unit_id: transporter_id, 239 | mode: MoveMode::Fast, 240 | cost: MovePoints{n: 0}, 241 | from: from, 242 | to: to, 243 | }); 244 | events.push(CoreEvent::HideUnit { 245 | unit_id: transporter_id, 246 | }); 247 | } 248 | } 249 | }, 250 | CoreEvent::Detach{transporter_id, from, to} => { 251 | let transporter = state.unit(transporter_id); 252 | if transporter.player_id == player_id { 253 | events.push(event.clone()) 254 | } else { 255 | active_unit_ids.insert(transporter_id); 256 | let is_from_vis = fow.is_visible_at(transporter, from); 257 | let is_to_vis = fow.is_visible_at(transporter, to); 258 | if is_from_vis { 259 | events.push(event.clone()); 260 | if !is_to_vis { 261 | events.push(CoreEvent::HideUnit { 262 | unit_id: transporter_id, 263 | }); 264 | } 265 | } else if is_to_vis { 266 | events.push(CoreEvent::ShowUnit { 267 | unit_info: Unit { 268 | pos: from, 269 | attached_unit_id: None, 270 | .. filtered_unit(transporter) 271 | }, 272 | }); 273 | events.push(CoreEvent::Move { 274 | unit_id: transporter_id, 275 | mode: MoveMode::Fast, 276 | cost: MovePoints{n: 0}, 277 | from: from, 278 | to: to, 279 | }); 280 | } 281 | } 282 | }, 283 | CoreEvent::SetReactionFireMode{unit_id, ..} => { 284 | let unit = state.unit(unit_id); 285 | if unit.player_id == player_id { 286 | events.push(event.clone()); 287 | } 288 | }, 289 | CoreEvent::Smoke{id, pos, unit_id} => { 290 | let unit_id = unit_id.expect("Core must know about everything"); 291 | let unit = state.unit(unit_id); 292 | if fow.is_visible(unit) { 293 | events.push(event.clone()); 294 | } else { 295 | events.push(CoreEvent::Smoke { 296 | id: id, 297 | pos: pos, 298 | unit_id: None, 299 | }); 300 | } 301 | }, 302 | CoreEvent::EndTurn{..} | 303 | CoreEvent::RemoveSmoke{..} | 304 | CoreEvent::VictoryPoint{..} | 305 | CoreEvent::SectorOwnerChanged{..} => { 306 | events.push(event.clone()); 307 | }, 308 | } 309 | (events, active_unit_ids) 310 | } 311 | -------------------------------------------------------------------------------- /core/src/db.rs: -------------------------------------------------------------------------------- 1 | use unit::{UnitType, WeaponType, UnitTypeId, WeaponTypeId}; 2 | use map::{Distance}; 3 | use movement::{MovePoints}; 4 | use attack::{AttackPoints}; 5 | use game_state::{ReinforcementPoints}; 6 | 7 | fn weapon_type_id(weapon_types: &[WeaponType], name: &str) 8 | -> WeaponTypeId 9 | { 10 | for (id, weapon_type) in weapon_types.iter().enumerate() { 11 | if weapon_type.name == name { 12 | return WeaponTypeId{id: id as i32}; 13 | } 14 | } 15 | panic!("No weapon type with name \"{}\"", name); 16 | } 17 | 18 | // TODO: read from json/toml config 19 | fn get_weapon_types() -> Vec { 20 | vec![ 21 | WeaponType { 22 | name: "mortar".to_owned(), 23 | damage: 6, 24 | ap: 2, 25 | accuracy: 5, 26 | max_distance: Distance{n: 5}, 27 | max_air_distance: None, 28 | min_distance: Distance{n: 1}, 29 | is_inderect: true, 30 | reaction_fire: false, 31 | smoke: Some(3), 32 | }, 33 | WeaponType { 34 | name: "super_heavy_tank_gun".to_owned(), 35 | damage: 11, 36 | ap: 11, 37 | accuracy: 5, 38 | max_distance: Distance{n: 6}, 39 | max_air_distance: None, 40 | min_distance: Distance{n: 0}, 41 | is_inderect: false, 42 | reaction_fire: true, 43 | smoke: None, 44 | }, 45 | WeaponType { 46 | name: "heavy_tank_gun".to_owned(), 47 | damage: 9, 48 | ap: 9, 49 | accuracy: 5, 50 | max_distance: Distance{n: 5}, 51 | max_air_distance: None, 52 | min_distance: Distance{n: 0}, 53 | is_inderect: false, 54 | reaction_fire: true, 55 | smoke: None, 56 | }, 57 | WeaponType { 58 | name: "medium_tank_gun".to_owned(), 59 | damage: 7, 60 | ap: 7, 61 | accuracy: 5, 62 | max_distance: Distance{n: 4}, 63 | max_air_distance: None, 64 | min_distance: Distance{n: 0}, 65 | is_inderect: false, 66 | reaction_fire: true, 67 | smoke: None, 68 | }, 69 | WeaponType { 70 | name: "light_tank_gun".to_owned(), 71 | damage: 6, 72 | ap: 5, 73 | accuracy: 5, 74 | max_distance: Distance{n: 4}, 75 | max_air_distance: None, 76 | min_distance: Distance{n: 0}, 77 | is_inderect: false, 78 | reaction_fire: true, 79 | smoke: None, 80 | }, 81 | WeaponType { 82 | name: "rifle".to_owned(), 83 | damage: 2, 84 | ap: 1, 85 | accuracy: 5, 86 | max_distance: Distance{n: 3}, 87 | max_air_distance: Some(Distance{n: 2}), 88 | min_distance: Distance{n: 0}, 89 | is_inderect: false, 90 | reaction_fire: true, 91 | smoke: None, 92 | }, 93 | WeaponType { 94 | name: "submachine_gun".to_owned(), 95 | damage: 3, 96 | ap: 1, 97 | accuracy: 4, 98 | max_distance: Distance{n: 2}, 99 | max_air_distance: Some(Distance{n: 1}), 100 | min_distance: Distance{n: 0}, 101 | is_inderect: false, 102 | reaction_fire: true, 103 | smoke: None, 104 | }, 105 | WeaponType { 106 | name: "machine_gun".to_owned(), 107 | damage: 5, 108 | ap: 2, 109 | accuracy: 5, 110 | max_distance: Distance{n: 3}, 111 | max_air_distance: Some(Distance{n: 2}), 112 | min_distance: Distance{n: 0}, 113 | is_inderect: false, 114 | reaction_fire: true, 115 | smoke: None, 116 | }, 117 | ] 118 | } 119 | 120 | // TODO: read from json/toml config 121 | fn get_unit_types(weapon_types: &[WeaponType]) -> Vec { 122 | vec![ 123 | UnitType { 124 | name: "mammoth_tank".to_owned(), 125 | size: 12, 126 | count: 1, 127 | armor: 13, 128 | toughness: 9, 129 | weapon_skill: 5, 130 | weapon_type_id: weapon_type_id(weapon_types, "super_heavy_tank_gun"), 131 | move_points: MovePoints{n: 5}, 132 | attack_points: AttackPoints{n: 1}, 133 | reactive_attack_points: AttackPoints{n: 1}, 134 | los_range: Distance{n: 7}, 135 | cover_los_range: Distance{n: 0}, 136 | is_transporter: false, 137 | is_big: true, 138 | is_air: false, 139 | is_infantry: false, 140 | can_be_towed: false, 141 | cost: ReinforcementPoints{n: 16}, 142 | }, 143 | UnitType { 144 | name: "heavy_tank".to_owned(), 145 | size: 8, 146 | count: 1, 147 | armor: 11, 148 | toughness: 9, 149 | weapon_skill: 5, 150 | weapon_type_id: weapon_type_id(weapon_types, "heavy_tank_gun"), 151 | move_points: MovePoints{n: 7}, 152 | attack_points: AttackPoints{n: 2}, 153 | reactive_attack_points: AttackPoints{n: 1}, 154 | los_range: Distance{n: 7}, 155 | cover_los_range: Distance{n: 0}, 156 | is_transporter: false, 157 | is_big: false, 158 | is_air: false, 159 | is_infantry: false, 160 | can_be_towed: true, 161 | cost: ReinforcementPoints{n: 10}, 162 | }, 163 | UnitType { 164 | name: "medium_tank".to_owned(), 165 | size: 7, 166 | count: 1, 167 | armor: 9, 168 | toughness: 9, 169 | weapon_skill: 5, 170 | weapon_type_id: weapon_type_id(weapon_types, "medium_tank_gun"), 171 | move_points: MovePoints{n: 8}, 172 | attack_points: AttackPoints{n: 2}, 173 | reactive_attack_points: AttackPoints{n: 1}, 174 | los_range: Distance{n: 7}, 175 | cover_los_range: Distance{n: 0}, 176 | is_transporter: false, 177 | is_big: false, 178 | is_air: false, 179 | is_infantry: false, 180 | can_be_towed: true, 181 | cost: ReinforcementPoints{n: 8}, 182 | }, 183 | UnitType { 184 | name: "light_tank".to_owned(), 185 | size: 6, 186 | count: 1, 187 | armor: 7, 188 | toughness: 9, 189 | weapon_skill: 5, 190 | weapon_type_id: weapon_type_id(weapon_types, "light_tank_gun"), 191 | move_points: MovePoints{n: 10}, 192 | attack_points: AttackPoints{n: 2}, 193 | reactive_attack_points: AttackPoints{n: 1}, 194 | los_range: Distance{n: 7}, 195 | cover_los_range: Distance{n: 0}, 196 | is_transporter: false, 197 | is_big: false, 198 | is_air: false, 199 | is_infantry: false, 200 | can_be_towed: true, 201 | cost: ReinforcementPoints{n: 6}, 202 | }, 203 | UnitType { 204 | name: "light_spg".to_owned(), 205 | size: 6, 206 | count: 1, 207 | armor: 5, 208 | toughness: 9, 209 | weapon_skill: 7, 210 | weapon_type_id: weapon_type_id(weapon_types, "medium_tank_gun"), 211 | move_points: MovePoints{n: 10}, 212 | attack_points: AttackPoints{n: 2}, 213 | reactive_attack_points: AttackPoints{n: 1}, 214 | los_range: Distance{n: 7}, 215 | cover_los_range: Distance{n: 0}, 216 | is_transporter: false, 217 | is_big: false, 218 | is_air: false, 219 | is_infantry: false, 220 | can_be_towed: true, 221 | cost: ReinforcementPoints{n: 6}, 222 | }, 223 | UnitType { 224 | name: "field_gun".to_owned(), 225 | size: 6, 226 | count: 1, 227 | armor: 3, 228 | toughness: 7, 229 | weapon_skill: 7, 230 | // TODO: "tank_gun" on field gun?? 231 | weapon_type_id: weapon_type_id(weapon_types, "medium_tank_gun"), 232 | move_points: MovePoints{n: 7}, 233 | attack_points: AttackPoints{n: 2}, 234 | reactive_attack_points: AttackPoints{n: 1}, 235 | los_range: Distance{n: 7}, 236 | cover_los_range: Distance{n: 0}, 237 | is_transporter: false, 238 | is_big: false, 239 | is_air: false, 240 | is_infantry: true, 241 | can_be_towed: true, 242 | cost: ReinforcementPoints{n: 5}, 243 | }, 244 | UnitType { 245 | name: "jeep".to_owned(), 246 | size: 5, 247 | count: 1, 248 | armor: 2, 249 | toughness: 3, 250 | weapon_skill: 5, 251 | weapon_type_id: weapon_type_id(weapon_types, "machine_gun"), 252 | move_points: MovePoints{n: 12}, 253 | attack_points: AttackPoints{n: 2}, 254 | reactive_attack_points: AttackPoints{n: 1}, 255 | los_range: Distance{n: 8}, 256 | cover_los_range: Distance{n: 0}, 257 | is_transporter: false, 258 | is_big: false, 259 | is_air: false, 260 | is_infantry: false, 261 | can_be_towed: true, 262 | cost: ReinforcementPoints{n: 4}, 263 | }, 264 | UnitType { 265 | name: "truck".to_owned(), 266 | size: 6, 267 | count: 1, 268 | armor: 2, 269 | toughness: 3, 270 | weapon_skill: 0, 271 | weapon_type_id: weapon_type_id(weapon_types, "machine_gun"), // TODO: remove hack 272 | move_points: MovePoints{n: 10}, 273 | attack_points: AttackPoints{n: 0}, 274 | reactive_attack_points: AttackPoints{n: 0}, 275 | los_range: Distance{n: 6}, 276 | cover_los_range: Distance{n: 0}, 277 | is_transporter: true, 278 | is_big: false, 279 | is_air: false, 280 | is_infantry: false, 281 | can_be_towed: true, 282 | cost: ReinforcementPoints{n: 4}, 283 | }, 284 | UnitType { 285 | name: "helicopter".to_owned(), 286 | size: 9, 287 | count: 1, 288 | armor: 3, 289 | toughness: 3, 290 | weapon_skill: 5, 291 | weapon_type_id: weapon_type_id(weapon_types, "machine_gun"), 292 | move_points: MovePoints{n: 10}, 293 | attack_points: AttackPoints{n: 2}, 294 | reactive_attack_points: AttackPoints{n: 1}, 295 | los_range: Distance{n: 8}, 296 | cover_los_range: Distance{n: 0}, 297 | is_transporter: false, 298 | is_big: true, 299 | is_air: true, 300 | is_infantry: false, 301 | can_be_towed: false, 302 | cost: ReinforcementPoints{n: 10}, 303 | }, 304 | UnitType { 305 | name: "soldier".to_owned(), 306 | size: 4, 307 | count: 4, 308 | armor: 1, 309 | toughness: 2, 310 | weapon_skill: 5, 311 | weapon_type_id: weapon_type_id(weapon_types, "rifle"), 312 | move_points: MovePoints{n: 9}, 313 | attack_points: AttackPoints{n: 2}, 314 | reactive_attack_points: AttackPoints{n: 1}, 315 | los_range: Distance{n: 6}, 316 | cover_los_range: Distance{n: 1}, 317 | is_transporter: false, 318 | is_big: false, 319 | is_air: false, 320 | is_infantry: true, 321 | can_be_towed: false, 322 | cost: ReinforcementPoints{n: 2}, 323 | }, 324 | UnitType { 325 | name: "smg".to_owned(), 326 | size: 4, 327 | count: 3, 328 | armor: 1, 329 | toughness: 2, 330 | weapon_skill: 5, 331 | weapon_type_id: weapon_type_id(weapon_types, "submachine_gun"), 332 | move_points: MovePoints{n: 9}, 333 | attack_points: AttackPoints{n: 2}, 334 | reactive_attack_points: AttackPoints{n: 1}, 335 | los_range: Distance{n: 6}, 336 | cover_los_range: Distance{n: 1}, 337 | is_transporter: false, 338 | is_big: false, 339 | is_air: false, 340 | is_infantry: true, 341 | can_be_towed: false, 342 | cost: ReinforcementPoints{n: 2}, 343 | }, 344 | UnitType { 345 | name: "scout".to_owned(), 346 | size: 4, 347 | count: 2, 348 | armor: 1, 349 | toughness: 2, 350 | weapon_skill: 5, 351 | weapon_type_id: weapon_type_id(weapon_types, "rifle"), 352 | move_points: MovePoints{n: 11}, 353 | attack_points: AttackPoints{n: 2}, 354 | reactive_attack_points: AttackPoints{n: 1}, 355 | los_range: Distance{n: 8}, 356 | cover_los_range: Distance{n: 2}, 357 | is_transporter: false, 358 | is_big: false, 359 | is_air: false, 360 | is_infantry: true, 361 | can_be_towed: false, 362 | cost: ReinforcementPoints{n: 3}, 363 | }, 364 | UnitType { 365 | name: "mortar".to_owned(), 366 | size: 4, 367 | count: 1, 368 | armor: 1, 369 | toughness: 2, 370 | weapon_skill: 5, 371 | weapon_type_id: weapon_type_id(weapon_types, "mortar"), 372 | move_points: MovePoints{n: 7}, 373 | attack_points: AttackPoints{n: 2}, 374 | reactive_attack_points: AttackPoints{n: 0}, 375 | los_range: Distance{n: 6}, 376 | cover_los_range: Distance{n: 1}, 377 | is_transporter: false, 378 | is_big: false, 379 | is_air: false, 380 | is_infantry: true, 381 | can_be_towed: false, 382 | cost: ReinforcementPoints{n: 4}, 383 | }, 384 | ] 385 | } 386 | 387 | #[derive(Clone, Debug)] 388 | pub struct Db { 389 | unit_types: Vec, 390 | weapon_types: Vec, 391 | } 392 | 393 | impl Default for Db { 394 | fn default() -> Self { 395 | Db::new() 396 | } 397 | } 398 | 399 | impl Db { 400 | pub fn new() -> Db { 401 | let weapon_types = get_weapon_types(); 402 | let unit_types = get_unit_types(&weapon_types); 403 | Db { 404 | weapon_types: weapon_types, 405 | unit_types: unit_types, 406 | } 407 | } 408 | 409 | fn unit_type_id_opt(&self, name: &str) -> Option { 410 | for (id, unit_type) in self.unit_types.iter().enumerate() { 411 | if unit_type.name == name { 412 | return Some(UnitTypeId{id: id as i32}); 413 | } 414 | } 415 | None 416 | } 417 | 418 | pub fn unit_types(&self) -> &[UnitType] { 419 | &self.unit_types 420 | } 421 | 422 | pub fn unit_type(&self, unit_type_id: UnitTypeId) -> &UnitType { 423 | &self.unit_types[unit_type_id.id as usize] 424 | } 425 | 426 | pub fn weapon_type(&self, type_id: WeaponTypeId) -> &WeaponType { 427 | &self.weapon_types[type_id.id as usize] 428 | } 429 | 430 | pub fn unit_type_id(&self, name: &str) -> UnitTypeId { 431 | match self.unit_type_id_opt(name) { 432 | Some(id) => id, 433 | None => panic!("No unit type with name: \"{}\"", name), 434 | } 435 | } 436 | 437 | pub fn weapon_type_id(&self, name: &str) -> WeaponTypeId { 438 | weapon_type_id(&self.weapon_types, name) 439 | } 440 | } 441 | --------------------------------------------------------------------------------