From 5708a47157c9c046f7d8375859e752edb085cb87 Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Thu, 16 Mar 2023 14:00:20 +0000 Subject: [PATCH] Snake: Implement image-based skins Co-authored-by: HawDevelopment --- .../graphics/snake/skins/ladybird/corner.png | Bin 0 -> 206 bytes .../graphics/snake/skins/ladybird/head.png | Bin 0 -> 210 bytes .../snake/skins/ladybird/horizontal.png | Bin 0 -> 191 bytes .../graphics/snake/skins/ladybird/tail.png | Bin 0 -> 201 bytes .../snake/skins/ladybird/vertical.png | Bin 0 -> 201 bytes .../res/graphics/snake/skins/snake/corner.png | Bin 0 -> 141 bytes Base/res/graphics/snake/skins/snake/head.png | Bin 0 -> 168 bytes .../graphics/snake/skins/snake/horizontal.png | Bin 0 -> 120 bytes Base/res/graphics/snake/skins/snake/tail.png | Bin 0 -> 146 bytes .../graphics/snake/skins/snake/vertical.png | Bin 0 -> 125 bytes Userland/Games/Snake/CMakeLists.txt | 3 + Userland/Games/Snake/Game.cpp | 109 ++++++++++++++---- Userland/Games/Snake/Game.h | 22 +++- Userland/Games/Snake/Skins/ClassicSkin.cpp | 48 ++++++++ Userland/Games/Snake/Skins/ClassicSkin.h | 32 +++++ Userland/Games/Snake/Skins/ImageSkin.cpp | 103 +++++++++++++++++ Userland/Games/Snake/Skins/ImageSkin.h | 38 ++++++ Userland/Games/Snake/Skins/SnakeSkin.cpp | 28 +++++ Userland/Games/Snake/Skins/SnakeSkin.h | 27 +++++ Userland/Games/Snake/main.cpp | 48 +++++++- 20 files changed, 425 insertions(+), 33 deletions(-) create mode 100644 Base/res/graphics/snake/skins/ladybird/corner.png create mode 100644 Base/res/graphics/snake/skins/ladybird/head.png create mode 100644 Base/res/graphics/snake/skins/ladybird/horizontal.png create mode 100644 Base/res/graphics/snake/skins/ladybird/tail.png create mode 100644 Base/res/graphics/snake/skins/ladybird/vertical.png create mode 100644 Base/res/graphics/snake/skins/snake/corner.png create mode 100644 Base/res/graphics/snake/skins/snake/head.png create mode 100644 Base/res/graphics/snake/skins/snake/horizontal.png create mode 100644 Base/res/graphics/snake/skins/snake/tail.png create mode 100644 Base/res/graphics/snake/skins/snake/vertical.png create mode 100644 Userland/Games/Snake/Skins/ClassicSkin.cpp create mode 100644 Userland/Games/Snake/Skins/ClassicSkin.h create mode 100644 Userland/Games/Snake/Skins/ImageSkin.cpp create mode 100644 Userland/Games/Snake/Skins/ImageSkin.h create mode 100644 Userland/Games/Snake/Skins/SnakeSkin.cpp create mode 100644 Userland/Games/Snake/Skins/SnakeSkin.h diff --git a/Base/res/graphics/snake/skins/ladybird/corner.png b/Base/res/graphics/snake/skins/ladybird/corner.png new file mode 100644 index 0000000000000000000000000000000000000000..17ce6b5ead606d89308d41cff41c83a87745b211 GIT binary patch literal 206 zcmeAS@N?(olHy`uVBq!ia0y~yU;weXIG7n27|*`umtLk$1_S02?hb_-l3WvV-C?QB^g7H#(H vZHbMpH)nlWwGr(F+?Ifw~w3efC7heqIi5+ z(RHyzzFN;vEqm>}&5hk%*(WY7&Uqkkw_*vi_p+VVulLnid{eX6Dt21GOn~i8rDoQN zD_q>7=kH1TKl}1f_Mf49a+0Zt+?3?3`5{H0Oys7zAI(bBXB7!Kd%)r>Cj$cmgQu&X J%Q~loCICj-LfQZT literal 0 HcmV?d00001 diff --git a/Base/res/graphics/snake/skins/ladybird/horizontal.png b/Base/res/graphics/snake/skins/ladybird/horizontal.png new file mode 100644 index 0000000000000000000000000000000000000000..001490fb78ad6195455392320ea8b1dcab3b1fc7 GIT binary patch literal 191 zcmeAS@N?(olHy`uVBq!ia0y~yU;weXIM^5%7=M}m*JfZ~U@Q)DcVbv~PUa;80|QIC zqpu?a!^VE@KZ&di3=EtF9+AZi419+`m{C;2tAT-mA;i4ZQ&1_2&t>+ky~ zZQi&^SLgFnRj2H!Y%(toY}o9ix{Cj_Kx}iF%C$S2R`zJ0Ijug?=JDBY-N!iRe=-mL r=$jelQvQ7EDtqUX4>$c}Kfuy{MtZ|)e{ORI1_lOCS3j3^P6YCqs zgp)dFrm1Yv>eB6yzA;@XX?xGFqytT=tlFL)&pxs&HB$6%H*-Cha@I~%ZvBT>yRIH( z)9XInbJ*u?`Y&elQ(BgZB{ux)?#oE;w*J6WWTO6C{)3wY0|Nttr>mdKI;Vst0F~M+)pu5AiCkbvOZ|{s0W#9l)z4*}Q$iB}F=8oM literal 0 HcmV?d00001 diff --git a/Base/res/graphics/snake/skins/snake/head.png b/Base/res/graphics/snake/skins/snake/head.png new file mode 100644 index 0000000000000000000000000000000000000000..a1c293f4628b5913560ed6c3b7b0e6a27a330838 GIT binary patch literal 168 zcmeAS@N?(olHy`uVBq!ia0y~yU;weXIG7n27|*`umtzDU{ OZ1HsUb6Mw<&;$Tan>zpi literal 0 HcmV?d00001 diff --git a/Base/res/graphics/snake/skins/snake/horizontal.png b/Base/res/graphics/snake/skins/snake/horizontal.png new file mode 100644 index 0000000000000000000000000000000000000000..67c163047c62734461d7a49a127c23366d7cb93a GIT binary patch literal 120 zcmeAS@N?(olHy`uVBq!ia0y~yU;weXIM^5%7=M}m*JfZ~u=jLv43P*=ZeVoVTGjSH zz)YZ_qQ~z4#MT82dR*5VLuG}hsPS}M?+X<;`%s`ER6zQ`J9nLijQ;}f3m-8sGczdi X2=8(^|L8FT0|SGntDnm{r-UW|gB2wt literal 0 HcmV?d00001 diff --git a/Base/res/graphics/snake/skins/snake/tail.png b/Base/res/graphics/snake/skins/snake/tail.png new file mode 100644 index 0000000000000000000000000000000000000000..56972d8627341b08adf94d40e4476333f199d320 GIT binary patch literal 146 zcmeAS@N?(olHy`uVBq!ia0y~yU;weXn3x$D7~7-8cQ7z8@C5jTxT@7N`21&J_^-&o zz>sVqHj{yYfu$tKFZloe{|pXVE59%>Fc^BeIEHXsPqsKv&~T8EL*IafVIjleP`0+& yJUolo7kGSPXgFfq}Us$S?T+e}?O)cpVuS7!*8R978y+CubyNBor_l;^bjWu$ao&;vyXokiCM* aijBc}5%;(A>*wtQ8R6;b=d#Wzp$Pzr1s~@C literal 0 HcmV?d00001 diff --git a/Userland/Games/Snake/CMakeLists.txt b/Userland/Games/Snake/CMakeLists.txt index c862075738..687f0d419f 100644 --- a/Userland/Games/Snake/CMakeLists.txt +++ b/Userland/Games/Snake/CMakeLists.txt @@ -9,6 +9,9 @@ compile_gml(Snake.gml SnakeGML.h snake_gml) set(SOURCES Game.cpp main.cpp + Skins/ClassicSkin.cpp + Skins/ImageSkin.cpp + Skins/SnakeSkin.cpp ) set(GENERATED_SOURCES diff --git a/Userland/Games/Snake/Game.cpp b/Userland/Games/Snake/Game.cpp index 5442c8c4d9..e440037721 100644 --- a/Userland/Games/Snake/Game.cpp +++ b/Userland/Games/Snake/Game.cpp @@ -2,6 +2,7 @@ * Copyright (c) 2018-2020, Andreas Kling * Copyright (c) 2021, Mustafa Quraish * Copyright (c) 2022, the SerenityOS developers. + * Copyright (c) 2023, Sam Atkins * * SPDX-License-Identifier: BSD-2-Clause */ @@ -66,16 +67,21 @@ ErrorOr> Game::try_create() food_bitmaps.unchecked_append(bitmap.release_value()); } - return adopt_nonnull_ref_or_enomem(new (nothrow) Game(move(food_bitmaps))); + auto color = Color::from_argb(Config::read_u32("Snake"sv, "Snake"sv, "BaseColor"sv, Color(Color::Green).value())); + auto skin_name = Config::read_string("Snake"sv, "Snake"sv, "SnakeSkin"sv, "classic"sv); + auto skin = TRY(SnakeSkin::create(skin_name, color)); + + return adopt_nonnull_ref_or_enomem(new (nothrow) Game(move(food_bitmaps), color, skin_name, move(skin))); } -Game::Game(Vector> food_bitmaps) +Game::Game(Vector> food_bitmaps, Color snake_color, DeprecatedString snake_skin_name, NonnullOwnPtr skin) : m_food_bitmaps(move(food_bitmaps)) + , m_snake_color(move(snake_color)) + , m_snake_skin_name(move(snake_skin_name)) + , m_snake_skin(move(skin)) { set_font(Gfx::FontDatabase::default_fixed_width_font().bold_variant()); reset(); - - m_snake_base_color = Color::from_argb(Config::read_u32("Snake"sv, "Snake"sv, "BaseColor"sv, m_snake_base_color.value())); } void Game::pause() @@ -107,12 +113,6 @@ void Game::reset() update(); } -void Game::set_snake_base_color(Color color) -{ - Config::write_u32("Snake"sv, "Snake"sv, "BaseColor"sv, color.value()); - m_snake_base_color = color; -} - bool Game::is_available(Coordinate const& coord) { for (size_t i = 0; i < m_tail.size(); ++i) { @@ -154,6 +154,7 @@ void Game::timer_event(Core::TimerEvent&) m_velocity = m_velocity_queue.dequeue(); dirty_cells.append(m_head); + dirty_cells.append(m_tail.last()); m_head.row += m_velocity.vertical; m_head.column += m_velocity.horizontal; @@ -248,19 +249,19 @@ void Game::paint_event(GUI::PaintEvent& event) painter.add_clip_rect(event.rect()); painter.fill_rect(event.rect(), Color::Black); - painter.fill_rect(cell_rect(m_head), m_snake_base_color); - for (auto& part : m_tail) { - auto rect = cell_rect(part); - painter.fill_rect(rect, m_snake_base_color.darkened(0.77)); + auto head_rect = cell_rect(m_head); + m_snake_skin->draw_head(painter, head_rect, m_last_velocity.as_direction()); - Gfx::IntRect left_side(rect.x(), rect.y(), 2, rect.height()); - Gfx::IntRect top_side(rect.x(), rect.y(), rect.width(), 2); - Gfx::IntRect right_side(rect.right() - 1, rect.y(), 2, rect.height()); - Gfx::IntRect bottom_side(rect.x(), rect.bottom() - 1, rect.width(), 2); - painter.fill_rect(left_side, m_snake_base_color.darkened(0.88)); - painter.fill_rect(right_side, m_snake_base_color.darkened(0.55)); - painter.fill_rect(top_side, m_snake_base_color.darkened(0.88)); - painter.fill_rect(bottom_side, m_snake_base_color.darkened(0.55)); + for (size_t i = 0; i < m_tail.size(); i++) { + auto previous_position = i > 0 ? m_tail[i - 1] : m_head; + auto rect = cell_rect(m_tail[i]); + + if (i == m_tail.size() - 1) { + m_snake_skin->draw_tail(painter, rect, direction_to_position(m_tail[i], previous_position)); + continue; + } + + m_snake_skin->draw_body(painter, rect, direction_to_position(m_tail[i], previous_position), direction_to_position(m_tail[i], m_tail[i + 1])); } painter.draw_scaled_bitmap(cell_rect(m_fruit), m_food_bitmaps[m_fruit_type], m_food_bitmaps[m_fruit_type]->rect()); @@ -298,4 +299,68 @@ Velocity const& Game::last_velocity() const return m_last_velocity; } +Direction Game::direction_to_position(Snake::Coordinate const& from, Snake::Coordinate const& to) const +{ + auto x_difference = to.column - from.column; + auto y_difference = to.row - from.row; + + if (y_difference == 1) + return Direction::Down; + if (y_difference == -1) + return Direction::Up; + if (y_difference != 0) { + // We wrapped around the screen, so invert the direction. + return (y_difference > 0) ? Direction::Up : Direction::Down; + } + + if (x_difference == 1) + return Direction::Right; + if (x_difference == -1) + return Direction::Left; + if (x_difference != 0) { + // We wrapped around the screen, so invert the direction. + return (x_difference > 0) ? Direction::Left : Direction::Right; + } + + VERIFY_NOT_REACHED(); +} + +void Game::config_string_did_change(DeprecatedString const& domain, DeprecatedString const& group, DeprecatedString const& key, DeprecatedString const& value) +{ + if (domain == "Snake"sv && group == "Snake"sv && key == "SnakeSkin"sv) { + set_skin_name(value); + return; + } +} + +void Game::config_u32_did_change(DeprecatedString const& domain, DeprecatedString const& group, DeprecatedString const& key, u32 value) +{ + if (domain == "Snake"sv && group == "Snake"sv && key == "BaseColor"sv) { + set_skin_color(Color::from_argb(value)); + return; + } +} + +void Game::set_skin_color(Gfx::Color color) +{ + if (m_snake_color != color) { + m_snake_color = color; + set_skin(SnakeSkin::create(m_snake_skin_name, m_snake_color).release_value_but_fixme_should_propagate_errors()); + } +} + +void Game::set_skin_name(DeprecatedString name) +{ + if (m_snake_skin_name != name) { + m_snake_skin_name = name; + set_skin(SnakeSkin::create(m_snake_skin_name, m_snake_color).release_value_but_fixme_should_propagate_errors()); + } +} + +void Game::set_skin(NonnullOwnPtr skin) +{ + m_snake_skin = move(skin); + update(); +} + } diff --git a/Userland/Games/Snake/Game.h b/Userland/Games/Snake/Game.h index da3a57427f..955c936f59 100644 --- a/Userland/Games/Snake/Game.h +++ b/Userland/Games/Snake/Game.h @@ -9,12 +9,16 @@ #pragma once #include "Geometry.h" +#include "Skins/SnakeSkin.h" #include +#include #include namespace Snake { -class Game : public GUI::Frame { +class Game + : public GUI::Frame + , public Config::Listener { C_OBJECT_ABSTRACT(Game); public: @@ -27,23 +31,29 @@ public: void pause(); void reset(); - void set_snake_base_color(Color color); - Function on_score_update; + void set_skin_color(Color); + void set_skin_name(DeprecatedString); + void set_skin(NonnullOwnPtr skin); + private: - explicit Game(Vector> food_bitmaps); + explicit Game(Vector> food_bitmaps, Color snake_color, DeprecatedString snake_skin_name, NonnullOwnPtr skin); virtual void paint_event(GUI::PaintEvent&) override; virtual void keydown_event(GUI::KeyEvent&) override; virtual void timer_event(Core::TimerEvent&) override; + virtual void config_string_did_change(DeprecatedString const& domain, DeprecatedString const& group, DeprecatedString const& key, DeprecatedString const& value) override; + void config_u32_did_change(DeprecatedString const& domain, DeprecatedString const& group, DeprecatedString const& key, u32 value) override; + void game_over(); void spawn_fruit(); bool is_available(Coordinate const&); void queue_velocity(int v, int h); Velocity const& last_velocity() const; Gfx::IntRect cell_rect(Coordinate const&) const; + Direction direction_to_position(Coordinate const& from, Coordinate const& to) const; int m_rows { 20 }; int m_columns { 20 }; @@ -65,7 +75,9 @@ private: Vector> m_food_bitmaps; - Gfx::Color m_snake_base_color { Color::Yellow }; + Color m_snake_color; + DeprecatedString m_snake_skin_name; + NonnullOwnPtr m_snake_skin; }; } diff --git a/Userland/Games/Snake/Skins/ClassicSkin.cpp b/Userland/Games/Snake/Skins/ClassicSkin.cpp new file mode 100644 index 0000000000..2d4ae725ec --- /dev/null +++ b/Userland/Games/Snake/Skins/ClassicSkin.cpp @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling + * Copyright (c) 2021, Mustafa Quraish + * Copyright (c) 2023, the SerenityOS developers. + * Copyright (c) 2023, Sam Atkins + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "ClassicSkin.h" + +namespace Snake { + +ClassicSkin::ClassicSkin(Color color) + : m_skin_color(color) +{ +} + +void ClassicSkin::draw_tile_at(Gfx::Painter& painter, Gfx::IntRect const& rect) +{ + painter.fill_rect(rect, m_skin_color.darkened(0.77)); + + Gfx::IntRect left_side(rect.x(), rect.y(), 2, rect.height()); + Gfx::IntRect top_side(rect.x(), rect.y(), rect.width(), 2); + Gfx::IntRect right_side(rect.right() - 1, rect.y(), 2, rect.height()); + Gfx::IntRect bottom_side(rect.x(), rect.bottom() - 1, rect.width(), 2); + auto top_left_color = m_skin_color.lightened(0.88); + auto bottom_right_color = m_skin_color.darkened(0.55); + painter.fill_rect(left_side, top_left_color); + painter.fill_rect(right_side, bottom_right_color); + painter.fill_rect(top_side, top_left_color); + painter.fill_rect(bottom_side, bottom_right_color); +} + +void ClassicSkin::draw_head(Gfx::Painter& painter, Gfx::IntRect const& head, Direction) +{ + painter.fill_rect(head, m_skin_color); +} +void ClassicSkin::draw_body(Gfx::Painter& painter, Gfx::IntRect const& rect, Direction, Direction) +{ + draw_tile_at(painter, rect); +} +void ClassicSkin::draw_tail(Gfx::Painter& painter, Gfx::IntRect const& tail, Direction) +{ + draw_tile_at(painter, tail); +} + +} diff --git a/Userland/Games/Snake/Skins/ClassicSkin.h b/Userland/Games/Snake/Skins/ClassicSkin.h new file mode 100644 index 0000000000..9a742eeb74 --- /dev/null +++ b/Userland/Games/Snake/Skins/ClassicSkin.h @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling + * Copyright (c) 2021, Mustafa Quraish + * Copyright (c) 2023, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include "SnakeSkin.h" +#include + +namespace Snake { + +class ClassicSkin : public SnakeSkin { +public: + ClassicSkin(Color); + + virtual ~ClassicSkin() override = default; + + void draw_head(Gfx::Painter&, Gfx::IntRect const& head, Direction body_direction) override; + void draw_body(Gfx::Painter&, Gfx::IntRect const& rect, Direction previous_direction, Direction next_direction) override; + void draw_tail(Gfx::Painter& painter, Gfx::IntRect const& tail, Direction body_direction) override; + +private: + void draw_tile_at(Gfx::Painter&, Gfx::IntRect const&); + + Gfx::Color m_skin_color = { Color::Yellow }; +}; + +} diff --git a/Userland/Games/Snake/Skins/ImageSkin.cpp b/Userland/Games/Snake/Skins/ImageSkin.cpp new file mode 100644 index 0000000000..8f9e0ae7db --- /dev/null +++ b/Userland/Games/Snake/Skins/ImageSkin.cpp @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling + * Copyright (c) 2021, Mustafa Quraish + * Copyright (c) 2023, the SerenityOS developers. + * Copyright (c) 2023, Sam Atkins + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "ImageSkin.h" +#include + +namespace Snake { + +ErrorOr> ImageSkin::create(StringView skin_name) +{ + auto skin_directory = TRY(Core::Directory::create(DeprecatedString::formatted("/res/graphics/snake/skins/{}", skin_name), Core::Directory::CreateDirectories::No)); + + auto head = TRY(Gfx::Bitmap::load_from_file(TRY(skin_directory.open("head.png"sv, Core::File::OpenMode::Read)), "head.png"sv)); + Vector> head_bitmaps; + TRY(head_bitmaps.try_ensure_capacity(4)); + TRY(head_bitmaps.try_append(head)); + TRY(head_bitmaps.try_append(TRY(head->rotated(Gfx::RotationDirection::Clockwise)))); + TRY(head_bitmaps.try_append(TRY(head_bitmaps[1]->rotated(Gfx::RotationDirection::Clockwise)))); + TRY(head_bitmaps.try_append(TRY(head_bitmaps[2]->rotated(Gfx::RotationDirection::Clockwise)))); + + Vector> body_bitmaps; + TRY(body_bitmaps.try_ensure_capacity(16)); + auto tail_up = TRY(Gfx::Bitmap::load_from_file(TRY(skin_directory.open("tail.png"sv, Core::File::OpenMode::Read)), "tail.png"sv)); + auto tail_right = TRY(tail_up->rotated(Gfx::RotationDirection::Clockwise)); + auto tail_down = TRY(tail_right->rotated(Gfx::RotationDirection::Clockwise)); + auto tail_left = TRY(tail_down->rotated(Gfx::RotationDirection::Clockwise)); + auto corner_ur = TRY(Gfx::Bitmap::load_from_file(TRY(skin_directory.open("corner.png"sv, Core::File::OpenMode::Read)), "corner.png"sv)); + auto corner_dr = TRY(corner_ur->rotated(Gfx::RotationDirection::Clockwise)); + auto corner_dl = TRY(corner_dr->rotated(Gfx::RotationDirection::Clockwise)); + auto corner_ul = TRY(corner_dl->rotated(Gfx::RotationDirection::Clockwise)); + auto horizontal = TRY(Gfx::Bitmap::load_from_file(TRY(skin_directory.open("horizontal.png"sv, Core::File::OpenMode::Read)), "horizontal.png"sv)); + auto vertical = TRY(Gfx::Bitmap::load_from_file(TRY(skin_directory.open("vertical.png"sv, Core::File::OpenMode::Read)), "vertical.png"sv)); + + TRY(body_bitmaps.try_append(tail_up)); + TRY(body_bitmaps.try_append(corner_ur)); + TRY(body_bitmaps.try_append(vertical)); + TRY(body_bitmaps.try_append(corner_ul)); + + TRY(body_bitmaps.try_append(corner_ur)); + TRY(body_bitmaps.try_append(tail_right)); + TRY(body_bitmaps.try_append(corner_dr)); + TRY(body_bitmaps.try_append(horizontal)); + + TRY(body_bitmaps.try_append(vertical)); + TRY(body_bitmaps.try_append(corner_dr)); + TRY(body_bitmaps.try_append(tail_down)); + TRY(body_bitmaps.try_append(corner_dl)); + + TRY(body_bitmaps.try_append(corner_ul)); + TRY(body_bitmaps.try_append(horizontal)); + TRY(body_bitmaps.try_append(corner_dl)); + TRY(body_bitmaps.try_append(tail_left)); + + return adopt_nonnull_own_or_enomem(new (nothrow) ImageSkin(skin_name, move(head_bitmaps), move(body_bitmaps))); +} + +ImageSkin::ImageSkin(StringView skin_name, Vector> head_bitmaps, Vector> body_bitmaps) + : m_skin_name(skin_name) + , m_head_bitmaps(move(head_bitmaps)) + , m_body_bitmaps(move(body_bitmaps)) +{ +} + +static int image_index_from_directions(Direction from, Direction to) +{ + // Sprites are ordered in memory like this, to make the calculation easier: + // + // From direction + // U R D L + // ╹ ┗ ┃ ┛ Up To direction + // ┗ ╺ ┏ ━ Right + // ┃ ┏ ╻ ┓ Down + // ┛ ━ ┓ ╸ Left + // (Numbered 0-15, starting top left, one row at a time.) + // + // This does cause some redundancy for now, but RefPtrs are small. + return to_underlying(to) * 4 + to_underlying(from); +} + +void ImageSkin::draw_head(Gfx::Painter& painter, Gfx::IntRect const& head, Direction facing_direction) +{ + auto& bitmap = m_head_bitmaps[to_underlying(facing_direction)]; + painter.draw_scaled_bitmap(head, bitmap, bitmap->rect()); +} + +void ImageSkin::draw_body(Gfx::Painter& painter, Gfx::IntRect const& rect, Direction previous_direction, Direction next_direction) +{ + auto& bitmap = m_body_bitmaps[image_index_from_directions(previous_direction, next_direction)]; + painter.draw_scaled_bitmap(rect, bitmap, bitmap->rect()); +} + +void ImageSkin::draw_tail(Gfx::Painter& painter, Gfx::IntRect const& rect, Direction body_direction) +{ + draw_body(painter, rect, body_direction, body_direction); +} + +} diff --git a/Userland/Games/Snake/Skins/ImageSkin.h b/Userland/Games/Snake/Skins/ImageSkin.h new file mode 100644 index 0000000000..037c2fe127 --- /dev/null +++ b/Userland/Games/Snake/Skins/ImageSkin.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling + * Copyright (c) 2021, Mustafa Quraish + * Copyright (c) 2023, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include "SnakeSkin.h" +#include +#include +#include +#include + +namespace Snake { + +class ImageSkin : public SnakeSkin { +public: + static ErrorOr> create(StringView skin_name); + + virtual ~ImageSkin() override = default; + + void draw_head(Gfx::Painter&, Gfx::IntRect const& head, Direction facing_direction) override; + void draw_body(Gfx::Painter&, Gfx::IntRect const& rect, Direction previous_direction, Direction next_direction) override; + void draw_tail(Gfx::Painter&, Gfx::IntRect const& tail, Direction body_direction) override; + +private: + ImageSkin(StringView skin_name, Vector> head_bitmaps, Vector> body_bitmaps); + + DeprecatedString m_skin_name; + + Vector> m_head_bitmaps; + Vector> m_body_bitmaps; +}; + +} diff --git a/Userland/Games/Snake/Skins/SnakeSkin.cpp b/Userland/Games/Snake/Skins/SnakeSkin.cpp new file mode 100644 index 0000000000..da1d77729f --- /dev/null +++ b/Userland/Games/Snake/Skins/SnakeSkin.cpp @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023, Sam Atkins + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "SnakeSkin.h" +#include "ClassicSkin.h" +#include "ImageSkin.h" +#include +#include + +namespace Snake { + +ErrorOr> SnakeSkin::create(StringView skin_name, Color color) +{ + if (skin_name == "classic"sv) + return try_make(color); + + // Try to find an image-based skin matching the name. + if (Core::DeprecatedFile::exists(TRY(String::formatted("/res/graphics/snake/skins/{}", skin_name)))) + return ImageSkin::create(skin_name); + + // Fall-back on classic + return try_make(color); +} + +} diff --git a/Userland/Games/Snake/Skins/SnakeSkin.h b/Userland/Games/Snake/Skins/SnakeSkin.h new file mode 100644 index 0000000000..64f747f4c5 --- /dev/null +++ b/Userland/Games/Snake/Skins/SnakeSkin.h @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023, the SerenityOS developers. + * Copyright (c) 2023, Sam Atkins + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include "../Geometry.h" +#include +#include + +namespace Snake { + +class SnakeSkin { +public: + static ErrorOr> create(StringView skin_name, Color color); + + virtual ~SnakeSkin() = default; + + virtual void draw_head(Gfx::Painter&, Gfx::IntRect const& rect, Direction facing_direction) = 0; + virtual void draw_body(Gfx::Painter&, Gfx::IntRect const& rect, Direction previous_direction, Direction next_direction) = 0; + virtual void draw_tail(Gfx::Painter&, Gfx::IntRect const& rect, Direction body_direction) = 0; +}; + +} diff --git a/Userland/Games/Snake/main.cpp b/Userland/Games/Snake/main.cpp index 53e3de7a85..d38084351f 100644 --- a/Userland/Games/Snake/main.cpp +++ b/Userland/Games/Snake/main.cpp @@ -1,16 +1,20 @@ /* * Copyright (c) 2018-2020, Andreas Kling + * Copyright (c) 2023, Sam Atkins * * SPDX-License-Identifier: BSD-2-Clause */ #include "Game.h" +#include "Skins/SnakeSkin.h" #include #include #include +#include #include #include #include +#include #include #include #include @@ -21,7 +25,6 @@ #include #include #include -#include ErrorOr serenity_main(Main::Arguments arguments) { @@ -30,6 +33,7 @@ ErrorOr serenity_main(Main::Arguments arguments) auto app = TRY(GUI::Application::try_create(arguments)); Config::pledge_domain("Snake"); + Config::monitor_domain("Snake"); TRY(Desktop::Launcher::add_allowed_handler_with_only_specific_urls("/bin/Help", { URL::create_with_file_scheme("/usr/share/man/man6/Snake.md") })); TRY(Desktop::Launcher::seal_allowlist()); @@ -55,6 +59,7 @@ ErrorOr serenity_main(Main::Arguments arguments) game.set_focus(true); auto high_score = Config::read_u32("Snake"sv, "Snake"sv, "HighScore"sv, 0); + auto snake_skin_name = Config::read_string("Snake"sv, "Snake"sv, "SnakeSkin"sv, "classic"sv); auto& statusbar = *widget->find_descendant_of_type_named("statusbar"sv); statusbar.set_text(0, "Score: 0"sv); @@ -92,17 +97,48 @@ ErrorOr serenity_main(Main::Arguments arguments) action.set_icon(pause_icon); } }))); - TRY(game_menu->try_add_action(GUI::Action::create("&Change snake color", TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/color-chooser.png"sv)), [&](auto&) { - game.pause(); + + auto change_snake_color = GUI::Action::create("&Change snake color", TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/color-chooser.png"sv)), [&](auto&) { auto was_paused = game.is_paused(); if (!was_paused) game.pause(); auto dialog = GUI::ColorPicker::construct(Gfx::Color::White, window); - if (dialog->exec() == GUI::Dialog::ExecResult::OK) - game.set_snake_base_color(dialog->color()); + if (dialog->exec() == GUI::Dialog::ExecResult::OK) { + Config::write_u32("Snake"sv, "Snake"sv, "BaseColor"sv, dialog->color().value()); + game.set_skin_color(dialog->color()); + } if (!was_paused) game.start(); - }))); + }); + change_snake_color->set_enabled(snake_skin_name == "classic"sv); + TRY(game_menu->try_add_action(change_snake_color)); + + GUI::ActionGroup skin_action_group; + skin_action_group.set_exclusive(true); + + auto skin_menu = TRY(game_menu->try_add_submenu("&Skin")); + skin_menu->set_icon(app_icon.bitmap_for_size(16)); + + auto add_skin_action = [&](StringView name, bool enable_color) -> ErrorOr { + auto action = TRY(GUI::Action::try_create_checkable(name, {}, [&, enable_color](auto& action) { + Config::write_string("Snake"sv, "Snake"sv, "SnakeSkin"sv, action.text()); + game.set_skin_name(action.text()); + change_snake_color->set_enabled(enable_color); + })); + + skin_action_group.add_action(*action); + if (snake_skin_name == name) + action->set_checked(true); + TRY(skin_menu->try_add_action(*action)); + return {}; + }; + + TRY(Core::Directory::for_each_entry("/res/graphics/snake/skins/"sv, Core::DirIterator::SkipParentAndBaseDir, [&](auto& entry, auto&) -> ErrorOr { + TRY(add_skin_action(entry.name, false)); + return IterationDecision::Continue; + })); + TRY(add_skin_action("classic"sv, true)); + TRY(game_menu->try_add_separator()); TRY(game_menu->try_add_action(GUI::CommonActions::make_quit_action([](auto&) { GUI::Application::the()->quit();