init
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
18
Cargo.lock
generated
Normal file
18
Cargo.lock
generated
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cli"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"game-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "game-core"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "web"
|
||||||
|
version = "0.1.0"
|
||||||
3
Cargo.toml
Normal file
3
Cargo.toml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[workspace]
|
||||||
|
resolver = "3"
|
||||||
|
members = ["game-core", "platforms/cli", "platforms/web"]
|
||||||
1
game-core/.gitignore
vendored
Normal file
1
game-core/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
6
game-core/Cargo.toml
Normal file
6
game-core/Cargo.toml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[package]
|
||||||
|
name = "game-core"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
5
game-core/src/assets.rs
Normal file
5
game-core/src/assets.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
pub trait AssetLoader {
|
||||||
|
fn load_chunk(&mut self, offset: u32, length: usize, dest: &mut [u8]) -> Result<usize, ()>;
|
||||||
|
|
||||||
|
fn load_map(&mut self, map_id: u8, buffer: &mut [u8]) -> Result<usize, ()>;
|
||||||
|
}
|
||||||
13
game-core/src/combat.rs
Normal file
13
game-core/src/combat.rs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
use crate::{math::Rng, types::Stats};
|
||||||
|
|
||||||
|
pub fn resolve_attack(rng: &mut Rng, attacker: &Stats, defender: &Stats) -> (bool, i8) {
|
||||||
|
let roll = rng.roll_d20();
|
||||||
|
|
||||||
|
// simplified logic for now
|
||||||
|
if roll > 10 {
|
||||||
|
let dmg = (attacker.strength / 2) as i8;
|
||||||
|
(true, dmg)
|
||||||
|
} else {
|
||||||
|
(false, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
0
game-core/src/errors.rs
Normal file
0
game-core/src/errors.rs
Normal file
8
game-core/src/input.rs
Normal file
8
game-core/src/input.rs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
use crate::types::Direction;
|
||||||
|
|
||||||
|
pub enum InputEvent {
|
||||||
|
None,
|
||||||
|
Move(Direction),
|
||||||
|
Action,
|
||||||
|
Char(char),
|
||||||
|
}
|
||||||
12
game-core/src/lib.rs
Normal file
12
game-core/src/lib.rs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#![no_std]
|
||||||
|
extern crate alloc;
|
||||||
|
|
||||||
|
pub mod assets;
|
||||||
|
pub mod combat;
|
||||||
|
pub mod errors;
|
||||||
|
pub mod input;
|
||||||
|
pub mod map;
|
||||||
|
pub mod math;
|
||||||
|
pub mod render;
|
||||||
|
pub mod state;
|
||||||
|
pub mod types;
|
||||||
43
game-core/src/map.rs
Normal file
43
game-core/src/map.rs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
use crate::types::{Direction, TileType};
|
||||||
|
|
||||||
|
pub struct MapView<'a> {
|
||||||
|
pub width: u8,
|
||||||
|
pub height: u8,
|
||||||
|
pub data: &'a [u8],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> MapView<'a> {
|
||||||
|
pub fn new(raw_data: &'a [u8]) -> Self {
|
||||||
|
let width = raw_data[0];
|
||||||
|
let height = raw_data[1];
|
||||||
|
let data = &raw_data[2..];
|
||||||
|
MapView {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_tile(&self, index: usize) -> TileType {
|
||||||
|
if index < self.data.len() {
|
||||||
|
self.data[index].into()
|
||||||
|
} else {
|
||||||
|
TileType::Void
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_neighbor_index(&self, current_index: usize, direction: Direction) -> Option<usize> {
|
||||||
|
let x = (current_index as u8) % self.width;
|
||||||
|
let y = (current_index as u8) / self.width;
|
||||||
|
|
||||||
|
let (nx, ny) = match direction {
|
||||||
|
Direction::North if y > 0 => (x, y - 1),
|
||||||
|
Direction::South if y < (self.height - 1) => (x, y + 1),
|
||||||
|
Direction::East if x < (self.width - 1) => (x + 1, y),
|
||||||
|
Direction::West if x > 0 => (x - 1, y),
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Some((ny * self.width + nx) as usize)
|
||||||
|
}
|
||||||
|
}
|
||||||
27
game-core/src/math.rs
Normal file
27
game-core/src/math.rs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
pub struct Rng {
|
||||||
|
state: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rng {
|
||||||
|
pub fn new(seed: u32) -> Self {
|
||||||
|
Rng { state: seed }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next(&mut self) -> u32 {
|
||||||
|
let mut x = self.state;
|
||||||
|
x ^= x << 13;
|
||||||
|
x ^= x >> 17;
|
||||||
|
x ^= x << 5;
|
||||||
|
self.state = x;
|
||||||
|
x
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_range(&mut self, min: u32, max: u32) -> u32 {
|
||||||
|
let range = max - min;
|
||||||
|
(self.next() % range) + min
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn roll_d20(&mut self) -> u8 {
|
||||||
|
(self.next_range(1, 21)) as u8
|
||||||
|
}
|
||||||
|
}
|
||||||
24
game-core/src/render.rs
Normal file
24
game-core/src/render.rs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
use crate::types::Stats;
|
||||||
|
|
||||||
|
pub enum RenderRequest<'a> {
|
||||||
|
MainMenu,
|
||||||
|
CharacterCreation {
|
||||||
|
step: u8,
|
||||||
|
prompt: &'static str,
|
||||||
|
},
|
||||||
|
Exploration {
|
||||||
|
tiles: &'a [u8],
|
||||||
|
width: u8,
|
||||||
|
player_idx: usize,
|
||||||
|
message: &'a str,
|
||||||
|
},
|
||||||
|
Combat {
|
||||||
|
player_stats: &'a Stats,
|
||||||
|
enemy_stats: &'a Stats,
|
||||||
|
log: &'a str,
|
||||||
|
},
|
||||||
|
Dialogue {
|
||||||
|
text: &'a str,
|
||||||
|
options: &'a [&'static str],
|
||||||
|
},
|
||||||
|
}
|
||||||
178
game-core/src/state.rs
Normal file
178
game-core/src/state.rs
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
use crate::{
|
||||||
|
assets::AssetLoader,
|
||||||
|
combat,
|
||||||
|
input::InputEvent,
|
||||||
|
map::MapView,
|
||||||
|
math::Rng,
|
||||||
|
render::RenderRequest,
|
||||||
|
types::{Stats, TileType},
|
||||||
|
};
|
||||||
|
|
||||||
|
enum State {
|
||||||
|
Menu,
|
||||||
|
Creation,
|
||||||
|
Roaming,
|
||||||
|
Fighting,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Game {
|
||||||
|
state: State,
|
||||||
|
|
||||||
|
pub player: Stats,
|
||||||
|
pub player_pos_index: usize,
|
||||||
|
|
||||||
|
map_buffer: [u8; 1024],
|
||||||
|
rng: Rng,
|
||||||
|
|
||||||
|
last_message: &'static str,
|
||||||
|
enemy_temp: Stats,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Game {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
state: State::Menu,
|
||||||
|
player: Stats::default(),
|
||||||
|
player_pos_index: 0,
|
||||||
|
map_buffer: [0; 1024],
|
||||||
|
rng: Rng::new(12345),
|
||||||
|
last_message: "",
|
||||||
|
enemy_temp: Stats::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tick<L: AssetLoader>(
|
||||||
|
&'_ mut self,
|
||||||
|
input: InputEvent,
|
||||||
|
loader: &mut L,
|
||||||
|
) -> RenderRequest<'_> {
|
||||||
|
match self.state {
|
||||||
|
State::Menu => {
|
||||||
|
if let InputEvent::Action = input {
|
||||||
|
self.state = State::Creation;
|
||||||
|
return RenderRequest::CharacterCreation {
|
||||||
|
step: 1,
|
||||||
|
prompt: "Rolling Stats...",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
RenderRequest::MainMenu
|
||||||
|
}
|
||||||
|
|
||||||
|
State::Creation => {
|
||||||
|
self.player = Stats {
|
||||||
|
strength: self.rng.next_range(1, 20) as u8,
|
||||||
|
charisma: self.rng.next_range(1, 20) as u8,
|
||||||
|
intelligence: self.rng.next_range(1, 20) as u8,
|
||||||
|
constitution: self.rng.next_range(1, 20) as u8,
|
||||||
|
dexterity: self.rng.next_range(1, 20) as u8,
|
||||||
|
hp_current: 10,
|
||||||
|
hp_max: 10,
|
||||||
|
wisdom: self.rng.next_range(1, 20) as u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = loader.load_map(1, &mut self.map_buffer);
|
||||||
|
|
||||||
|
self.player_pos_index = 22; // Starting position arbitrary for mvp
|
||||||
|
self.state = State::Roaming;
|
||||||
|
self.last_message = "You find yourself in a mysterious land. Explore!";
|
||||||
|
|
||||||
|
RenderRequest::Exploration {
|
||||||
|
tiles: &self.map_buffer,
|
||||||
|
width: self.map_buffer[0],
|
||||||
|
player_idx: self.player_pos_index,
|
||||||
|
message: self.last_message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
State::Roaming => self.handle_roaming(input),
|
||||||
|
|
||||||
|
State::Fighting => self.handle_combat(input),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_roaming(&'_ mut self, input: InputEvent) -> RenderRequest<'_> {
|
||||||
|
let view = MapView::new(&self.map_buffer);
|
||||||
|
|
||||||
|
if let InputEvent::Move(dir) = input {
|
||||||
|
if let Some(new_idx) = view.get_neighbor_index(self.player_pos_index, dir) {
|
||||||
|
let tile = view.get_tile(new_idx);
|
||||||
|
match tile {
|
||||||
|
TileType::Wall => self.last_message = "You bump into a wall.",
|
||||||
|
TileType::Enemy => {
|
||||||
|
self.last_message = "An enemy appears!";
|
||||||
|
self.state = State::Fighting;
|
||||||
|
self.enemy_temp = Stats {
|
||||||
|
strength: self.rng.next_range(1, 20) as u8,
|
||||||
|
charisma: self.rng.next_range(1, 20) as u8,
|
||||||
|
intelligence: self.rng.next_range(1, 20) as u8,
|
||||||
|
constitution: self.rng.next_range(1, 20) as u8,
|
||||||
|
dexterity: self.rng.next_range(1, 20) as u8,
|
||||||
|
hp_current: 10,
|
||||||
|
hp_max: 10,
|
||||||
|
wisdom: self.rng.next_range(1, 20) as u8,
|
||||||
|
};
|
||||||
|
return RenderRequest::Combat {
|
||||||
|
player_stats: &self.player,
|
||||||
|
enemy_stats: &self.enemy_temp,
|
||||||
|
log: self.last_message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
self.player_pos_index = new_idx;
|
||||||
|
self.last_message = "Moving...";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderRequest::Exploration {
|
||||||
|
tiles: &self.map_buffer,
|
||||||
|
width: self.map_buffer[0],
|
||||||
|
player_idx: self.player_pos_index,
|
||||||
|
message: self.last_message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_combat(&'_ mut self, input: InputEvent) -> RenderRequest<'_> {
|
||||||
|
let mut log = "Your turn.";
|
||||||
|
|
||||||
|
if let InputEvent::Action = input {
|
||||||
|
let (hit, dmg) = combat::resolve_attack(&mut self.rng, &self.player, &self.enemy_temp);
|
||||||
|
|
||||||
|
if hit {
|
||||||
|
self.enemy_temp.hp_current -= dmg;
|
||||||
|
if self.enemy_temp.hp_current <= 0 {
|
||||||
|
self.state = State::Roaming;
|
||||||
|
// Clear the tile (hacky simple update)
|
||||||
|
// In real engine, we'd update an entity list, not the raw map buffer
|
||||||
|
self.last_message = "Victory!";
|
||||||
|
return self.handle_roaming(InputEvent::None);
|
||||||
|
}
|
||||||
|
log = "Hit!";
|
||||||
|
} else {
|
||||||
|
log = "You missed!";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enemy turn (simplified)
|
||||||
|
let (enemy_hit, enemy_dmg) =
|
||||||
|
combat::resolve_attack(&mut self.rng, &self.enemy_temp, &self.player);
|
||||||
|
if enemy_hit {
|
||||||
|
self.player.hp_current -= enemy_dmg;
|
||||||
|
if self.player.hp_current <= 0 {
|
||||||
|
self.state = State::Menu;
|
||||||
|
self.last_message = "You have been defeated. Returning to menu.";
|
||||||
|
return RenderRequest::MainMenu;
|
||||||
|
}
|
||||||
|
log = "Enemy hits you!";
|
||||||
|
} else {
|
||||||
|
log = "Enemy missed!";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderRequest::Combat {
|
||||||
|
player_stats: &self.player,
|
||||||
|
enemy_stats: &self.enemy_temp,
|
||||||
|
log,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
56
game-core/src/types.rs
Normal file
56
game-core/src/types.rs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum Direction {
|
||||||
|
North = 0,
|
||||||
|
South = 1,
|
||||||
|
East = 2,
|
||||||
|
West = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum Attribute {
|
||||||
|
Strength = 0,
|
||||||
|
Dexterity = 1,
|
||||||
|
Constitution = 2,
|
||||||
|
Intelligence = 3,
|
||||||
|
Wisdom = 4,
|
||||||
|
Charisma = 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct Stats {
|
||||||
|
pub strength: u8,
|
||||||
|
pub dexterity: u8,
|
||||||
|
pub constitution: u8,
|
||||||
|
pub intelligence: u8,
|
||||||
|
pub wisdom: u8,
|
||||||
|
pub charisma: u8,
|
||||||
|
pub hp_current: i8,
|
||||||
|
pub hp_max: i8,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum TileType {
|
||||||
|
Void = 0,
|
||||||
|
Grass = 1,
|
||||||
|
Water = 2,
|
||||||
|
Wall = 3,
|
||||||
|
Enemy = 4,
|
||||||
|
Npc = 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u8> for TileType {
|
||||||
|
fn from(val: u8) -> Self {
|
||||||
|
match val {
|
||||||
|
1 => TileType::Grass,
|
||||||
|
2 => TileType::Water,
|
||||||
|
3 => TileType::Wall,
|
||||||
|
4 => TileType::Enemy,
|
||||||
|
5 => TileType::Npc,
|
||||||
|
_ => TileType::Void,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
platforms/cli/.gitignore
vendored
Normal file
1
platforms/cli/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
8
platforms/cli/Cargo.toml
Normal file
8
platforms/cli/Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[package]
|
||||||
|
name = "cli"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
default-run = "cli"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
game-core = { path = "../../game-core" }
|
||||||
23
platforms/cli/src/disk_loader.rs
Normal file
23
platforms/cli/src/disk_loader.rs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
use game_core::assets::AssetLoader;
|
||||||
|
|
||||||
|
pub struct DiskLoader;
|
||||||
|
|
||||||
|
impl AssetLoader for DiskLoader {
|
||||||
|
fn load_chunk(&mut self, offset: u32, length: usize, dest: &mut [u8]) -> Result<usize, ()> {
|
||||||
|
// In a real implementation, this would read from disk.
|
||||||
|
// For this example, we'll just fill the buffer with dummy data.
|
||||||
|
let dummy_data = vec![0u8; length];
|
||||||
|
dest[..length].copy_from_slice(&dummy_data);
|
||||||
|
Ok(length)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_map(&mut self, map_id: u8, buffer: &mut [u8]) -> Result<usize, ()> {
|
||||||
|
let map_data = [
|
||||||
|
5, 5, 2, 2, 2, 2, 2, 2, 1, 1, 4, 2, 2, 1, 3, 1, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2,
|
||||||
|
];
|
||||||
|
|
||||||
|
let len = map_data.len();
|
||||||
|
buffer[..len].copy_from_slice(&map_data);
|
||||||
|
Ok(len)
|
||||||
|
}
|
||||||
|
}
|
||||||
93
platforms/cli/src/main.rs
Normal file
93
platforms/cli/src/main.rs
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
use std::io::{self, Write};
|
||||||
|
|
||||||
|
use game_core::{input::InputEvent, render::RenderRequest, state::Game, types::Direction};
|
||||||
|
|
||||||
|
use crate::disk_loader::DiskLoader;
|
||||||
|
|
||||||
|
mod disk_loader;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut game = Game::new();
|
||||||
|
let mut loader = DiskLoader;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mut input_str = String::new();
|
||||||
|
print!("> ");
|
||||||
|
io::stdout().flush().unwrap();
|
||||||
|
io::stdin().read_line(&mut input_str).unwrap();
|
||||||
|
|
||||||
|
let input = match input_str.trim() {
|
||||||
|
"w" => InputEvent::Move(Direction::North),
|
||||||
|
"s" => InputEvent::Move(Direction::South),
|
||||||
|
"a" => InputEvent::Move(Direction::West),
|
||||||
|
"d" => InputEvent::Move(Direction::East),
|
||||||
|
"e" => InputEvent::Action,
|
||||||
|
_ => InputEvent::None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let render = game.tick(input, &mut loader);
|
||||||
|
|
||||||
|
print!("{}[2J", 27 as char); // Clear screen
|
||||||
|
match render {
|
||||||
|
RenderRequest::MainMenu => {
|
||||||
|
println!("Welcome to the Twillight Island! Press 'e' to start.")
|
||||||
|
}
|
||||||
|
RenderRequest::CharacterCreation { prompt, .. } => {
|
||||||
|
println!("Creating Char: {}", prompt)
|
||||||
|
}
|
||||||
|
RenderRequest::Exploration {
|
||||||
|
tiles,
|
||||||
|
width,
|
||||||
|
player_idx,
|
||||||
|
message,
|
||||||
|
} => {
|
||||||
|
println!("STATUS: {}", message);
|
||||||
|
let w = width as usize;
|
||||||
|
// skip header (2 bytes)
|
||||||
|
let map_tiles = &tiles[2..];
|
||||||
|
|
||||||
|
for (i, &tile_byte) in map_tiles.iter().enumerate() {
|
||||||
|
if i % w == 0 {
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == player_idx {
|
||||||
|
print!("@");
|
||||||
|
} else {
|
||||||
|
let char = match tile_byte {
|
||||||
|
1 => '.', // Grass
|
||||||
|
2 => '~', // Water
|
||||||
|
3 => '#', // Wall
|
||||||
|
4 => 'E', // Enemy
|
||||||
|
_ => ' ',
|
||||||
|
};
|
||||||
|
print!("{}", char);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
RenderRequest::Combat {
|
||||||
|
player_stats,
|
||||||
|
enemy_stats,
|
||||||
|
log,
|
||||||
|
} => {
|
||||||
|
println!("COMBAT!");
|
||||||
|
println!(
|
||||||
|
"Player HP: {}/{}",
|
||||||
|
player_stats.hp_current, player_stats.hp_max
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"Enemy HP: {}/{}",
|
||||||
|
enemy_stats.hp_current, enemy_stats.hp_max
|
||||||
|
);
|
||||||
|
println!("{}", log);
|
||||||
|
}
|
||||||
|
RenderRequest::Dialogue { text, options } => {
|
||||||
|
println!("{}", text);
|
||||||
|
for (i, option) in options.iter().enumerate() {
|
||||||
|
println!("{}: {}", i + 1, option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
platforms/web/.gitignore
vendored
Normal file
1
platforms/web/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
6
platforms/web/Cargo.toml
Normal file
6
platforms/web/Cargo.toml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[package]
|
||||||
|
name = "web"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
3
platforms/web/src/main.rs
Normal file
3
platforms/web/src/main.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
println!("Hello, world!");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user