From d940b8dec834de660af753aad1244b16bc3a32a0 Mon Sep 17 00:00:00 2001 From: Grant-Postma Date: Fri, 17 Jul 2020 11:04:50 -0400 Subject: [PATCH 1/2] Function to CreateRectangle. Function to InitializeAuthenticaionLayer. Function to InitializeLoginLayer. Function to InitializeSignUpLayer. --- demos/TicTacToe/Classes/main_menu_scene.cc | 1436 +++++++++++--------- demos/TicTacToe/Classes/main_menu_scene.h | 61 +- 2 files changed, 864 insertions(+), 633 deletions(-) diff --git a/demos/TicTacToe/Classes/main_menu_scene.cc b/demos/TicTacToe/Classes/main_menu_scene.cc index 58f15e11..06d4d9ad 100644 --- a/demos/TicTacToe/Classes/main_menu_scene.cc +++ b/demos/TicTacToe/Classes/main_menu_scene.cc @@ -17,6 +17,7 @@ #include #include "cocos2d.h" +#include "cocos\ui\UIButton.h" #include "cocos\ui\UITextField.h" #include "firebase/util.h" #include "tic_tac_toe_scene.h" @@ -29,6 +30,9 @@ using cocos2d::Color4F; using cocos2d::DelayTime; using cocos2d::DrawNode; using cocos2d::EventListenerTouchOneByOne; +using cocos2d::Menu; +using cocos2d::MenuItem; +using cocos2d::MenuItemSprite; using cocos2d::RepeatForever; using cocos2d::Scene; using cocos2d::Sequence; @@ -36,7 +40,9 @@ using cocos2d::Size; using cocos2d::TextFieldTTF; using cocos2d::TextHAlignment; using cocos2d::Vec2; +using cocos2d::ui::Button; using cocos2d::ui::TextField; +using cocos2d::ui::Widget; static const char* kCreateGameImage = "create_game.png"; static const char* kTextFieldBorderImage = "text_field_border.png"; @@ -63,49 +69,21 @@ bool MainMenuScene::init() { return false; } - // Call the function to initialize the firebase features. + // Initializes the firebase features. this->InitializeFirebase(); - // Create the background to add all of the authentication elements on. The - // visiblity of this node should match kAuthState, disable any - // touch_listeners when not in this state. - auth_background_ = DrawNode::create(); - auto auth_background_border = DrawNode::create(); + // Initializes the authentication layer by creating the background and placing + // all required cocos2d components. + this->InitializeAuthenticationLayer(); - const auto auth_background_size = Size(500, 550); - const auto auth_background_origin = Size(50, 50); - Vec2 auth_background_corners[4]; - auth_background_corners[0] = - Vec2(auth_background_origin.width, auth_background_origin.height); - auth_background_corners[1] = - Vec2(auth_background_origin.width + auth_background_size.width, - auth_background_origin.height); - auth_background_corners[2] = - Vec2(auth_background_origin.width + auth_background_size.width, - auth_background_origin.height + auth_background_size.height); - auth_background_corners[3] = - Vec2(auth_background_origin.width, - auth_background_origin.height + auth_background_size.height); - - Color4F white(1, 1, 1, 1); - auth_background_->drawPolygon(auth_background_corners, 4, Color4F::BLACK, 1, - Color4F::BLACK); - auth_background_border->drawPolygon(auth_background_corners, 4, - Color4F(0, 0, 0, 0), 1, Color4F::WHITE); - auth_background_->addChild(auth_background_border); - - this->addChild(auth_background_, /*layer_index=*/10); + // Initializes the login layer by creating the background and placing all + // required cocos2d components. + this->InitializeLoginLayer(); - // Label the background as Authentication. - auto auth_label = Label::createWithSystemFont("authentication", "Arial", 48); - auth_label->setPosition(Vec2(300, 550)); - auth_background_->addChild(auth_label); + // Initializes the login layer by creating the background and placing all + // required cocos2d components. - // Label to print out all of the login errors. - invalid_login_label_ = Label::createWithSystemFont("", "Arial", 24); - invalid_login_label_->setTextColor(Color4B::RED); - invalid_login_label_->setPosition(Vec2(300, 220)); - auth_background_->addChild(invalid_login_label_); + this->InitializeSignUpLayer(); // Label to display the users record. user_record_label_ = Label::createWithSystemFont("", "Arial", 24); @@ -114,661 +92,873 @@ bool MainMenuScene::init() { user_record_label_->setPosition(Vec2(500, 600)); this->addChild(user_record_label_); - // Label for anonymous sign in. - auto anonymous_login_label = - Label::createWithSystemFont("anonymous sign-in", "Arial", 18); - anonymous_login_label->setTextColor(Color4B::WHITE); - anonymous_login_label->setPosition(Vec2(460, 75)); + auto join_text_field_position = Size(480, 95); + auto join_text_field_size = Size(180, 80); + auto join_text_field = TextField::create("code", "Arial", 48); + join_text_field->setTextHorizontalAlignment(TextHAlignment::CENTER); + join_text_field->setPosition(join_text_field_position); + join_text_field->setAnchorPoint(Vec2::ANCHOR_MIDDLE); + join_text_field->setTouchSize(join_text_field_size); + join_text_field->setTouchAreaEnabled(true); + join_text_field->setMaxLength(/*max_characters=*/4); + join_text_field->setMaxLengthEnabled(true); + + // Adds the event listener to handle the actions for a textfield. + join_text_field->addEventListener( + [this](Ref* sender, TextField::EventType type) { + auto join_text_field = dynamic_cast(sender); + string join_text_field_string = join_text_field->getString(); + // Transforms the letter casing to uppercase. + std::transform( + join_text_field_string.begin(), join_text_field_string.end(), + join_text_field_string.begin(), + [](unsigned char c) -> unsigned char { return std::toupper(c); }); + // Creates a repeating blink action for the cursor. + switch (type) { + case TextField::EventType::ATTACH_WITH_IME: + // Runs the repeated blinking cursor action. + CreateBlinkingCursorAction(join_text_field); + break; + case TextField::EventType::DETACH_WITH_IME: + // Stops the blinking cursor. + join_text_field->stopAllActions(); + break; + case TextField::EventType::INSERT_TEXT: + join_text_field->setString(join_text_field_string); + break; + default: + break; + } + }); - auto anonymous_label_touch_listener = EventListenerTouchOneByOne::create(); + // Set up the constraints of the border so it surrounds the text box. + const auto pos = join_text_field_position; + const auto size = join_text_field_size; + const Vec2 join_text_border_corners[4] = { + Vec2(pos.width - size.width / 2, pos.height - size.height / 2), + Vec2(pos.width + size.width / 2, pos.height - size.height / 2), + Vec2(pos.width + size.width / 2, pos.height + size.height / 2), + Vec2(pos.width - size.width / 2, pos.height + size.height / 2)}; - anonymous_label_touch_listener->onTouchBegan = - [this](cocos2d::Touch* touch, cocos2d::Event* event) -> bool { - const auto bounds = event->getCurrentTarget()->getBoundingBox(); - const auto point = touch->getLocation(); - if (bounds.containsPoint(point)) { - // Use anonymous sign in for the user. - user_result_ = auth_->SignInAnonymously(); - current_state_ = kWaitingAnonymousState; - } + // Creates a border and adds it around the text field. + auto join_text_field_border = DrawNode::create(); + join_text_field_border->drawPolygon(join_text_border_corners, 4, + Color4F(0, 0, 0, 0), 1, Color4F::WHITE); + this->addChild(join_text_field_border); - return true; - }; + // Creates a sprite for the create button and sets its position to the + // center of the screen. TODO(grantpostma): Dynamically choose the location. + auto create_button = Sprite::create(kCreateGameImage); + create_button->setPosition(25, 200); + create_button->setAnchorPoint(Vec2(0, 0)); - // Attach the touch listener to the text field. - Director::getInstance() - ->getEventDispatcher() - ->addEventListenerWithSceneGraphPriority(anonymous_label_touch_listener, - anonymous_login_label); - auth_background_->addChild(anonymous_login_label); - - // Extract the origin, size and position of the text field so that the - // border can be created based on those values. - const auto email_text_field_origin = Vec2(0, 0); - const auto email_text_field_position = Size(110, 350); - const auto text_field_padding = 20; - const auto email_font_size = 36.0; - const auto email_text_field_size = Size(400, 2 * email_font_size); + // Creates a button listener to handle the touch event. + auto create_button_touch_listener = EventListenerTouchOneByOne::create(); - // Set up the constraints of the border so it surrounds the text box. - Vec2 email_border_corners[4] = { - Vec2(email_text_field_position.width - text_field_padding, - email_text_field_position.height), - Vec2(email_text_field_position.width + email_text_field_size.width, - email_text_field_position.height), - Vec2(email_text_field_position.width + email_text_field_size.width, - email_text_field_position.height + email_text_field_size.height), - Vec2(email_text_field_position.width - text_field_padding, - email_text_field_position.height + email_text_field_size.height)}; - - // Creates border and adds it around the text field. - auto email_text_field_border = DrawNode::create(); - email_text_field_border->drawPolygon(email_border_corners, 4, - Color4F(0, 0, 0, 0), 1, Color4F::WHITE); - - // Creates a text field to enter the user's email. - email_text_field_ = cocos2d::TextFieldTTF::textFieldWithPlaceHolder( - "enter an email address", email_text_field_size, TextHAlignment::LEFT, - "Arial", email_font_size); - email_text_field_->setPosition(email_text_field_position); - email_text_field_->setAnchorPoint(Vec2(0, 0)); - email_text_field_->setColorSpaceHolder(Color3B::GRAY); - email_text_field_->setDelegate(this); - - auto email_text_field_touch_listener = EventListenerTouchOneByOne::create(); - - email_text_field_touch_listener->onTouchBegan = - [this](cocos2d::Touch* touch, cocos2d::Event* event) -> bool { + // Set the onTouchBegan event up to a lambda tha will replace the + // MainMenu scene with a TicTacToe scene. + create_button_touch_listener->onTouchBegan = + [this, join_text_field](Touch* touch, Event* event) -> bool { const auto bounds = event->getCurrentTarget()->getBoundingBox(); const auto point = touch->getLocation(); + + // Replaces the scene with a new TicTacToe scene if the touched point is + // within the bounds of the button. if (bounds.containsPoint(point)) { - // Show the on screen keyboard and places character inputs into the text - // field. - auto text_field = dynamic_cast(event->getCurrentTarget()); - text_field->setCursorEnabled(true); - text_field->attachWithIME(); - } else { - auto text_field = dynamic_cast(event->getCurrentTarget()); - text_field->setCursorEnabled(false); - text_field->detachWithIME(); + Director::getInstance()->pushScene( + TicTacToe::createScene(std::string(), database_, user_uid_)); + join_text_field->setString(""); + current_state_ = kWaitingGameOutcome; } return true; }; - // Attach the touch listener to the text field. + // Attach the touch listener to the create game button. Director::getInstance() ->getEventDispatcher() - ->addEventListenerWithSceneGraphPriority(email_text_field_touch_listener, - email_text_field_); + ->addEventListenerWithSceneGraphPriority(create_button_touch_listener, + create_button); - auth_background_->addChild(email_text_field_, /*layer_index=*/1); - auth_background_->addChild(email_text_field_border, /*layer_index=*/1); + // Creates a sprite for the logout button and sets its position to the + auto logout_button = Sprite::create(kLogoutButtonImage); + logout_button->setPosition(25, 575); + logout_button->setAnchorPoint(Vec2(0, 0)); + logout_button->setContentSize(Size(125, 50)); - // Extract the origin, size and position of the text field so that the - // border can be created based on those values. - const auto password_text_field_origin = Vec2(0, 0); - const auto password_text_field_position = Size(110, 250); - const auto password_font_size = 36.0; - const auto password_text_field_size = Size(400, 2 * password_font_size); - - // Set up the constraints of the border so it surrounds the text box. - Vec2 password_border_corners[4] = { - Vec2(password_text_field_position.width - text_field_padding, - password_text_field_position.height), - Vec2(password_text_field_position.width + password_text_field_size.width, - password_text_field_position.height), - Vec2(password_text_field_position.width + password_text_field_size.width, - password_text_field_position.height + - password_text_field_size.height), - Vec2(password_text_field_position.width - text_field_padding, - password_text_field_position.height + - password_text_field_size.height)}; + // Creates a button listener to handle the touch event. + auto logout_button_touch_listener = EventListenerTouchOneByOne::create(); - // Creates a border and adds it around the text field. - auto password_text_field_border = DrawNode::create(); - password_text_field_border->drawPolygon( - password_border_corners, 4, Color4F(0, 0, 0, 0), 1, Color4F::WHITE); - - // Creates a text field to enter the user's password. - password_text_field_ = cocos2d::TextFieldTTF::textFieldWithPlaceHolder( - "enter a password", password_text_field_size, TextHAlignment::LEFT, - "Arial", password_font_size); - password_text_field_->setPosition(password_text_field_position); - password_text_field_->setAnchorPoint(Vec2(0, 0)); - password_text_field_->setColorSpaceHolder(Color3B::GRAY); - password_text_field_->setSecureTextEntry(true); - password_text_field_->setDelegate(this); - - auto password_text_field_touch_listener = - EventListenerTouchOneByOne::create(); - - password_text_field_touch_listener->onTouchBegan = - [this](cocos2d::Touch* touch, cocos2d::Event* event) -> bool { + // Set the onTouchBegan event up to a lambda tha will replace the + // MainMenu scene with a TicTacToe scene. + logout_button_touch_listener->onTouchBegan = [this](Touch* touch, + Event* event) -> bool { const auto bounds = event->getCurrentTarget()->getBoundingBox(); const auto point = touch->getLocation(); + + // Replaces the scene with a new TicTacToe scene if the touched point is + // within the bounds of the button. if (bounds.containsPoint(point)) { - // Shows the on screen keyboard and places character inputs into the text - // field. - auto text_field = dynamic_cast(event->getCurrentTarget()); - text_field->setCursorEnabled(true); - text_field->attachWithIME(); - } else { - auto text_field = dynamic_cast(event->getCurrentTarget()); - text_field->setCursorEnabled(false); - text_field->detachWithIME(); + current_state_ = kAuthMenuState; + user_uid_ = ""; + user_ = nullptr; + user_record_label_->setString(""); } return true; }; - auth_background_->addChild(password_text_field_, /*layer_index=*/1); - auth_background_->addChild(password_text_field_border, /*layer_index=*/1); - - // Attach the touch listener to the text field. + // Attach the touch listener to the logout game button. Director::getInstance() ->getEventDispatcher() - ->addEventListenerWithSceneGraphPriority( - password_text_field_touch_listener, password_text_field_); + ->addEventListenerWithSceneGraphPriority(logout_button_touch_listener, + logout_button); - // Creates the login button and give it a position, anchor point and - // touch_listener. - auto login_button = Sprite::create(kLoginButtonImage); - login_button->setPosition(90, 120); - login_button->setAnchorPoint(Vec2(0, 0)); - login_button->setContentSize(Size(200, 75)); + // Creates a sprite for the join button and sets its position to the center + // of the screen. TODO(grantpostma): Dynamically choose the location and set + // size(). + auto join_button = Sprite::create(kJoinButtonImage); + join_button->setPosition(25, 50); + join_button->setAnchorPoint(Vec2(0, 0)); + join_button->setScale(1.3f); // Creates a button listener to handle the touch event. - auto login_button_touch_listener = EventListenerTouchOneByOne::create(); + auto join_button_touch_listener = EventListenerTouchOneByOne::create(); - // Transition from kAuthState to kWaitingLoginState on button press and set - // user_result_ to SignInWithEmailAndPassword future result. - login_button_touch_listener->onTouchBegan = [this](Touch* touch, - Event* event) -> bool { + // Set the onTouchBegan event up to a lambda tha will replace the + // MainMenu scene with a TicTacToe scene and pass in join_text_field string. + join_button_touch_listener->onTouchBegan = + [join_text_field, this](Touch* touch, Event* event) -> bool { const auto bounds = event->getCurrentTarget()->getBoundingBox(); const auto point = touch->getLocation(); if (bounds.containsPoint(point)) { - // Check the string for a valid email pattern. - if (!std::regex_match(email_text_field_->getString(), email_pattern)) { - invalid_login_label_->setString("invalid email address"); - } else if (password_text_field_->getString().length() < 8) { - invalid_login_label_->setString( - "password must be at least 8 characters long"); + // Get the string from join_text_field. + std::string join_text_field_string = join_text_field->getString(); + if (join_text_field_string.length() == 4) { + Director::getInstance()->pushScene(TicTacToe::createScene( + join_text_field_string, database_, user_uid_)); + current_state_ = kWaitingGameOutcome; + join_text_field->setString(""); } else { - user_result_ = auth_->SignInWithEmailAndPassword( - email_text_field_->getString().c_str(), - password_text_field_->getString().c_str()); - current_state_ = kWaitingLoginState; + join_text_field->setString(""); } } return true; }; - // Attach the touch listener to the login button. + // Attach the touch listener to the join button. Director::getInstance() ->getEventDispatcher() - ->addEventListenerWithSceneGraphPriority(login_button_touch_listener, - login_button); - auth_background_->addChild(login_button, /*layer_index=*/1); + ->addEventListenerWithSceneGraphPriority(join_button_touch_listener, + join_button); - // Creates the sign_up button and give it a position, anchor point and - // touch_listener. - auto sign_up_button = Sprite::create(kSignUpButtonImage); - sign_up_button->setPosition(310, 120); - sign_up_button->setAnchorPoint(Vec2(0, 0)); - sign_up_button->setContentSize(Size(200, 75)); + // Attach the create button, join button and join text field to the + // MainMenu scene. + this->addChild(create_button); + this->addChild(join_button); + this->addChild(logout_button); + this->addChild(join_text_field, /*layer_index=*/1); - // Creates a button listener to handle the touch event. - auto sign_up_button_touch_listener = EventListenerTouchOneByOne::create(); + this->scheduleUpdate(); - // Transition from kAuthState to kWaitingSignUpState on button press and set - // user_result_ to CreateUserWithEmailAndPassword future result. - sign_up_button_touch_listener->onTouchBegan = [this](Touch* touch, - Event* event) -> bool { - const auto bounds = event->getCurrentTarget()->getBoundingBox(); - const auto point = touch->getLocation(); - if (bounds.containsPoint(point)) { - // Check the string for a valid email pattern. - if (!std::regex_match(email_text_field_->getString(), email_pattern)) { - invalid_login_label_->setString("invalid email address"); - } else if (password_text_field_->getString().length() < 8) { - invalid_login_label_->setString( - "password must be at least 8 characters long"); - } else { - user_result_ = auth_->CreateUserWithEmailAndPassword( - email_text_field_->getString().c_str(), - password_text_field_->getString().c_str()); - current_state_ = kWaitingSignUpState; - } - } - return true; - }; + return true; +} - // Attach the touch listener to the sign_up button. - Director::getInstance() - ->getEventDispatcher() - ->addEventListenerWithSceneGraphPriority(sign_up_button_touch_listener, - sign_up_button); - auth_background_->addChild(sign_up_button, /*layer_index=*/1); - - // Creates, sets the position and assigns a placeholder to the text - // field for the user to enter the join game uuid. - TextFieldTTF* join_text_field = - cocos2d::TextFieldTTF::textFieldWithPlaceHolder( - "code", cocos2d::Size(200, 100), TextHAlignment::LEFT, "Arial", 55.0); - join_text_field->setPosition(420, 45); - join_text_field->setAnchorPoint(Vec2(0, 0)); - join_text_field->setColorSpaceHolder(Color3B::WHITE); - join_text_field->setDelegate(this); - - auto join_text_field_border = Sprite::create(kTextFieldBorderImage); - join_text_field_border->setPosition(390, 50); - join_text_field_border->setAnchorPoint(Vec2(0, 0)); - join_text_field_border->setScale(.53f); - this->addChild(join_text_field_border, /*layer_index=*/0); - - // Create a touch listener to handle the touch event. - auto join_text_field_touch_listener = EventListenerTouchOneByOne::create(); - - join_text_field_touch_listener->onTouchBegan = - [join_text_field, this](cocos2d::Touch* touch, - cocos2d::Event* event) -> bool { - const auto bounds = event->getCurrentTarget()->getBoundingBox(); - const auto point = touch->getLocation(); - if (bounds.containsPoint(point)) { - // Show the on screen keyboard and places character inputs into the text - // field. - auto str = join_text_field->getString(); - auto text_field = dynamic_cast(event->getCurrentTarget()); - text_field->setCursorEnabled(true); - text_field->attachWithIME(); - } else { - auto text_field = dynamic_cast(event->getCurrentTarget()); - text_field->setCursorEnabled(false); - text_field->detachWithIME(); - } +// Initialize the firebase auth and database while also ensuring no +// dependencies are missing. +void MainMenuScene::InitializeFirebase() { + LogMessage("Initialize Firebase App."); + ::firebase::App* app; - auto join_text_field_position = Size(480, 95); - auto join_text_field_size = Size(180, 80); - auto join_text_field = TextField::create("code", "Arial", 48); - join_text_field->setTextHorizontalAlignment(TextHAlignment::CENTER); - join_text_field->setPosition(join_text_field_position); - join_text_field->setAnchorPoint(Vec2::ANCHOR_MIDDLE); - join_text_field->setTouchSize(join_text_field_size); - join_text_field->setTouchAreaEnabled(true); - join_text_field->setMaxLength(/*max_characters=*/4); - join_text_field->setMaxLengthEnabled(true); - - // Adds the event listener to handle the actions for a textfield. - join_text_field->addEventListener( - [this](Ref* sender, TextField::EventType type) { - auto join_text_field = dynamic_cast(sender); - string join_text_field_string = join_text_field->getString(); - // Transforms the letter casing to uppercase. - std::transform( - join_text_field_string.begin(), join_text_field_string.end(), - join_text_field_string.begin(), - [](unsigned char c) -> unsigned char { return std::toupper(c); }); - // Creates a repeating blink action for the cursor. - switch (type) { - case TextField::EventType::ATTACH_WITH_IME: - // Runs the repeated blinking cursor action. - join_text_field->runAction( - CreateBlinkingCursorAction(join_text_field)); - break; - case TextField::EventType::DETACH_WITH_IME: - // Stops the blinking cursor. - join_text_field->stopAllActions(); - break; - case TextField::EventType::INSERT_TEXT: - join_text_field->setString(join_text_field_string); - break; - case TextField::EventType::DELETE_BACKWARD: - break; - default: - break; - } - }); - - // Set up the constraints of the border so it surrounds the text box. - const auto pos = join_text_field_position; - const auto size = join_text_field_size; - const Vec2 join_text_border_corners[4] = { - Vec2(pos.width - size.width / 2, pos.height - size.height / 2), - Vec2(pos.width + size.width / 2, pos.height - size.height / 2), - Vec2(pos.width + size.width / 2, pos.height + size.height / 2), - Vec2(pos.width - size.width / 2, pos.height + size.height / 2)}; - - // Creates a border and adds it around the text field. - auto join_text_field_border = DrawNode::create(); - join_text_field_border->drawPolygon(join_text_border_corners, 4, - Color4F(0, 0, 0, 0), 1, Color4F::WHITE); - this->addChild(join_text_field_border); - - // Creates a sprite for the create button and sets its position to the - // center of the screen. TODO(grantpostma): Dynamically choose the location. - auto create_button = Sprite::create(kCreateGameImage); - create_button->setPosition(25, 200); - create_button->setAnchorPoint(Vec2(0, 0)); - - // Creates a button listener to handle the touch event. - auto create_button_touch_listener = EventListenerTouchOneByOne::create(); - - // Set the onTouchBegan event up to a lambda tha will replace the - // MainMenu scene with a TicTacToe scene. - create_button_touch_listener->onTouchBegan = - [this, join_text_field](Touch* touch, Event* event) -> bool { - const auto bounds = event->getCurrentTarget()->getBoundingBox(); - const auto point = touch->getLocation(); - - // Replaces the scene with a new TicTacToe scene if the touched point is - // within the bounds of the button. - if (bounds.containsPoint(point)) { - Director::getInstance()->pushScene( - TicTacToe::createScene(std::string(), database_, user_uid_)); - join_text_field->setString(""); - current_state_ = kWaitingGameOutcome; - } +#if defined(_ANDROID_) + app = ::firebase::App::Create(GetJniEnv(), GetActivity()); +#else + app = ::firebase::App::Create(); +#endif // defined(ANDROID) - return true; - }; - - // Attach the touch listener to the create game button. - Director::getInstance() - ->getEventDispatcher() - ->addEventListenerWithSceneGraphPriority(create_button_touch_listener, - create_button); - - // Creates a sprite for the logout button and sets its position to the - auto logout_button = Sprite::create(kLogoutButtonImage); - logout_button->setPosition(25, 575); - logout_button->setAnchorPoint(Vec2(0, 0)); - logout_button->setContentSize(Size(125, 50)); - - // Creates a button listener to handle the touch event. - auto logout_button_touch_listener = EventListenerTouchOneByOne::create(); - - // Set the onTouchBegan event up to a lambda tha will replace the - // MainMenu scene with a TicTacToe scene. - logout_button_touch_listener->onTouchBegan = [this](Touch* touch, - Event* event) -> bool { - const auto bounds = event->getCurrentTarget()->getBoundingBox(); - const auto point = touch->getLocation(); - - // Replaces the scene with a new TicTacToe scene if the touched point is - // within the bounds of the button. - if (bounds.containsPoint(point)) { - current_state_ = kAuthState; - user_uid_ = ""; - user_ = nullptr; - invalid_login_label_->setString(""); - email_text_field_->setString(""); - password_text_field_->setString(""); - user_record_label_->setString(""); - } + LogMessage("Initialize Firebase Auth and Firebase Database."); - return true; - }; - - // Attach the touch listener to the logout game button. - Director::getInstance() - ->getEventDispatcher() - ->addEventListenerWithSceneGraphPriority(logout_button_touch_listener, - logout_button); - - // Creates a sprite for the join button and sets its position to the center - // of the screen. TODO(grantpostma): Dynamically choose the location and set - // size(). - auto join_button = Sprite::create(kJoinButtonImage); - join_button->setPosition(25, 50); - join_button->setAnchorPoint(Vec2(0, 0)); - join_button->setScale(1.3f); - - // Creates a button listener to handle the touch event. - auto join_button_touch_listener = EventListenerTouchOneByOne::create(); - - // Set the onTouchBegan event up to a lambda tha will replace the - // MainMenu scene with a TicTacToe scene and pass in join_text_field string. - join_button_touch_listener->onTouchBegan = - [join_text_field, this](Touch* touch, Event* event) -> bool { - const auto bounds = event->getCurrentTarget()->getBoundingBox(); - const auto point = touch->getLocation(); - if (bounds.containsPoint(point)) { - // Get the string from join_text_field. - std::string join_text_field_string = join_text_field->getString(); - if (join_text_field_string.length() == 4) { - Director::getInstance()->pushScene(TicTacToe::createScene( - join_text_field_string, database_, user_uid_)); - current_state_ = kWaitingGameOutcome; - join_text_field->setString(""); - } else { - join_text_field->setString(""); - } - } - return true; - }; + // Use ModuleInitializer to initialize both Auth and Database, ensuring no + // dependencies are missing. + database_ = nullptr; + auth_ = nullptr; + void* initialize_targets[] = {&auth_, &database_}; + + const firebase::ModuleInitializer::InitializerFn initializers[] = { + [](::firebase::App* app, void* data) { + LogMessage("Attempt to initialize Firebase Auth."); + void** targets = reinterpret_cast(data); + ::firebase::InitResult result; + *reinterpret_cast<::firebase::auth::Auth**>(targets[0]) = + ::firebase::auth::Auth::GetAuth(app, &result); + return result; + }, + [](::firebase::App* app, void* data) { + LogMessage("Attempt to initialize Firebase Database."); + void** targets = reinterpret_cast(data); + ::firebase::InitResult result; + *reinterpret_cast<::firebase::database::Database**>(targets[1]) = + ::firebase::database::Database::GetInstance(app, &result); + return result; + }}; + + ::firebase::ModuleInitializer initializer; + initializer.Initialize(app, initialize_targets, initializers, + sizeof(initializers) / sizeof(initializers[0])); + + WaitForCompletion(initializer.InitializeLastResult(), "Initialize"); + + if (initializer.InitializeLastResult().error() != 0) { + LogMessage("Failed to initialize Firebase libraries: %s", + initializer.InitializeLastResult().error_message()); + ProcessEvents(2000); + } + LogMessage("Successfully initialized Firebase Auth and Firebase Database."); - // Attach the touch listener to the join button. - Director::getInstance() - ->getEventDispatcher() - ->addEventListenerWithSceneGraphPriority(join_button_touch_listener, - join_button); + database_->set_persistence_enabled(true); +} - // Attach the create button, join button and join text field to the - // MainMenu scene. - this->addChild(create_button); - this->addChild(join_button); - this->addChild(logout_button); - this->addChild(join_text_field, /*layer_index=*/1); +// 1. Creates the background node. +// 2. Adds the error label and layer title label: sign up. +// 3. Adds the id and password text fields and their event listeners. +// 4. Adds the back and sign up button. +void MainMenuScene::InitializeSignUpLayer() { + // Creates a background to add on all of the cocos2d components. The + // visiblity of this node should match kSignUpState and only active this + // layers event listeners. + + // Creates and places the sign_up_background_. + const auto sign_up_background_size = Size(500, 450); + const auto sign_up_background_origin = Size(300, 300); + sign_up_background_ = + this->CreateRectangle(sign_up_background_size, sign_up_background_origin, + /*background_color=*/Color4F::BLACK); + this->addChild(sign_up_background_, /*layer_index=*/10); + + // Creates the layer title label: sign up. + auto layer_title = Label::createWithSystemFont("sign up", "Arial", 48); + layer_title->setAnchorPoint(Vec2(.5, .5)); + layer_title->setPosition(Vec2(300, 475)); + sign_up_background_->addChild(layer_title); + + // Label to output sign up errors. + sign_up_error_label_ = Label::createWithSystemFont("", "Arial", 24); + sign_up_error_label_->setTextColor(Color4B::RED); + sign_up_error_label_->setPosition(Vec2(300, 425)); + sign_up_background_->addChild(sign_up_error_label_); + + // Creates the sign_up_id_. + const auto id_font_size = 28; + const auto id_position = Size(300, 375); + const auto id_size = Size(450, id_font_size * 1.75); + sign_up_id_ = TextField::create("email", "Arial", id_font_size); + sign_up_id_->setPosition(id_position); + sign_up_id_->setTouchAreaEnabled(true); + sign_up_id_->setTouchSize(id_size); + sign_up_background_->addChild(sign_up_id_); + + // Creates the border for the sign_up_id_ text field. + auto id_border = this->CreateRectangle(id_size, id_position); + sign_up_background_->addChild(id_border); + + // Adds the event listener to handle the actions for the sign_up_id_. + sign_up_id_->addEventListener([this](Ref* sender, TextField::EventType type) { + auto sign_up_id_ = dynamic_cast(sender); + switch (type) { + case TextField::EventType::ATTACH_WITH_IME: + // Creates and runs the repeated blinking cursor action. + CreateBlinkingCursorAction(sign_up_id_); + break; + case TextField::EventType::DETACH_WITH_IME: + // Stops the blinking cursor. + sign_up_id_->stopAllActions(); + break; + default: + break; + } + }); + + // Creates the sign_up_password_. + const auto password_font_size = 28; + const auto password_position = Size(300, 300); + const auto password_size = Size(450, password_font_size * 1.75); + sign_up_password_ = + TextField::create("password", "Arial", password_font_size); + sign_up_password_->setPosition(password_position); + sign_up_password_->setTouchAreaEnabled(true); + sign_up_password_->setTouchSize(password_size); + sign_up_password_->setPasswordEnabled(true); + sign_up_background_->addChild(sign_up_password_); + + // Creates the border for the sign_up_password_ text field. + auto password_border = + this->CreateRectangle(password_size, password_position); + sign_up_background_->addChild(password_border); + + // Adds the event listener to handle the actions for the sign_up_password_ + // text field. + sign_up_password_->addEventListener( + [this](Ref* sender, TextField::EventType type) { + auto sign_up_password_ = dynamic_cast(sender); + switch (type) { + case TextField::EventType::ATTACH_WITH_IME: + // Creates and runs the repeated blinking cursor action. + CreateBlinkingCursorAction(sign_up_password_); + break; + case TextField::EventType::DETACH_WITH_IME: + // Stops the blinking cursor. + sign_up_password_->stopAllActions(); + break; + default: + break; + } + }); + + // Creates the password_confirm text_field. + const auto password_confirm_font_size = 28; + const auto password_confirm_position = Size(300, 225); + const auto password_confirm_size = + Size(450, password_confirm_font_size * 1.75); + sign_up_password_confirm_ = TextField::create("confirm password", "Arial", + password_confirm_font_size); + sign_up_password_confirm_->setPosition(password_confirm_position); + sign_up_password_confirm_->setTouchAreaEnabled(true); + sign_up_password_confirm_->setTouchSize(password_confirm_size); + sign_up_password_confirm_->setPasswordEnabled(true); + sign_up_background_->addChild(sign_up_password_confirm_); + + // Creates the border for the sign_up_password_confirm_ text field. + auto password_confirm_border = + this->CreateRectangle(password_confirm_size, password_confirm_position); + sign_up_background_->addChild(password_confirm_border); + + // Adds the event listener to handle the actions for the + // sign_up_password_confirm text field. + sign_up_password_confirm_->addEventListener( + [this](Ref* sender, TextField::EventType type) { + auto sign_up_password_confirm_ = dynamic_cast(sender); + switch (type) { + case TextField::EventType::ATTACH_WITH_IME: + // Creates and runs the repeated blinking cursor action. + CreateBlinkingCursorAction(sign_up_password_confirm_); + break; + case TextField::EventType::DETACH_WITH_IME: + // Stops the blinking cursor. + sign_up_password_confirm_->stopAllActions(); + break; + default: + break; + } + }); + + // Creates the sign_up_button. + auto sign_up_button = Button::create(kSignUpButtonImage, kLoginButtonImage); + sign_up_button->setScale(.5); + sign_up_button->setPosition(Size(300, 130)); + sign_up_background_->addChild(sign_up_button); + + // Adds the event listener to handle touch actions for the sign_up_button. + sign_up_button->addTouchEventListener( + [this](Ref* sender, Widget::TouchEventType type) { + switch (type) { + case Widget::TouchEventType::ENDED: + // Validates the id and passwords are valid, then sets the + // user_result_ future and swaps to kWaitingSignUpState. + if (!std::regex_match(sign_up_id_->getString(), email_pattern)) { + sign_up_error_label_->setString("invalid email address"); + } else if (sign_up_password_->getString().length() < 8) { + sign_up_error_label_->setString( + "password must be at least 8 characters long"); + } else if (sign_up_password_->getString() != + sign_up_password_confirm_->getString()) { + sign_up_error_label_->setString("passwords do not match"); + } else { + sign_up_error_label_->setString(""); + user_result_ = auth_->CreateUserWithEmailAndPassword( + sign_up_id_->getString().c_str(), + sign_up_password_->getString().c_str()); + current_state_ = kWaitingSignUpState; + } + break; + default: + break; + } + }); + + // Creates the back_button. + auto back_button = Button::create(kJoinButtonImage, kSignUpButtonImage); + back_button->setScale(.5); + back_button->setPosition(Size(130, 500)); + sign_up_background_->addChild(back_button); + + // Adds the event listener to return back to the kAuthMenuState. + back_button->addTouchEventListener( + [this](Ref* sender, Widget::TouchEventType type) { + switch (type) { + case Widget::TouchEventType::ENDED: + current_state_ = kAuthMenuState; + break; + default: + break; + } + }); +} - this->scheduleUpdate(); +// 1. Creates the background node. +// 2. Adds the error label and layer title label: login. +// 3. Adds the id and password text fields and their event listeners. +// 4. Adds the back and login button. +void MainMenuScene::InitializeLoginLayer() { + // Creates a background to add on all of the cocos2d components. The + // visiblity of this node should match kLoginState and only active this + // layers event listeners. + + // Creates and places the login_background_. + const auto login_background_size = Size(500, 450); + const auto login_background_origin = Size(300, 300); + login_background_ = + this->CreateRectangle(login_background_size, login_background_origin, + /*background_color=*/Color4F::BLACK); + this->addChild(login_background_, /*layer_index=*/10); + + // Creates the layer title label: login. + auto layer_title = Label::createWithSystemFont("Login", "Arial", 48); + layer_title->setAnchorPoint(Vec2(.5, .5)); + layer_title->setPosition(Vec2(300, 475)); + login_background_->addChild(layer_title); + + // Label to output login errors. + login_error_label_ = Label::createWithSystemFont("", "Arial", 24); + login_error_label_->setTextColor(Color4B::RED); + login_error_label_->setPosition(Vec2(300, 425)); + login_background_->addChild(login_error_label_); + + // Creating the login_id_. + const auto id_font_size = 28; + const auto id_position = Size(300, 375); + const auto id_size = Size(450, id_font_size * 1.75); + login_id_ = TextField::create("email", "Arial", id_font_size); + login_id_->setPosition(id_position); + login_id_->setTouchAreaEnabled(true); + login_id_->setTouchSize(id_size); + login_background_->addChild(login_id_); + + // Creates the border for the login_id_ text field. + auto id_border = this->CreateRectangle(id_size, id_position); + login_background_->addChild(id_border); + + // Adds the event listener to handle the actions for the login_id_. + login_id_->addEventListener([this](Ref* sender, TextField::EventType type) { + auto login_id_ = dynamic_cast(sender); + switch (type) { + case TextField::EventType::ATTACH_WITH_IME: + // Creates and runs the repeated blinking cursor action. + CreateBlinkingCursorAction(login_id_); + break; + case TextField::EventType::DETACH_WITH_IME: + // Stops the blinking cursor. + login_id_->stopAllActions(); + break; + default: + break; + } + }); + + // Creates the login_password_ text field. + const auto password_font_size = 28; + const auto password_position = Size(300, 300); + const auto password_size = Size(450, password_font_size * 1.75); + login_password_ = TextField::create("password", "Arial", password_font_size); + login_password_->setPosition(password_position); + login_password_->setTouchAreaEnabled(true); + login_password_->setTouchSize(password_size); + login_password_->setPasswordEnabled(true); + login_background_->addChild(login_password_); + + // Creates the border for the login_password_ text field. + auto password_border = + this->CreateRectangle(password_size, password_position); + login_background_->addChild(password_border); + + // Adds the event listener to handle the actions for the login_password_ text + // field. + login_password_->addEventListener( + [this](Ref* sender, TextField::EventType type) { + auto login_password_ = dynamic_cast(sender); + switch (type) { + case TextField::EventType::ATTACH_WITH_IME: + // Creates and runs the repeated blinking cursor action. + CreateBlinkingCursorAction(login_password_); + break; + case TextField::EventType::DETACH_WITH_IME: + // Stops the blinking cursor. + login_password_->stopAllActions(); + break; + default: + break; + } + }); + + // Creates the login_button. + auto login_button = Button::create(kLoginButtonImage, kSignUpButtonImage); + login_button->setScale(.5); + login_button->setPosition(Size(300, 200)); + login_background_->addChild(login_button); + + // Adds the event listener to handle touch actions for the login_button. + login_button->addTouchEventListener( + [this](Ref* sender, Widget::TouchEventType type) { + switch (type) { + case Widget::TouchEventType::ENDED: + // Validates the id and passwords are valid, then sets the + // user_result_ future and swaps to kWaitingLoginState. + if (!std::regex_match(login_id_->getString(), email_pattern)) { + login_error_label_->setString("invalid email address"); + } else if (login_password_->getString().length() < 8) { + login_error_label_->setString( + "password must be at least 8 characters long"); + } else { + login_error_label_->setString(""); + user_result_ = auth_->SignInWithEmailAndPassword( + login_id_->getString().c_str(), + login_password_->getString().c_str()); + current_state_ = kWaitingLoginState; + } + break; + default: + break; + } + }); + + // Creates the back_button. + auto back_button = Button::create(kJoinButtonImage, kSignUpButtonImage); + back_button->setScale(.5); + back_button->setPosition(Size(130, 500)); + login_background_->addChild(back_button); + + // Adds the event listener to return back to the kAuthMenuState. + back_button->addTouchEventListener( + [this](Ref* sender, Widget::TouchEventType type) { + switch (type) { + case Widget::TouchEventType::ENDED: + current_state_ = kAuthMenuState; + break; + default: + break; + } + }); +} - return true; - } +// 1. Creates the background node. +// 2. Adds the layer title label: authentication. +// 3. Adds the id and password text fields and their event listeners. +// 4. Adds the back and login button. +void MainMenuScene::InitializeAuthenticationLayer() { + // Creates a background to add on all of the cocos2d components. The + // visiblity of this node should match kAuthMenuState and only active this + // layers event listeners. - // Initialize the firebase auth and database while also ensuring no - // dependencies are missing. - void - MainMenuScene::InitializeFirebase() { - LogMessage("Initialize Firebase App."); - ::firebase::App* app; + // Creates and places the auth_background_. + const auto auth_background_size = Size(500, 550); + const auto auth_background_origin = Size(300, 300); + auth_background_ = + this->CreateRectangle(auth_background_size, auth_background_origin, + /*background_color=*/Color4F::BLACK); + this->addChild(auth_background_, /*layer_index=*/10); -#if defined(_ANDROID_) - app = ::firebase::App::Create(GetJniEnv(), GetActivity()); -#else - app = ::firebase::App::Create(); -#endif // defined(ANDROID) + // Creates the layer title label: authentication. + auto layer_title = Label::createWithSystemFont("authentication", "Arial", 48); + layer_title->setPosition(Vec2(300, 550)); + auth_background_->addChild(layer_title); + + // Creates three buttons for the menu items (login,sign up, and anonymous sign + // in). + // For each menu item button, creates a normal and selected version and attach + // the touch listener. + + // Creates the sign up menu item. + const auto sign_up_normal_item = Sprite::create(kSignUpButtonImage); + sign_up_normal_item->setContentSize(Size(450, 175)); + auto sign_up_selected = Sprite::create(kSignUpButtonImage); + sign_up_normal_item->setContentSize(Size(450, 175)); + sign_up_selected->setColor(Color3B::GRAY); + + auto sign_up_item = MenuItemSprite::create( + sign_up_normal_item, sign_up_selected, [this](Ref* sender) { + auto node = dynamic_cast(sender); + if (node != nullptr) { + current_state_ = kSignUpState; + } + }); + sign_up_item->setTag(0); + + // Creates the login menu item. + const auto login_normal_item = Sprite::create(kLoginButtonImage); + login_normal_item->setContentSize(Size(450, 175)); + auto login_selected_item = Sprite::create(kLoginButtonImage); + login_normal_item->setContentSize(Size(450, 175)); + login_selected_item->setColor(Color3B::GRAY); + + auto login_item = MenuItemSprite::create( + login_normal_item, login_selected_item, [this](Ref* sender) { + auto node = dynamic_cast(sender); + if (node != nullptr) { + current_state_ = kLoginState; + } + }); + login_item->setTag(1); + + // Creates the skip login menu item. + const auto skip_login_normal_item = Sprite::create(kJoinButtonImage); + skip_login_normal_item->setContentSize(Size(450, 175)); + auto skip_login_selected_item = Sprite::create(kJoinButtonImage); + skip_login_selected_item->setContentSize(Size(450, 175)); + skip_login_selected_item->setColor(Color3B::GRAY); + + auto skip_login_item = MenuItemSprite::create( + skip_login_normal_item, skip_login_selected_item, [this](Ref* sender) { + auto node = dynamic_cast(sender); + if (node != nullptr) { + user_result_ = auth_->SignInAnonymously(); + current_state_ = kWaitingAnonymousState; + } + }); + skip_login_item->setTag(2); + + // Combines the individual items to create the menu. + cocos2d::Vector menuItems = {sign_up_item, login_item, + skip_login_item}; + auto menu = Menu::createWithArray(menuItems); + menu->setPosition(Size(290, 260)); + menu->setContentSize(Size(100, 200)); + menu->setScale(.8, .7); + menu->alignItemsVerticallyWithPadding(30.0f); + auth_background_->addChild(menu); +} - LogMessage("Initialize Firebase Auth and Firebase Database."); - - // Use ModuleInitializer to initialize both Auth and Database, ensuring no - // dependencies are missing. - database_ = nullptr; - auth_ = nullptr; - void* initialize_targets[] = {&auth_, &database_}; - - const firebase::ModuleInitializer::InitializerFn initializers[] = { - [](::firebase::App* app, void* data) { - LogMessage("Attempt to initialize Firebase Auth."); - void** targets = reinterpret_cast(data); - ::firebase::InitResult result; - *reinterpret_cast<::firebase::auth::Auth**>(targets[0]) = - ::firebase::auth::Auth::GetAuth(app, &result); - return result; - }, - [](::firebase::App* app, void* data) { - LogMessage("Attempt to initialize Firebase Database."); - void** targets = reinterpret_cast(data); - ::firebase::InitResult result; - *reinterpret_cast<::firebase::database::Database**>(targets[1]) = - ::firebase::database::Database::GetInstance(app, &result); - return result; - }}; - - ::firebase::ModuleInitializer initializer; - initializer.Initialize(app, initialize_targets, initializers, - sizeof(initializers) / sizeof(initializers[0])); - - WaitForCompletion(initializer.InitializeLastResult(), "Initialize"); - - if (initializer.InitializeLastResult().error() != 0) { - LogMessage("Failed to initialize Firebase libraries: %s", - initializer.InitializeLastResult().error_message()); - ProcessEvents(2000); - } - LogMessage("Successfully initialized Firebase Auth and Firebase Database."); +// Updates the user record variables to reflect what is in the database. +void MainMenuScene::UpdateUserRecord() { + ref_ = database_->GetReference("users").Child(user_uid_); + auto future_wins = ref_.Child("wins").GetValue(); + auto future_loses = ref_.Child("loses").GetValue(); + auto future_ties = ref_.Child("ties").GetValue(); + WaitForCompletion(future_wins, "getUserWinsData"); + WaitForCompletion(future_loses, "getUserLosesData"); + WaitForCompletion(future_ties, "getUserTiesData"); + user_wins_ = future_wins.result()->value().int64_value(); + user_loses_ = future_loses.result()->value().int64_value(); + user_ties_ = future_ties.result()->value().int64_value(); + user_record_label_->setString("W:" + to_string(user_wins_) + + " L:" + to_string(user_loses_) + + " T:" + to_string(user_ties_)); +} - database_->set_persistence_enabled(true); - } +// Creates a rectangle of Size size and centered on the origin. +// Optional parameters: background_color, border_color, border_thickness. +DrawNode* MainMenuScene::CreateRectangle(Size size, Vec2 origin, + Color4F background_color, + Color4F border_color, + int border_thickness) { + // Creates the corners of the rectangle. + Vec2 corners[4]; + corners[0] = Vec2(origin.x - (size.width / 2), origin.y - (size.height / 2)); + corners[1] = Vec2(origin.x - (size.width / 2), origin.y + (size.height / 2)); + corners[2] = Vec2(origin.x + (size.width / 2), origin.y + (size.height / 2)); + corners[3] = Vec2(origin.x + (size.width / 2), origin.y - (size.height / 2)); + + // Draws the rectangle. + DrawNode* rectangle = DrawNode::create(); + rectangle->drawPolygon(corners, /*number_of_points=*/4, background_color, + border_thickness, border_color); + return rectangle; +} - // Updates the user record variables to reflect what is in the database. - void MainMenuScene::UpdateUserRecord() { - ref_ = database_->GetReference("users").Child(user_uid_); - auto future_wins = ref_.Child("wins").GetValue(); - auto future_loses = ref_.Child("loses").GetValue(); - auto future_ties = ref_.Child("ties").GetValue(); - WaitForCompletion(future_wins, "getUserWinsData"); - WaitForCompletion(future_loses, "getUserLosesData"); - WaitForCompletion(future_ties, "getUserTiesData"); - user_wins_ = future_wins.result()->value().int64_value(); - user_loses_ = future_loses.result()->value().int64_value(); - user_ties_ = future_ties.result()->value().int64_value(); - user_record_label_->setString("W:" + to_string(user_wins_) + - " L:" + to_string(user_loses_) + - " T:" + to_string(user_ties_)); - } +// Initialize the user records in the database. +void MainMenuScene::InitializeUserRecord() { + ref_ = database_->GetReference("users").Child(user_uid_); + auto future_wins = ref_.Child("wins").SetValue(0); + auto future_loses = ref_.Child("loses").SetValue(0); + auto future_ties = ref_.Child("ties").SetValue(0); + WaitForCompletion(future_wins, "setUserWinsData"); + WaitForCompletion(future_loses, "setUserLosesData"); + WaitForCompletion(future_ties, "setUserTiesData"); + user_wins_ = 0; + user_loses_ = 0; + user_ties_ = 0; + user_record_label_->setString("W:" + to_string(user_wins_) + + " L:" + to_string(user_loses_) + + " T:" + to_string(user_ties_)); +} - // Initialize the user records in the database. - void MainMenuScene::InitializeUserRecord() { - ref_ = database_->GetReference("users").Child(user_uid_); - auto future_wins = ref_.Child("wins").SetValue(0); - auto future_loses = ref_.Child("loses").SetValue(0); - auto future_ties = ref_.Child("ties").SetValue(0); - WaitForCompletion(future_wins, "setUserWinsData"); - WaitForCompletion(future_loses, "setUserLosesData"); - WaitForCompletion(future_ties, "setUserTiesData"); - user_wins_ = 0; - user_loses_ = 0; - user_ties_ = 0; - user_record_label_->setString("W:" + to_string(user_wins_) + - " L:" + to_string(user_loses_) + - " T:" + to_string(user_ties_)); +// Overriding the onEnter method to update the user_record on reenter. +void MainMenuScene::onEnter() { + // if the scene enter is from the game, updateUserRecords and change + // current_state_. + if (current_state_ == kWaitingGameOutcome) { + this->UpdateUserRecord(); + current_state_ = kGameMenuState; } + Layer::onEnter(); +} - // Overriding the onEnter method to update the user_record on reenter. - void MainMenuScene::onEnter() { - // if the scene enter is from the game, updateUserRecords and change - // current_state_. - if (current_state_ == kWaitingGameOutcome) { - this->UpdateUserRecord(); - current_state_ = kGameMenuState; - } - Layer::onEnter(); - } +// Clears all of the labels and text fields on the login and sign up layers. +void MainMenuScene::ClearAuthFields() { + // Clears the login components. + login_id_->setString(""); + login_password_->setString(""); + login_error_label_->setString(""); + + // Clears the sign up components. + sign_up_id_->setString(""); + sign_up_password_->setString(""); + sign_up_password_confirm_->setString(""); + sign_up_error_label_->setString(""); +} - // Updates the previous_state_ when current_state_ != previous_state_: - // - // switch (current_state_) - // (1) kAuthState: makes the auth_background_ visable. - // (2) kGameMenuState: makes the auth_background_ invisable. - // (3) kWaitingAnonymousState: waits for anonymous sign in then swaps to (1). - // (4) kWaitingSignUpState: waits for sign up future completion, - // updates user variables, and swaps to (2). - // (5) kWaitingLoginState: waits for login future completion, - // updates user variables, and swaps to (2). - // (6) kWaitingGameOutcome: waits for director to pop the TicTacToeScene. - void MainMenuScene::update(float /*delta*/) { - if (current_state_ != previous_state_) { - if (current_state_ == kWaitingAnonymousState) { - if (user_result_.status() == firebase::kFutureStatusComplete) { - if (user_result_.error() == firebase::auth::kAuthErrorNone) { - user_ = *user_result_.result(); - user_uid_ = GenerateUid(10); - - this->InitializeUserRecord(); - - current_state_ = kGameMenuState; - } +// Updates the previous_state_ when current_state_ != previous_state_: +// +// switch (current_state_) +// (1) kAuthMenuState: makes the auth_background_ visable. +// (2) kGameMenuState: makes the auth_background_ invisable. +// (3) kWaitingAnonymousState: waits for anonymous sign in then swaps to (1). +// (4) kWaitingSignUpState: waits for sign up future completion, +// updates user variables, and swaps to (2). +// (5) kWaitingLoginState: waits for login future completion, +// updates user variables, and swaps to (2). +// (6) kWaitingGameOutcome: waits for director to pop the TicTacToeScene. +void MainMenuScene::update(float /*delta*/) { + if (current_state_ != previous_state_) { + if (current_state_ == kWaitingAnonymousState) { + if (user_result_.status() == firebase::kFutureStatusComplete) { + if (user_result_.error() == firebase::auth::kAuthErrorNone) { + user_ = *user_result_.result(); + user_uid_ = GenerateUid(10); + + this->InitializeUserRecord(); + + current_state_ = kGameMenuState; } - } else if (current_state_ == kWaitingSignUpState) { - if (user_result_.status() == firebase::kFutureStatusComplete) { - if (user_result_.error() == firebase::auth::kAuthErrorNone) { - user_ = *user_result_.result(); - user_uid_ = user_->uid(); + } + } else if (current_state_ == kWaitingSignUpState) { + if (user_result_.status() == firebase::kFutureStatusComplete) { + if (user_result_.error() == firebase::auth::kAuthErrorNone) { + user_ = *user_result_.result(); + user_uid_ = user_->uid(); - this->InitializeUserRecord(); + this->ClearAuthFields(); + this->InitializeUserRecord(); - current_state_ = kGameMenuState; + current_state_ = kGameMenuState; - } else { - // Change invalid_login_label_ to display the user_create failed. - invalid_login_label_->setString("invalid sign up"); - current_state_ = kAuthState; - } + } else { + // Change invalid_login_label_ to display the user_create failed. + sign_up_error_label_->setString("invalid credentials"); + current_state_ = kSignUpState; } - } else if (current_state_ == kWaitingLoginState) { - if (user_result_.status() == firebase::kFutureStatusComplete) { - if (user_result_.error() == firebase::auth::kAuthErrorNone) { - user_ = *user_result_.result(); - user_uid_ = user_->uid(); - - this->UpdateUserRecord(); - - current_state_ = kGameMenuState; - } else { - // Change invalid_login_label_ to display the auth_result errored. - auto err = user_result_.error_message(); - invalid_login_label_->setString("invalid login"); - current_state_ = kAuthState; - } + } + } else if (current_state_ == kWaitingLoginState) { + if (user_result_.status() == firebase::kFutureStatusComplete) { + if (user_result_.error() == firebase::auth::kAuthErrorNone) { + user_ = *user_result_.result(); + user_uid_ = user_->uid(); + this->ClearAuthFields(); + this->UpdateUserRecord(); + + current_state_ = kGameMenuState; + } else { + // Change invalid_login_label_ to display the auth_result errored. + login_error_label_->setString("invalid credentials"); + current_state_ = kLoginState; } - } else if (current_state_ == kAuthState) { - // Sign out logic, adding auth screen. - auth_background_->setVisible(true); - // Pauses all event touch listeners & then resumes the ones attached to - // auth_background_. - const auto event_dispatcher = - Director::getInstance()->getEventDispatcher(); - event_dispatcher->pauseEventListenersForTarget(this, - /*recursive=*/true); - event_dispatcher->resumeEventListenersForTarget(auth_background_, - /*recursive=*/true); - user_ = nullptr; - previous_state_ = current_state_; - } else if (current_state_ == kGameMenuState) { - // Removes the authentication screen. - auth_background_->setVisible(false); - const auto event_dispatcher = - Director::getInstance()->getEventDispatcher(); - // Resumes all event touch listeners & then pauses the ones - // attached to auth_background_. - event_dispatcher->resumeEventListenersForTarget(this, - /*recursive=*/true); - event_dispatcher->pauseEventListenersForTarget(auth_background_, - /*recursive=*/true); - previous_state_ = current_state_; } + } else if (current_state_ == kAuthMenuState) { + // Sets the authentication layer visable and hides the login & + // sign up layers. + auth_background_->setVisible(true); + login_background_->setVisible(false); + sign_up_background_->setVisible(false); + // Pauses all event touch listeners & then resumes the ones attached to + // auth_background_. + const auto event_dispatcher = + Director::getInstance()->getEventDispatcher(); + event_dispatcher->pauseEventListenersForTarget(this, + /*recursive=*/true); + event_dispatcher->resumeEventListenersForTarget(auth_background_, + /*recursive=*/true); + user_ = nullptr; + previous_state_ = current_state_; + } else if (current_state_ == kLoginState) { + // Sets the login layer visable and hides the authentication & + // sign up layers. + auth_background_->setVisible(false); + sign_up_background_->setVisible(false); + login_background_->setVisible(true); + // Pauses all event touch listeners & then resumes the ones attached to + // login_background_. + const auto event_dispatcher = + Director::getInstance()->getEventDispatcher(); + event_dispatcher->pauseEventListenersForTarget(this, + /*recursive=*/true); + event_dispatcher->resumeEventListenersForTarget(login_background_, + /*recursive=*/true); + user_ = nullptr; + previous_state_ = current_state_; + } else if (current_state_ == kSignUpState) { + // Sets the sign up layer visable and hides the authentication & + // login layers. + auth_background_->setVisible(false); + login_background_->setVisible(false); + sign_up_background_->setVisible(true); + + // Pauses all event touch listeners & then resumes the ones attached to + // sign_up_background_. + const auto event_dispatcher = + Director::getInstance()->getEventDispatcher(); + event_dispatcher->pauseEventListenersForTarget(this, + /*recursive=*/true); + event_dispatcher->resumeEventListenersForTarget(sign_up_background_, + /*recursive=*/true); + user_ = nullptr; + previous_state_ = current_state_; + } else if (current_state_ == kGameMenuState) { + // hides the authentication,login, and sign up layers. + auth_background_->setVisible(false); + login_background_->setVisible(false); + sign_up_background_->setVisible(false); + const auto event_dispatcher = + Director::getInstance()->getEventDispatcher(); + // Resumes all event touch listeners & then pauses the ones + // attached to auth_background_. + event_dispatcher->resumeEventListenersForTarget(this, + /*recursive=*/true); + event_dispatcher->pauseEventListenersForTarget(auth_background_, + /*recursive=*/true); + previous_state_ = current_state_; } - return; } +} - // Returns a repeating action that toggles the cursor of the text field passed - // in based on the toggle_delay. - cocos2d::RepeatForever* MainMenuScene::CreateBlinkingCursorAction( - cocos2d::ui::TextField * text_field) { - // Creates a callable function that shows the cursor and sets the cursor - // character. - const auto show_cursor = CallFunc::create([text_field]() { - text_field->setCursorEnabled(true); - text_field->setCursorChar('|'); - }); - // Creates a callable function that hides the cursor character. - const auto hide_cursor = - CallFunc::create([text_field]() { text_field->setCursorChar(' '); }); - const auto toggle_delay = DelayTime::create(1.0f); - // Aligns the sequence of actions to create a blinking cursor. - auto blink_cursor_action = Sequence::create( - show_cursor, toggle_delay, hide_cursor, toggle_delay, nullptr); - // Creates a forever repeating action based on the blink_cursor_action. - return RepeatForever::create(blink_cursor_action); - } +// Returns a repeating action that toggles the cursor of the text field passed +// in based on the toggle_delay. +void MainMenuScene::CreateBlinkingCursorAction( + cocos2d::ui::TextField* text_field) { + // Creates a callable function that shows the cursor and sets the cursor + // character. + const auto show_cursor = CallFunc::create([text_field]() { + text_field->setCursorEnabled(true); + text_field->setCursorChar('|'); + }); + // Creates a callable function that hides the cursor character. + const auto hide_cursor = + CallFunc::create([text_field]() { text_field->setCursorChar(' '); }); + + // Creates a delay action. + const cocos2d::DelayTime* delay = DelayTime::create(/*delay_durration=*/0.3f); + + // Aligns the sequence of actions to create a blinking cursor. + auto blink_cursor_action = + Sequence::create(show_cursor, delay, hide_cursor, delay, nullptr); + + // Creates a forever repeating action based on the blink_cursor_action. + text_field->runAction(RepeatForever::create(blink_cursor_action)); +} diff --git a/demos/TicTacToe/Classes/main_menu_scene.h b/demos/TicTacToe/Classes/main_menu_scene.h index 589b9c92..aa9c0ecf 100644 --- a/demos/TicTacToe/Classes/main_menu_scene.h +++ b/demos/TicTacToe/Classes/main_menu_scene.h @@ -15,7 +15,7 @@ #ifndef TICTACTOE_DEMO_CLASSES_MAINMENU_SCENE_H_ #define TICTACTOE_DEMO_CLASSES_MAINMENU_SCENE_H_ -#include +#include #include "cocos2d.h" #include "cocos\ui\UITextField.h" @@ -23,6 +23,10 @@ #include "firebase/database.h" #include "firebase/future.h" +using cocos2d::Color4F; +using cocos2d::DrawNode; +using cocos2d::Size; +using cocos2d::Vec2; using std::to_string; class MainMenuScene : public cocos2d::Layer, public cocos2d::TextFieldDelegate { @@ -36,7 +40,9 @@ class MainMenuScene : public cocos2d::Layer, public cocos2d::TextFieldDelegate { // the MainMenuScene::update(float) method. enum kSceneState { kInitializingState, - kAuthState, + kAuthMenuState, + kLoginState, + kSignUpState, kGameMenuState, kWaitingAnonymousState, kWaitingSignUpState, @@ -44,9 +50,10 @@ class MainMenuScene : public cocos2d::Layer, public cocos2d::TextFieldDelegate { kWaitingGameOutcome }; - // Creates an endless blinking cursor action for the textfield passed in. - cocos2d::RepeatForever* MainMenuScene::CreateBlinkingCursorAction( - cocos2d::ui::TextField*); + // Creates and runs an endless blinking cursor action for the textfield passed + // in. + void MainMenuScene::CreateBlinkingCursorAction(cocos2d::ui::TextField*); + // The game loop method for this layer which runs every frame once scheduled // using this->scheduleUpdate(). Acts as the state manager for this scene. void MainMenuScene::update(float) override; @@ -63,9 +70,29 @@ class MainMenuScene : public cocos2d::Layer, public cocos2d::TextFieldDelegate { // screen. void MainMenuScene::InitializeUserRecord(); + // Initializes the authentication layer which includes the background, buttons + // and labels. + void MainMenuScene::InitializeAuthenticationLayer(); + + // Initializes the sign up layer which includes the background, buttons + // and labels. + void MainMenuScene::InitializeSignUpLayer(); + + // Initializes the login layer which includes the background, buttons + // and labels. + void MainMenuScene::InitializeLoginLayer(); + + // Clears the labels and text fields for all authentication layers. + void MainMenuScene::ClearAuthFields(); + + // Creates a rectangle from the size, origin, border_color, background_color, + // and border_thickness. + DrawNode* MainMenuScene::CreateRectangle( + Size size, Vec2 origin, Color4F background_color = Color4F(0, 0, 0, 0), + Color4F border_color = Color4F::WHITE, int border_thickness = 1); + // Initializes the the firebase app, auth, and database. void MainMenuScene::InitializeFirebase(); - // Initializes the instance of a Node and returns a boolean based on if it was // successful in doing so. bool init() override; @@ -74,15 +101,29 @@ class MainMenuScene : public cocos2d::Layer, public cocos2d::TextFieldDelegate { // Node to be used as a background for the authentication menu. cocos2d::DrawNode* auth_background_; + // Node to be used as a background for the login menu. + cocos2d::DrawNode* login_background_; + + // Node to be used as a background for the sign-up menu. + cocos2d::DrawNode* sign_up_background_; + // Labels and textfields for the authentication menu. - cocos2d::Label* invalid_login_label_; + cocos2d::Label* login_error_label_; + cocos2d::Label* sign_up_error_label_; cocos2d::Label* user_record_label_; - cocos2d::TextFieldTTF* email_text_field_; - cocos2d::TextFieldTTF* password_text_field_; + + // Cocos2d components for the login layer. + cocos2d::ui::TextField* login_id_; + cocos2d::ui::TextField* login_password_; + + // Cocos2d components for the sign up layer. + cocos2d::ui::TextField* sign_up_id_; + cocos2d::ui::TextField* sign_up_password_; + cocos2d::ui::TextField* sign_up_password_confirm_; // Variable to track the current state and previous state to check against // to see if the state changed. - kSceneState current_state_ = kAuthState; + kSceneState current_state_ = kAuthMenuState; kSceneState previous_state_ = kInitializingState; // User record variabales that are stored in firebase database. From 522f9cdc87b54f83ead244a5c4d74cbd22132b59 Mon Sep 17 00:00:00 2001 From: Grant-Postma Date: Thu, 30 Jul 2020 11:50:18 -0400 Subject: [PATCH 2/2] Adding Loading layer that swaps after a delay. --- .../game_resources/Classes/main_menu_scene.cc | 37 ++++++++++++++++-- .../game_resources/Classes/main_menu_scene.h | 7 ++++ .../Resources/loading_background.png | Bin 0 -> 43471 bytes 3 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 demos/TicTacToe/game_resources/Resources/loading_background.png diff --git a/demos/TicTacToe/game_resources/Classes/main_menu_scene.cc b/demos/TicTacToe/game_resources/Classes/main_menu_scene.cc index 25632782..0cb1524f 100644 --- a/demos/TicTacToe/game_resources/Classes/main_menu_scene.cc +++ b/demos/TicTacToe/game_resources/Classes/main_menu_scene.cc @@ -67,6 +67,7 @@ static const char* kJoinButtonImage = "join_game.png"; static const char* kLoginButtonImage = "login.png"; static const char* kLogoutButtonImage = "logout.png"; static const char* kSignUpButtonImage = "sign_up.png"; +static const char* kLoadingBackgroundImage = "loading_background.png"; // Regex that will validate if the email entered is a valid email pattern. const std::regex email_pattern("(\\w+)(\\.|_)?(\\w*)@(\\w+)(\\.(\\w+))+"); @@ -89,6 +90,10 @@ bool MainMenuScene::init() { // Initializes the firebase features. this->InitializeFirebase(); + // Initializes the loading layer by setting the background that expires on a + // delay. + this->InitializeLoadingLayer(); + // Initializes the authentication layer by creating the background and placing // all required cocos2d components. this->InitializeAuthenticationLayer(); @@ -637,6 +642,29 @@ void MainMenuScene::InitializeLoginLayer() { }); } +// Creates and places the loading background. Creates a action sequence to delay +// then swap to the authentication state. +void MainMenuScene::InitializeLoadingLayer() { + // Creates the delay action. + auto loading_delay = DelayTime::create(/*delay_durration*/ 2.0f); + + // Creates a callback function to swap state to kAuthMenuState. + auto SwapToAuthState = + CallFunc::create([this]() { state_ = kAuthMenuState; }); + + // Runs the sequence that will delay followed by the swap state callback + // function. + this->runAction(Sequence::create(loading_delay, SwapToAuthState, NULL)); + + // Creates the loading background sprite. + const auto window_size = Director::getInstance()->getWinSize(); + const auto background = Sprite::create(kLoadingBackgroundImage); + background->setContentSize(window_size); + background->setAnchorPoint(Vec2(0, 0)); + loading_background_ = background; + this->addChild(loading_background_); +} + // 1. Creates the background node. // 2. Adds the layer title label: authentication. // 3. Adds the id and password text fields and their event listeners. @@ -843,11 +871,11 @@ void MainMenuScene::update(float /*delta*/) { assert(0); } } -// Returns kAuthMenuState. This will be the default update method and -// immediately swap to auth state. TODO(grantpostma): have this display a -// loading screen before swapping. +// Returns kInitializingState. Waits for the delay action sequence to callback +// SwaptoAuthState() to set state_ = kAuthMenuState. MainMenuScene::kSceneState MainMenuScene::UpdateInitialize() { - return kAuthMenuState; + this->UpdateLayer(state_); + return kInitializingState; } // Updates the layer and returns the kAuthMenuState. @@ -964,4 +992,5 @@ void MainMenuScene::UpdateLayer(MainMenuScene::kSceneState state) { auth_background_->setVisible(state == kAuthMenuState); login_background_->setVisible(state == kLoginState); sign_up_background_->setVisible(state == kSignUpState); + loading_background_->setVisible(state == kInitializingState); } \ No newline at end of file diff --git a/demos/TicTacToe/game_resources/Classes/main_menu_scene.h b/demos/TicTacToe/game_resources/Classes/main_menu_scene.h index d54b3ca2..9db57d5c 100644 --- a/demos/TicTacToe/game_resources/Classes/main_menu_scene.h +++ b/demos/TicTacToe/game_resources/Classes/main_menu_scene.h @@ -87,6 +87,10 @@ class MainMenuScene : public cocos2d::Layer, public cocos2d::TextFieldDelegate { // screen. void InitializeUserRecord(); + // Initializes the loading layer which includes a background loading image and + // state swap delay action. + void InitializeLoadingLayer(); + // Initializes the game menu layer which includes the background, buttons // and labels related to setting up the game menu. void InitializeGameMenuLayer(); @@ -129,6 +133,9 @@ class MainMenuScene : public cocos2d::Layer, public cocos2d::TextFieldDelegate { // Node to be used as a background for the sign-up menu. cocos2d::DrawNode* sign_up_background_; + // Node to be used as a background for the loading layer. + cocos2d::Sprite* loading_background_; + // Labels and textfields for the authentication menu. Label* login_error_label_; Label* sign_up_error_label_; diff --git a/demos/TicTacToe/game_resources/Resources/loading_background.png b/demos/TicTacToe/game_resources/Resources/loading_background.png new file mode 100644 index 0000000000000000000000000000000000000000..2eba5439ee84e303abdd7e57389b9fd31b27ba53 GIT binary patch literal 43471 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 literal 0 HcmV?d00001