diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8226cd..e94dc44 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,10 +10,34 @@ permissions: contents: read env: - GODOT_VERSION: 4.3-stable + GODOT_VERSION: 4.4-dev2 jobs: + assign: + name: assign build_id + runs-on: ubuntu-latest + steps: + - run: wget https://github.com/trevyn/animal-time/releases/latest/download/animal-time + - run: chmod +x animal-time + - run: ./animal-time > build_id + - run: cat build_id + + - uses: actions/upload-artifact@v4 + with: + name: build_id + path: build_id + + build-windows: + needs: [assign] + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - run: rustup default stable && rustup update stable + - run: cargo build --release --lib + working-directory: "deckbuilder" + publish: + needs: [build-windows] runs-on: ubuntu-latest if: ${{ github.ref == 'refs/heads/main' }} diff --git a/.gitignore b/.gitignore index eaae4e3..33af5b5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ -# Godot 4+ specific ignores .godot/ android/ .vscode/ addons/ Cargo.lock steam_appid.txt -*/res/media/ \ No newline at end of file +*/res/media/ +target diff --git a/deckbuilder/Cargo.toml b/deckbuilder/Cargo.toml new file mode 100644 index 0000000..0d9191b --- /dev/null +++ b/deckbuilder/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "deckbuilder" +version = "0.1.0" +edition = "2021" + +[lib] +name = "deckbuilder" +path = "src/lib.rs" +crate-type = ["cdylib", "rlib"] + +[[bin]] +name = "deckbuilder_cli" +path = "src/main.rs" + +[dependencies] +rand = "0.8" +godot = { git = "https://github.com/godot-rust/gdext", branch = "master" } diff --git a/deckbuilder/src/lib.rs b/deckbuilder/src/lib.rs new file mode 100644 index 0000000..7d2ab37 --- /dev/null +++ b/deckbuilder/src/lib.rs @@ -0,0 +1,346 @@ +use godot::prelude::*; +use rand::seq::SliceRandom; + +mod logger; +mod tutorial; + +pub use tutorial::TutorialState; + +#[derive(Clone, Debug)] +pub struct Card { + pub name: String, + pub attack: u32, + pub defense: u32, +} + +pub struct CoreGameState { + pub player: Player, + pub enemy: Enemy, + pub logger: logger::GameLogger, +} +impl CoreGameState { + pub fn add_user_comment(&mut self, comment: String) { + let formatted_comment = format!("User Comment: {}", comment); + self.log(formatted_comment); + } + pub fn new() -> Self { + let mut player = Player::new(); + player.deck = initialize_deck(); + + // Draw initial hand + for _ in 0..5 { + player.draw_card(); + } + + let enemy = Enemy::new(20, 2); + let enemy_stats = format!( + "Enemy stats: Health = {}, Attack = {}", + enemy.health, enemy.attack + ); + + let mut core_state = Self { + player, + enemy, + logger: logger::GameLogger::new(), + }; + + core_state.log("Game started".to_string()); + core_state.log(enemy_stats); + core_state + } + + pub fn log(&mut self, message: String) { + self.logger.add_entry(message.clone()); + } + + pub fn draw_card(&mut self) { + self.player.draw_card(); + } + + pub fn play_card(&mut self, card_index: i32) -> String { + if card_index < 0 || card_index as usize >= self.player.hand.len() { + let message = "Invalid card index".to_string(); + self.log(message.clone()); + return message; + } + if self.player.hand.is_empty() { + let message = "No cards in hand".to_string(); + self.log(message.clone()); + return message; + } + let card = self.player.hand.remove(card_index as usize); + let result = handle_combat(&mut self.player, &mut self.enemy, &card); + self.log(format!("Played card: {}. {}", card.name, result)); + result + } + + pub fn enemy_turn(&mut self) { + let damage = self.enemy.attack; + self.player.health = self.player.health.saturating_sub(damage); + self.log(format!("Enemy attacks! You take {} damage.", damage)); + } + + pub fn get_player_health(&self) -> u32 { + self.player.health + } + + pub fn get_enemy_health(&self) -> u32 { + self.enemy.health + } + + pub fn get_hand(&self) -> Vec { + self.player.hand.clone() + } + + pub fn get_enemy_attack(&self) -> u32 { + self.enemy.attack + } + + pub fn check_game_over(&self) -> Option { + is_game_over(&self.player, &self.enemy) + } + + pub fn get_log(&self) -> &[String] { + self.logger.get_log() + } +} + +#[derive(GodotClass)] +#[class(base=Node)] +pub struct GameState { + #[base] + base: Base, + core: CoreGameState, + tutorial: Option, +} + +#[godot_api] +impl GameState { + #[func] + pub fn start_tutorial(&mut self) { + self.tutorial = Some(TutorialState::new()); + } + + #[func] + pub fn get_tutorial_instruction(&self) -> GString { + if let Some(tutorial) = &self.tutorial { + GString::from(tutorial.get_current_instruction()) + } else { + GString::from("Tutorial not started") + } + } + + #[func] + pub fn handle_tutorial_input(&mut self, input: GString) -> GString { + if let Some(tutorial) = &mut self.tutorial { + let response = tutorial.handle_input(&input.to_string()); + if tutorial.is_complete() { + self.tutorial = None; + } + GString::from(response) + } else { + GString::from("Tutorial not started") + } + } + + #[func] + pub fn add_user_comment(&mut self, comment: GString) { + self.core.add_user_comment(comment.to_string()); + } + + #[func] + pub fn get_log(&self) -> Vec { + self.core + .logger + .get_log() + .iter() + .map(|s| GString::from(s)) + .collect() + } + + pub fn new(base: Base) -> Self { + Self { + base, + core: CoreGameState::new(), + tutorial: None, + } + } + + #[func] + pub fn draw_card(&mut self) { + self.core.draw_card(); + } + + #[func] + pub fn play_card(&mut self, card_index: i32) -> GString { + GString::from(self.core.play_card(card_index)) + } + #[func] + pub fn enemy_turn(&mut self) { + let damage = self.core.enemy.attack; + self.core.player.health = self.core.player.health.saturating_sub(damage); + godot_print!("Enemy attacks! You take {} damage.", damage); + } + + #[func] + pub fn get_player_health(&self) -> u32 { + self.core.player.health + } + + #[func] + pub fn get_enemy_health(&self) -> u32 { + self.core.enemy.health + } + + #[func] + pub fn get_hand(&self) -> Vec { + self.core + .player + .hand + .iter() + .map(|card| { + let mut dict = Dictionary::new(); + dict.insert("name", card.name.clone()).unwrap(); + dict.insert("attack", card.attack).unwrap(); + dict.insert("defense", card.defense).unwrap(); + dict + }) + .collect() + } + + #[func] + pub fn get_enemy_attack(&self) -> u32 { + self.core.enemy.attack + } + + #[func] + pub fn check_game_over(&self) -> GString { + match is_game_over(&self.core.player, &self.core.enemy) { + Some(message) => GString::from(message), + None => GString::new(), + } + } + + #[func] + pub fn is_tutorial_active(&self) -> bool { + self.tutorial.is_some() + } +} + +#[godot_api] +impl INode for GameState { + fn init(base: Base) -> Self { + Self { + base, + core: CoreGameState::new(), + tutorial: None, + } + } +} + +#[derive(Default)] +pub struct Player { + pub deck: Vec, + pub hand: Vec, + pub health: u32, +} + +impl Player { + pub fn new() -> Self { + Player { + deck: Vec::new(), + hand: Vec::new(), + health: 30, + } + } + + pub fn draw_card(&mut self) { + if let Some(card) = self.deck.pop() { + self.hand.push(card); + } + } +} + +#[derive(Default)] +pub struct Enemy { + pub health: u32, + pub attack: u32, +} + +impl Enemy { + pub fn new(health: u32, attack: u32) -> Self { + Enemy { health, attack } + } +} +pub fn handle_combat(player: &mut Player, enemy: &mut Enemy, card: &Card) -> String { + let mut combat_log = String::new(); + + // Player attacks enemy + let enemy_health_before = enemy.health; + enemy.health = enemy.health.saturating_sub(card.attack); + let damage_dealt = enemy_health_before - enemy.health; + combat_log.push_str(&format!( + "You attack with {}. Enemy takes {} damage.", + card.name, damage_dealt + )); + + // Enemy counterattacks if still alive + if enemy.health > 0 { + let enemy_attack = enemy.attack; + let damage_blocked = enemy_attack.saturating_sub(card.defense); + let damage_taken = damage_blocked; + player.health = player.health.saturating_sub(damage_taken); + combat_log.push_str(&format!( + "\nEnemy counterattacks with {} damage. ", + enemy_attack + )); + if damage_blocked < enemy_attack { + combat_log.push_str(&format!( + "You block {} damage with your {}'s defense. ", + enemy_attack - damage_blocked, + card.name + )); + } + combat_log.push_str(&format!("You take {} damage.", damage_taken)); + } + + combat_log +} + +pub fn initialize_deck() -> Vec { + let initial_cards = vec![ + Card { + name: "Warrior".to_string(), + attack: 3, + defense: 2, + }, + Card { + name: "Archer".to_string(), + attack: 2, + defense: 1, + }, + Card { + name: "Knight".to_string(), + attack: 4, + defense: 4, + }, + ]; + + let mut deck = Vec::new(); + for _ in 0..5 { + deck.extend(initial_cards.clone()); + } + + deck.shuffle(&mut rand::thread_rng()); + deck +} + +pub fn is_game_over(player: &Player, enemy: &Enemy) -> Option { + if player.health <= 0 { + Some("Game Over! You lost.".to_string()) + } else if enemy.health <= 0 { + Some("Congratulations! You defeated the enemy.".to_string()) + } else { + None + } +} diff --git a/deckbuilder/src/logger.rs b/deckbuilder/src/logger.rs new file mode 100644 index 0000000..d638180 --- /dev/null +++ b/deckbuilder/src/logger.rs @@ -0,0 +1,19 @@ +pub struct GameLogger { + log: Vec, +} + +impl GameLogger { + pub fn new() -> Self { + Self { log: Vec::new() } + } + + pub fn add_entry(&mut self, entry: String) { + let entry_clone = entry.clone(); + self.log.push(entry); + println!("{}", entry_clone); // Print to console immediately + } + + pub fn get_log(&self) -> &[String] { + &self.log + } +} diff --git a/deckbuilder/src/main.rs b/deckbuilder/src/main.rs new file mode 100644 index 0000000..77c355f --- /dev/null +++ b/deckbuilder/src/main.rs @@ -0,0 +1,125 @@ +use deckbuilder::{CoreGameState, TutorialState}; +use std::io; + +fn display_recent_logs(game: &CoreGameState, num_entries: usize) { + let log = game.get_log(); + if log.is_empty() { + println!("No recent game log entries."); + } else { + println!("Recent game log:"); + let entries: Vec<_> = log.iter().take(num_entries).collect(); + for entry in entries.iter() { + println!(" {}", entry); + } + } + println!(); +} + +fn main() { + let mut game = CoreGameState::new(); + + println!( + "Enemy: Health = {}, Attack = {}", + game.enemy.health, game.enemy.attack + ); + + // Main game loop + loop { + // Display player's hand + println!("Your hand:"); + for (i, card) in game.player.hand.iter().enumerate() { + println!( + "{}. {} (Attack: {}, Defense: {})", + i + 1, + card.name, + card.attack, + card.defense + ); + } + + // Player's turn + println!( + "Enter the number of the card you want to play, 'c' to add a comment, 't' for tutorial, or 'q' to quit:" + ); + let mut input = String::new(); + io::stdin() + .read_line(&mut input) + .expect("Failed to read line"); + + let input = input.trim(); + + match input { + "q" => break, + "c" => { + println!("Enter your comment:"); + let mut comment = String::new(); + io::stdin() + .read_line(&mut comment) + .expect("Failed to read comment"); + game.add_user_comment(comment.trim().to_string()); + println!("Comment added to the log."); + continue; + } + "t" => { + let mut tutorial = TutorialState::new(); + loop { + let instruction = tutorial.get_current_instruction(); + println!("{}", instruction); + let mut tutorial_input = String::new(); + io::stdin() + .read_line(&mut tutorial_input) + .expect("Failed to read tutorial input"); + let response = tutorial.handle_input(tutorial_input.trim()); + if !response.is_empty() { + println!("{}", response); + } + if tutorial.is_complete() { + break; + } + } + // Update the game state with the tutorial's core game + game = tutorial.core_game; + println!("Tutorial completed. Returning to the main game."); + println!( + "Enemy: Health = {}, Attack = {}", + game.enemy.health, game.enemy.attack + ); + continue; + } + _ => { + // Process player's move + if let Ok(index) = input.parse::() { + if index > 0 && index <= game.get_hand().len() { + let result = game.play_card(index as i32 - 1); + println!("{}", result); + } else { + println!("Invalid card number. Please try again."); + continue; + } + } else { + println!( + "Invalid input. Please enter a number, 'c' to comment, 't' for tutorial, or 'q' to quit." + ); + continue; + } + } + } + + // Display updated status + println!("Player Health: {}", game.get_player_health()); + println!("Enemy Health: {}", game.get_enemy_health()); + + // Enemy's turn + game.enemy_turn(); + println!("Player Health: {}", game.get_player_health()); + + // Display recent log entries + display_recent_logs(&game, 5); + + // Check for win/lose conditions + if let Some(game_over_message) = game.check_game_over() { + println!("{}", game_over_message); + break; + } + } +} diff --git a/deckbuilder/src/tutorial.rs b/deckbuilder/src/tutorial.rs new file mode 100644 index 0000000..0e6e57f --- /dev/null +++ b/deckbuilder/src/tutorial.rs @@ -0,0 +1,156 @@ +use crate::{Card, CoreGameState}; + +pub struct TutorialState { + pub step: usize, + pub core_game: CoreGameState, + enemy_health_before: u32, + player_health_before: u32, + card_played: Option, + enemy_damage_dealt: u32, + player_damage_taken: u32, + enemy_turn_damage: u32, +} + +impl TutorialState { + pub fn is_complete(&self) -> bool { + self.step >= 7 + } + + pub fn new() -> Self { + let core_game = CoreGameState::new(); + let enemy_health_before = core_game.enemy.health; + let player_health_before = core_game.player.health; + Self { + step: 0, + core_game, + enemy_health_before, + player_health_before, + card_played: None, + enemy_damage_dealt: 0, + player_damage_taken: 0, + enemy_turn_damage: 0, + } + } + + pub fn next_step(&mut self) -> String { + self.step += 1; + self.get_current_instruction() + } + + pub fn get_current_instruction(&self) -> String { + match self.step { + 0 => "Welcome to the Deckbuilder Tutorial! Let's start by looking at your hand. Press Enter or type 'next' to continue.".to_string(), + 1 => "You start with 5 cards in your hand. Each card has an Attack and Defense value. Press Enter or type 'next' to continue.".to_string(), + 2 => "Let's play your first card. Type '1' to play the first card in your hand.".to_string(), + 3 => { + let card = self.card_played.as_ref().unwrap(); + format!( + r#"-------------------------------------------------- +BATTLE ACTION SUMMARY: +-------------------------------------------------- +1. Your Move: + You played: {} (Attack: {}, Defense: {}) + +2. Your Attack: + Enemy Health: {} -> {} (*-{} damage*) + +3. Enemy Counterattack: + Your Health: {} -> {} (*-{} damage*) + {} + +4. Final Status: + YOU - Health: {}, Cards in hand: {} + ENEMY - Health: {}, Attack: {} +-------------------------------------------------- +Great job! You've successfully attacked the enemy and survived their counterattack. +Press Enter or type 'next' to continue the tutorial."#, + card.name, card.attack, card.defense, + self.enemy_health_before, self.core_game.enemy.health, self.enemy_damage_dealt, + self.player_health_before, self.core_game.player.health, self.player_damage_taken, + if self.player_damage_taken < self.core_game.enemy.attack { + format!("(You blocked {} damage with your {}'s defense!)", + self.core_game.enemy.attack - self.player_damage_taken, card.name) + } else { + String::new() + }, + self.core_game.player.health, self.core_game.player.hand.len(), + self.core_game.enemy.health, self.core_game.enemy.attack + ) + }, + 4 => "Now it's the enemy's turn. They will attack you. Press Enter or type 'next' to see what happens.".to_string(), + 5 => format!( + "The enemy attacked you! You took {} damage. Your health decreased from {} to {}. The game continues until either you or the enemy runs out of health. Press Enter or type 'next' to continue.", + self.enemy_turn_damage, + self.player_health_before, + self.core_game.player.health + ), + 6 => "That's the basics of combat! Keep playing cards and defeating enemies. Press Enter or type 'end' to finish the tutorial.".to_string(), + _ => "Tutorial complete! You can now start a real game.".to_string(), + } + } + + pub fn handle_input(&mut self, input: &str) -> String { + match self.step { + 2 => { + if input == "1" { + self.enemy_health_before = self.core_game.enemy.health; + self.player_health_before = self.core_game.player.health; + self.card_played = self.core_game.player.hand.get(0).cloned(); + let result = self.core_game.play_card(0); + println!("{}", result); // Print the result of playing the card + if let Some(card) = &self.card_played { + self.enemy_damage_dealt = + self.enemy_health_before - self.core_game.enemy.health; + self.player_damage_taken = + self.player_health_before - self.core_game.player.health; + } + self.next_step(); + String::new() // Return empty string when advancing + } else { + "Please type '1' to play the first card.".to_string() + } + } + + 4 => { + if input.is_empty() || input == "next" { + self.player_health_before = self.core_game.player.health; + self.core_game.enemy_turn(); + self.enemy_turn_damage = + self.player_health_before - self.core_game.player.health; + self.next_step(); + format!( + "The enemy attacks! You take {} damage.", + self.enemy_turn_damage + ) + } else { + "Press Enter or type 'next' to see what happens.".to_string() + } + } + 5 => { + if input.is_empty() || input == "next" { + self.next_step(); + String::new() + } else { + "Press Enter or type 'next' to continue.".to_string() + } + } + + 6 => { + if input == "end" || input.is_empty() { + self.next_step(); + String::new() // Return empty string when advancing + } else { + "Type 'end' or press Enter to finish the tutorial.".to_string() + } + } + _ => { + if input == "next" || input.is_empty() { + self.next_step(); + String::new() // Return empty string when advancing + } else { + "Press Enter or type 'next' to continue.".to_string() + } + } + } + } +}