1130 lines
39 KiB
C++
1130 lines
39 KiB
C++
#include "player.h"
|
|
|
|
#include <SDL3/SDL.h> // Para SDL_GetTicks, SDL_FlipMode
|
|
|
|
#include <algorithm> // Para clamp, max, min
|
|
#include <cmath> // Para fmod
|
|
#include <cstdlib> // Para rand
|
|
|
|
#include "animated_sprite.h" // Para AnimatedSprite
|
|
#include "asset.h" // Para Asset
|
|
#include "audio.h" // Para Audio
|
|
#include "input.h" // Para Input
|
|
#include "input_types.h" // Para InputAction
|
|
#include "manage_hiscore_table.h" // Para ManageHiScoreTable, HiScoreEntry
|
|
#include "param.h" // Para Param, ParamGame, param
|
|
#include "scoreboard.h" // Para Scoreboard
|
|
#include "stage.h" // Para power_can_be_added
|
|
#include "stage_interface.h" // Para IStageInfo
|
|
#include "texture.h" // Para Texture
|
|
#ifdef _DEBUG
|
|
#include <iostream>
|
|
#endif
|
|
|
|
// Constructor
|
|
Player::Player(const Config& config)
|
|
: player_sprite_(std::make_unique<AnimatedSprite>(config.texture.at(0), config.animations.at(0))),
|
|
power_sprite_(std::make_unique<AnimatedSprite>(config.texture.at(4), config.animations.at(1))),
|
|
enter_name_(std::make_unique<EnterName>()),
|
|
hi_score_table_(config.hi_score_table),
|
|
glowing_entry_(config.glowing_entry),
|
|
stage_info_(config.stage_info),
|
|
play_area_(*config.play_area),
|
|
id_(config.id),
|
|
default_pos_x_(config.x),
|
|
default_pos_y_(config.y),
|
|
demo_(config.demo) {
|
|
// Configura objetos
|
|
player_sprite_->addTexture(config.texture.at(1));
|
|
player_sprite_->addTexture(config.texture.at(2));
|
|
player_sprite_->addTexture(config.texture.at(3));
|
|
player_sprite_->setActiveTexture(coffees_);
|
|
power_sprite_->getTexture()->setAlpha(224);
|
|
power_up_x_offset_ = (power_sprite_->getWidth() - player_sprite_->getWidth()) / 2;
|
|
power_sprite_->setPosY(default_pos_y_ - (power_sprite_->getHeight() - player_sprite_->getHeight()));
|
|
|
|
// Inicializa variables
|
|
pos_x_ = default_pos_x_;
|
|
init();
|
|
}
|
|
|
|
// Iniciador
|
|
void Player::init() {
|
|
// Inicializa variables de estado
|
|
pos_y_ = default_pos_y_;
|
|
walking_state_ = State::WALKING_STOP;
|
|
firing_state_ = State::FIRING_NONE;
|
|
invulnerable_ = false;
|
|
invulnerable_counter_ = 0;
|
|
power_up_ = false;
|
|
power_up_counter_ = POWERUP_COUNTER;
|
|
extra_hit_ = false;
|
|
coffees_ = 0;
|
|
continue_counter_ = 10;
|
|
name_entry_idle_time_accumulator_ = 0.0f;
|
|
name_entry_total_time_accumulator_ = 0.0f;
|
|
shiftColliders();
|
|
vel_x_ = 0;
|
|
vel_y_ = 0;
|
|
score_ = 0;
|
|
score_multiplier_ = 1.0F;
|
|
enter_name_->init(last_enter_name_);
|
|
|
|
// Establece la posición del sprite
|
|
player_sprite_->clear();
|
|
shiftSprite();
|
|
}
|
|
|
|
// Actua en consecuencia de la entrada recibida
|
|
void Player::setInput(Input::Action action) {
|
|
switch (playing_state_) {
|
|
case State::PLAYING: {
|
|
setInputPlaying(action);
|
|
break;
|
|
}
|
|
case State::ENTERING_NAME:
|
|
case State::ENTERING_NAME_GAME_COMPLETED: {
|
|
setInputEnteringName(action);
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Procesa inputs para cuando está jugando
|
|
void Player::setInputPlaying(Input::Action action) {
|
|
switch (action) {
|
|
case Input::Action::LEFT: {
|
|
vel_x_ = -BASE_SPEED;
|
|
setWalkingState(State::WALKING_LEFT);
|
|
break;
|
|
}
|
|
case Input::Action::RIGHT: {
|
|
vel_x_ = BASE_SPEED;
|
|
setWalkingState(State::WALKING_RIGHT);
|
|
break;
|
|
}
|
|
case Input::Action::FIRE_CENTER: {
|
|
setFiringState(State::FIRING_UP);
|
|
break;
|
|
}
|
|
case Input::Action::FIRE_LEFT: {
|
|
setFiringState(State::FIRING_LEFT);
|
|
break;
|
|
}
|
|
case Input::Action::FIRE_RIGHT: {
|
|
setFiringState(State::FIRING_RIGHT);
|
|
break;
|
|
}
|
|
default: {
|
|
vel_x_ = 0;
|
|
setWalkingState(State::WALKING_STOP);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Procesa inputs para cuando está introduciendo el nombre
|
|
void Player::setInputEnteringName(Input::Action action) {
|
|
switch (action) {
|
|
case Input::Action::FIRE_LEFT:
|
|
enter_name_->addCharacter();
|
|
break;
|
|
case Input::Action::FIRE_CENTER:
|
|
enter_name_->removeLastCharacter();
|
|
break;
|
|
case Input::Action::RIGHT:
|
|
enter_name_->incIndex();
|
|
break;
|
|
case Input::Action::LEFT:
|
|
enter_name_->decIndex();
|
|
break;
|
|
case Input::Action::START:
|
|
last_enter_name_ = getRecordName();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
name_entry_idle_time_accumulator_ = 0.0f;
|
|
}
|
|
|
|
// Sistema de movimiento
|
|
void Player::move(float deltaTime) {
|
|
switch (playing_state_) {
|
|
case State::PLAYING:
|
|
handlePlayingMovement(deltaTime);
|
|
break;
|
|
case State::ROLLING:
|
|
handleRollingMovement();
|
|
break;
|
|
case State::TITLE_ANIMATION:
|
|
handleTitleAnimation(deltaTime);
|
|
break;
|
|
case State::CONTINUE_TIME_OUT:
|
|
handleContinueTimeOut();
|
|
break;
|
|
case State::LEAVING_SCREEN:
|
|
updateStepCounter(deltaTime);
|
|
handleLeavingScreen(deltaTime);
|
|
break;
|
|
case State::ENTERING_SCREEN:
|
|
updateStepCounter(deltaTime);
|
|
handleEnteringScreen(deltaTime);
|
|
break;
|
|
case State::CREDITS:
|
|
handleCreditsMovement(deltaTime);
|
|
break;
|
|
case State::WAITING:
|
|
handleWaitingMovement(deltaTime);
|
|
break;
|
|
case State::RECOVER:
|
|
handleRecoverMovement();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Movimiento time-based durante el juego
|
|
void Player::handlePlayingMovement(float deltaTime) {
|
|
// Mueve el jugador a derecha o izquierda (time-based en segundos)
|
|
pos_x_ += vel_x_ * deltaTime;
|
|
|
|
// Si el jugador abandona el area de juego por los laterales, restaura su posición
|
|
const float MIN_X = play_area_.x - 5;
|
|
const float MAX_X = play_area_.w + 5 - WIDTH;
|
|
pos_x_ = std::clamp(pos_x_, MIN_X, MAX_X);
|
|
|
|
shiftSprite();
|
|
}
|
|
|
|
void Player::handleRecoverMovement() {
|
|
if (player_sprite_->getCurrentAnimationFrame() == 10) { playSound("voice_recover.wav"); }
|
|
if (player_sprite_->animationIsCompleted()) { setPlayingState(State::RESPAWNING); }
|
|
}
|
|
|
|
void Player::handleRollingMovement() {
|
|
handleRollingBoundaryCollision();
|
|
handleRollingGroundCollision();
|
|
}
|
|
|
|
void Player::handleRollingBoundaryCollision() {
|
|
const int X = player_sprite_->getPosX();
|
|
const int MIN_X = play_area_.x;
|
|
const int MAX_X = play_area_.x + play_area_.w - WIDTH;
|
|
|
|
if ((X < MIN_X) || (X > MAX_X)) {
|
|
player_sprite_->setPosX(std::clamp(X, MIN_X, MAX_X));
|
|
player_sprite_->setVelX(-player_sprite_->getVelX());
|
|
playSound("jump.wav");
|
|
}
|
|
}
|
|
|
|
void Player::handleRollingGroundCollision() {
|
|
if (player_sprite_->getPosY() <= play_area_.h - HEIGHT) {
|
|
return;
|
|
}
|
|
|
|
if (player_sprite_->getVelY() < 120.0F) { // 2.0F * 60fps = 120.0F pixels/segundo
|
|
handleRollingStop();
|
|
} else {
|
|
handleRollingBounce();
|
|
}
|
|
}
|
|
|
|
void Player::handleRollingStop() {
|
|
const auto NEXT_PLAYER_STATE = qualifiesForHighScore() ? State::ENTERING_NAME : State::CONTINUE;
|
|
const auto NEXT_STATE = demo_ ? State::LYING_ON_THE_FLOOR_FOREVER : NEXT_PLAYER_STATE;
|
|
|
|
setPlayingState(NEXT_STATE);
|
|
pos_x_ = player_sprite_->getPosX();
|
|
pos_y_ = default_pos_y_;
|
|
player_sprite_->clear();
|
|
shiftSprite();
|
|
playSound("jump.wav");
|
|
}
|
|
|
|
void Player::handleRollingBounce() {
|
|
player_sprite_->setPosY(play_area_.h - HEIGHT);
|
|
player_sprite_->setVelY(player_sprite_->getVelY() * -0.5F);
|
|
player_sprite_->setVelX(player_sprite_->getVelX() * 0.75F);
|
|
player_sprite_->setAnimationSpeed(player_sprite_->getAnimationSpeed() * 2);
|
|
playSound("jump.wav");
|
|
}
|
|
|
|
void Player::handleTitleAnimation(float deltaTime) {
|
|
setInputBasedOnPlayerId();
|
|
|
|
pos_x_ += (vel_x_ * 2.0F) * deltaTime;
|
|
const float MIN_X = -WIDTH;
|
|
const float MAX_X = play_area_.w;
|
|
pos_x_ = std::clamp(pos_x_, MIN_X, MAX_X);
|
|
shiftSprite();
|
|
|
|
if (pos_x_ == MIN_X || pos_x_ == MAX_X) {
|
|
setPlayingState(State::TITLE_HIDDEN);
|
|
}
|
|
}
|
|
|
|
void Player::handleContinueTimeOut() {
|
|
// Si el cadaver desaparece por el suelo, cambia de estado
|
|
if (player_sprite_->getPosY() > play_area_.h) {
|
|
player_sprite_->stop();
|
|
setPlayingState(State::WAITING);
|
|
}
|
|
}
|
|
|
|
void Player::handleLeavingScreen(float deltaTime) {
|
|
// updateStepCounter se llama desde move() con deltaTime
|
|
setInputBasedOnPlayerId();
|
|
|
|
pos_x_ += vel_x_ * deltaTime;
|
|
const float MIN_X = -WIDTH;
|
|
const float MAX_X = play_area_.w;
|
|
pos_x_ = std::clamp(pos_x_, MIN_X, MAX_X);
|
|
shiftSprite();
|
|
|
|
if (pos_x_ == MIN_X || pos_x_ == MAX_X) {
|
|
setPlayingState(State::GAME_OVER);
|
|
}
|
|
}
|
|
|
|
void Player::handleEnteringScreen(float deltaTime) {
|
|
// updateStepCounter se llama desde move() con deltaTime
|
|
|
|
switch (id_) {
|
|
case Id::PLAYER1:
|
|
handlePlayer1Entering(deltaTime);
|
|
break;
|
|
case Id::PLAYER2:
|
|
handlePlayer2Entering(deltaTime);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
shiftSprite();
|
|
}
|
|
|
|
void Player::handlePlayer1Entering(float deltaTime) {
|
|
setInputPlaying(Input::Action::RIGHT);
|
|
pos_x_ += vel_x_ * deltaTime;
|
|
if (pos_x_ > default_pos_x_) {
|
|
pos_x_ = default_pos_x_;
|
|
setPlayingState(State::PLAYING);
|
|
}
|
|
}
|
|
|
|
void Player::handlePlayer2Entering(float deltaTime) {
|
|
setInputPlaying(Input::Action::LEFT);
|
|
pos_x_ += vel_x_ * deltaTime;
|
|
if (pos_x_ < default_pos_x_) {
|
|
pos_x_ = default_pos_x_;
|
|
setPlayingState(State::PLAYING);
|
|
}
|
|
}
|
|
|
|
// Movimiento general en la pantalla de créditos (time-based)
|
|
void Player::handleCreditsMovement(float deltaTime) {
|
|
pos_x_ += (vel_x_ / 2.0F) * deltaTime;
|
|
|
|
if (vel_x_ > 0) {
|
|
handleCreditsRightMovement();
|
|
} else {
|
|
handleCreditsLeftMovement();
|
|
}
|
|
|
|
updateWalkingStateForCredits();
|
|
shiftSprite();
|
|
}
|
|
|
|
void Player::handleCreditsRightMovement() {
|
|
if (pos_x_ > param.game.game_area.rect.w - WIDTH) {
|
|
pos_x_ = param.game.game_area.rect.w - WIDTH;
|
|
vel_x_ *= -1;
|
|
}
|
|
}
|
|
|
|
void Player::handleCreditsLeftMovement() {
|
|
if (pos_x_ < param.game.game_area.rect.x) {
|
|
pos_x_ = param.game.game_area.rect.x;
|
|
vel_x_ *= -1;
|
|
}
|
|
}
|
|
|
|
// Controla la animación del jugador saludando (time-based)
|
|
void Player::handleWaitingMovement(float deltaTime) {
|
|
waiting_time_accumulator_ += deltaTime;
|
|
const float WAITING_DURATION_S = static_cast<float>(WAITING_COUNTER) / 60.0f; // Convert frames to seconds
|
|
if (waiting_time_accumulator_ >= WAITING_DURATION_S) {
|
|
waiting_time_accumulator_ = 0.0f;
|
|
player_sprite_->resetAnimation();
|
|
}
|
|
}
|
|
|
|
void Player::updateWalkingStateForCredits() {
|
|
if (pos_x_ > param.game.game_area.center_x - WIDTH / 2) {
|
|
setWalkingState(State::WALKING_LEFT);
|
|
} else {
|
|
setWalkingState(State::WALKING_RIGHT);
|
|
}
|
|
}
|
|
|
|
void Player::setInputBasedOnPlayerId() {
|
|
switch (id_) {
|
|
case Id::PLAYER1:
|
|
setInputPlaying(Input::Action::LEFT);
|
|
break;
|
|
case Id::PLAYER2:
|
|
setInputPlaying(Input::Action::RIGHT);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Incrementa o ajusta el contador de pasos (time-based)
|
|
void Player::updateStepCounter(float deltaTime) {
|
|
step_time_accumulator_ += deltaTime;
|
|
const float STEP_INTERVAL_S = 10.0f / 60.0f; // 10 frames converted to seconds
|
|
if (step_time_accumulator_ >= STEP_INTERVAL_S) {
|
|
step_time_accumulator_ = 0.0f;
|
|
playSound("walk.wav");
|
|
}
|
|
}
|
|
|
|
// Pinta el jugador en pantalla
|
|
void Player::render() {
|
|
if (power_sprite_visible_ && isPlaying()) {
|
|
power_sprite_->render();
|
|
}
|
|
|
|
if (isRenderable()) {
|
|
player_sprite_->render();
|
|
}
|
|
}
|
|
|
|
// Calcula la animacion de moverse y disparar del jugador
|
|
auto Player::computeAnimation() const -> std::pair<std::string, SDL_FlipMode> {
|
|
const std::string BASE_ANIM = (walking_state_ == State::WALKING_STOP) ? "stand" : "walk";
|
|
std::string anim_name;
|
|
SDL_FlipMode flip_mode = SDL_FLIP_NONE;
|
|
|
|
switch (firing_state_) {
|
|
case State::FIRING_NONE:
|
|
anim_name = BASE_ANIM;
|
|
flip_mode = (walking_state_ == State::WALKING_RIGHT) ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE;
|
|
break;
|
|
|
|
case State::FIRING_UP:
|
|
anim_name = BASE_ANIM + "-fire-center";
|
|
flip_mode = (walking_state_ == State::WALKING_RIGHT) ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE;
|
|
break;
|
|
|
|
case State::FIRING_LEFT:
|
|
case State::FIRING_RIGHT:
|
|
anim_name = BASE_ANIM + "-fire-side";
|
|
flip_mode = (firing_state_ == State::FIRING_RIGHT) ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE;
|
|
break;
|
|
|
|
case State::RECOILING_UP:
|
|
anim_name = BASE_ANIM + "-recoil-center";
|
|
flip_mode = (walking_state_ == State::WALKING_RIGHT) ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE;
|
|
break;
|
|
|
|
case State::RECOILING_LEFT:
|
|
case State::RECOILING_RIGHT:
|
|
anim_name = BASE_ANIM + "-recoil-side";
|
|
flip_mode = (firing_state_ == State::RECOILING_RIGHT) ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE;
|
|
break;
|
|
|
|
case State::COOLING_UP:
|
|
anim_name = BASE_ANIM + "-cool-center";
|
|
flip_mode = (walking_state_ == State::WALKING_RIGHT) ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE;
|
|
break;
|
|
|
|
case State::COOLING_LEFT:
|
|
case State::COOLING_RIGHT:
|
|
anim_name = BASE_ANIM + "-cool-side";
|
|
flip_mode = (firing_state_ == State::COOLING_RIGHT) ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE;
|
|
break;
|
|
|
|
default:
|
|
anim_name = BASE_ANIM;
|
|
flip_mode = (walking_state_ == State::WALKING_RIGHT) ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE;
|
|
break;
|
|
}
|
|
|
|
return {anim_name, flip_mode};
|
|
}
|
|
|
|
// Establece la animación correspondiente al estado
|
|
void Player::setAnimation(float deltaTime) {
|
|
switch (playing_state_) {
|
|
case State::PLAYING:
|
|
case State::ENTERING_SCREEN:
|
|
case State::LEAVING_SCREEN:
|
|
case State::TITLE_ANIMATION:
|
|
case State::CREDITS: {
|
|
auto [animName, flipMode] = computeAnimation();
|
|
player_sprite_->setCurrentAnimation(animName, false);
|
|
player_sprite_->setFlip(flipMode);
|
|
break;
|
|
}
|
|
case State::RECOVER:
|
|
player_sprite_->setCurrentAnimation("recover");
|
|
break;
|
|
case State::WAITING:
|
|
case State::GAME_OVER:
|
|
player_sprite_->setCurrentAnimation("hello");
|
|
break;
|
|
case State::ROLLING:
|
|
case State::CONTINUE_TIME_OUT:
|
|
player_sprite_->setCurrentAnimation("rolling");
|
|
break;
|
|
case State::LYING_ON_THE_FLOOR_FOREVER:
|
|
case State::ENTERING_NAME:
|
|
case State::CONTINUE:
|
|
player_sprite_->setCurrentAnimation("dizzy");
|
|
break;
|
|
case State::ENTERING_NAME_GAME_COMPLETED:
|
|
case State::CELEBRATING:
|
|
player_sprite_->setCurrentAnimation("celebration");
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// La diferencia clave: usa deltaTime para las animaciones
|
|
player_sprite_->update(deltaTime);
|
|
power_sprite_->update(deltaTime);
|
|
}
|
|
|
|
// Actualiza al jugador con deltaTime (time-based)
|
|
void Player::update(float deltaTime) {
|
|
move(deltaTime); // Sistema de movimiento time-based
|
|
setAnimation(deltaTime); // Animaciones time-based
|
|
shiftColliders(); // Sin cambios (posicional)
|
|
updateFireSystem(deltaTime); // Sistema de disparo de dos líneas
|
|
updatePowerUp(deltaTime); // Sistema de power-up time-based
|
|
updateInvulnerable(deltaTime); // Sistema de invulnerabilidad time-based
|
|
updateScoreboard(); // Sin cambios (no temporal)
|
|
updateContinueCounter(deltaTime); // Sistema de continue time-based
|
|
updateEnterNameCounter(deltaTime); // Sistema de name entry time-based
|
|
updateShowingName(deltaTime); // Sistema de showing name time-based
|
|
}
|
|
|
|
void Player::passShowingName() {
|
|
if (game_completed_) {
|
|
setPlayingState(State::LEAVING_SCREEN);
|
|
} else {
|
|
setPlayingState(State::CONTINUE);
|
|
}
|
|
}
|
|
|
|
// Incrementa la puntuación del jugador
|
|
void Player::addScore(int score, int lowest_hi_score_entry) {
|
|
if (isPlaying()) {
|
|
score_ += score;
|
|
qualifies_for_high_score_ = score_ > lowest_hi_score_entry;
|
|
}
|
|
}
|
|
|
|
// Actualiza el panel del marcador
|
|
void Player::updateScoreboard() {
|
|
switch (playing_state_) {
|
|
case State::CONTINUE: {
|
|
Scoreboard::get()->setContinue(scoreboard_panel_, getContinueCounter());
|
|
break;
|
|
}
|
|
case State::ENTERING_NAME:
|
|
case State::ENTERING_NAME_GAME_COMPLETED: {
|
|
Scoreboard::get()->setEnterName(scoreboard_panel_, enter_name_->getCurrentName());
|
|
Scoreboard::get()->setCharacterSelected(scoreboard_panel_, enter_name_->getSelectedCharacter());
|
|
Scoreboard::get()->setCarouselAnimation(scoreboard_panel_, enter_name_->getSelectedIndex(), enter_name_.get());
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Cambia el modo del marcador
|
|
void Player::setScoreboardMode(Scoreboard::Mode mode) const {
|
|
if (!demo_) {
|
|
Scoreboard::get()->setMode(scoreboard_panel_, mode);
|
|
}
|
|
}
|
|
|
|
// Establece el estado del jugador en el juego
|
|
void Player::setPlayingState(State state) {
|
|
playing_state_ = state;
|
|
|
|
switch (playing_state_) {
|
|
case State::RECOVER: {
|
|
score_ = 0; // Pon los puntos a cero para que no se vea en el marcador
|
|
score_multiplier_ = 1.0F;
|
|
setScoreboardMode(Scoreboard::Mode::SCORE);
|
|
break;
|
|
}
|
|
case State::RESPAWNING: {
|
|
playSound("voice_thankyou.wav");
|
|
setPlayingState(State::PLAYING);
|
|
setInvulnerable(true);
|
|
break;
|
|
}
|
|
case State::PLAYING: {
|
|
init();
|
|
setInvulnerable(true);
|
|
setScoreboardMode(Scoreboard::Mode::SCORE);
|
|
stage_info_->enablePowerCollection();
|
|
break;
|
|
}
|
|
case State::CONTINUE: {
|
|
// Inicializa el contador de continuar
|
|
continue_counter_ = 9;
|
|
continue_time_accumulator_ = 0.0f; // Initialize time accumulator
|
|
playSound("continue_clock.wav");
|
|
setScoreboardMode(Scoreboard::Mode::CONTINUE);
|
|
break;
|
|
}
|
|
case State::WAITING: {
|
|
switch (id_) {
|
|
case Id::PLAYER1:
|
|
pos_x_ = param.game.game_area.rect.x;
|
|
break;
|
|
case Id::PLAYER2:
|
|
pos_x_ = param.game.game_area.rect.w - WIDTH;
|
|
break;
|
|
default:
|
|
pos_x_ = 0;
|
|
break;
|
|
}
|
|
pos_y_ = default_pos_y_;
|
|
waiting_counter_ = 0;
|
|
waiting_time_accumulator_ = 0.0f; // Initialize time accumulator
|
|
shiftSprite();
|
|
player_sprite_->setCurrentAnimation("hello");
|
|
player_sprite_->animtionPause();
|
|
setScoreboardMode(Scoreboard::Mode::WAITING);
|
|
break;
|
|
}
|
|
case State::ENTERING_NAME: {
|
|
setScoreboardMode(Scoreboard::Mode::SCORE_TO_ENTER_NAME); // Iniciar animación de transición
|
|
break;
|
|
}
|
|
case State::SHOWING_NAME: {
|
|
showing_name_time_accumulator_ = 0.0f; // Inicializar acumulador time-based
|
|
setScoreboardMode(Scoreboard::Mode::ENTER_TO_SHOW_NAME); // Iniciar animación de transición
|
|
Scoreboard::get()->setEnterName(scoreboard_panel_, last_enter_name_);
|
|
addScoreToScoreBoard();
|
|
break;
|
|
}
|
|
case State::ROLLING: {
|
|
// Activa la animación de rodar dando botes
|
|
player_sprite_->setCurrentAnimation("rolling");
|
|
player_sprite_->setAnimationSpeed(4.0f / 60.0f); // 4 frames convertido a segundos
|
|
player_sprite_->setVelY(-396.0F); // Velocidad inicial (6.6 * 60 = 396 pixels/s)
|
|
player_sprite_->setAccelY(720.0F); // Gravedad (0.2 * 60² = 720 pixels/s²)
|
|
player_sprite_->setPosY(pos_y_ - 2); // Para "sacarlo" del suelo, ya que está hundido un pixel para ocultar el outline de los pies
|
|
(rand() % 2 == 0) ? player_sprite_->setVelX(198.0F) : player_sprite_->setVelX(-198.0F); // 3.3 * 60 = 198 pixels/s
|
|
break;
|
|
}
|
|
case State::TITLE_ANIMATION: {
|
|
// Activa la animación de caminar
|
|
player_sprite_->setCurrentAnimation("walk");
|
|
playSound("voice_credit_thankyou.wav");
|
|
break;
|
|
}
|
|
case State::TITLE_HIDDEN: {
|
|
player_sprite_->setVelX(0.0F);
|
|
player_sprite_->setVelY(0.0F);
|
|
break;
|
|
}
|
|
case State::CONTINUE_TIME_OUT: {
|
|
// Activa la animación de sacar al jugador de la zona de juego
|
|
player_sprite_->setAccelY(720.0F); // 0.2 * 60² = 720 pixels/s²
|
|
player_sprite_->setVelY(-240.0F); // -4.0 * 60 = -240 pixels/s
|
|
player_sprite_->setVelX(0.0F);
|
|
player_sprite_->setCurrentAnimation("rolling");
|
|
player_sprite_->setAnimationSpeed(5.0f / 60.0f); // 5 frames convertido a segundos
|
|
setScoreboardMode(Scoreboard::Mode::GAME_OVER);
|
|
playSound("voice_aw_aw_aw.wav");
|
|
playSound("jump.wav");
|
|
break;
|
|
}
|
|
case State::GAME_OVER: {
|
|
setScoreboardMode(Scoreboard::Mode::GAME_OVER);
|
|
break;
|
|
}
|
|
case State::CELEBRATING: {
|
|
game_completed_ = true;
|
|
setScoreboardMode(Scoreboard::Mode::SCORE);
|
|
break;
|
|
}
|
|
case State::ENTERING_NAME_GAME_COMPLETED: {
|
|
// setWalkingState(State::WALKING_STOP);
|
|
// setFiringState(State::FIRING_NONE);
|
|
setScoreboardMode(Scoreboard::Mode::SCORE_TO_ENTER_NAME); // Iniciar animación de transición
|
|
break;
|
|
}
|
|
case State::LEAVING_SCREEN: {
|
|
step_counter_ = 0;
|
|
step_time_accumulator_ = 0.0f; // Initialize time accumulator
|
|
setScoreboardMode(Scoreboard::Mode::GAME_COMPLETED);
|
|
break;
|
|
}
|
|
case State::ENTERING_SCREEN: {
|
|
init();
|
|
step_counter_ = 0;
|
|
step_time_accumulator_ = 0.0f; // Initialize time accumulator
|
|
setScoreboardMode(Scoreboard::Mode::SCORE);
|
|
switch (id_) {
|
|
case Id::PLAYER1:
|
|
pos_x_ = param.game.game_area.rect.x - WIDTH;
|
|
break;
|
|
|
|
case Id::PLAYER2:
|
|
pos_x_ = param.game.game_area.rect.x + param.game.game_area.rect.w;
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
case State::CREDITS: {
|
|
vel_x_ = (walking_state_ == State::WALKING_RIGHT) ? BASE_SPEED : -BASE_SPEED;
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Aumenta el valor de la variable hasta un máximo
|
|
void Player::incScoreMultiplier() {
|
|
score_multiplier_ += 0.1F;
|
|
score_multiplier_ = std::min(score_multiplier_, 5.0F);
|
|
}
|
|
|
|
// Decrementa el valor de la variable hasta un mínimo
|
|
void Player::decScoreMultiplier() {
|
|
score_multiplier_ -= 0.1F;
|
|
score_multiplier_ = std::max(score_multiplier_, 1.0F);
|
|
}
|
|
|
|
// Establece el valor del estado
|
|
void Player::setInvulnerable(bool value) {
|
|
invulnerable_ = value;
|
|
invulnerable_counter_ = invulnerable_ ? INVULNERABLE_COUNTER : 0;
|
|
invulnerable_time_accumulator_ = invulnerable_ ? static_cast<float>(INVULNERABLE_COUNTER) / 60.0f : 0.0f; // Convert frames to seconds
|
|
}
|
|
|
|
// Monitoriza el estado (time-based)
|
|
void Player::updateInvulnerable(float deltaTime) {
|
|
if (playing_state_ == State::PLAYING && invulnerable_) {
|
|
if (invulnerable_time_accumulator_ > 0) {
|
|
invulnerable_time_accumulator_ -= deltaTime;
|
|
|
|
// Frecuencia fija de parpadeo adaptada a deltaTime (en segundos)
|
|
constexpr float BLINK_PERIOD_S = 8.0f / 60.0f; // 8 frames convertidos a segundos
|
|
|
|
// Calcula proporción decreciente basada en tiempo restante
|
|
const float TOTAL_INVULNERABLE_TIME_S = static_cast<float>(INVULNERABLE_COUNTER) / 60.0f;
|
|
float progress = 1.0f - (invulnerable_time_accumulator_ / TOTAL_INVULNERABLE_TIME_S);
|
|
float white_proportion = 0.5f - progress * 0.2f; // Menos blanco hacia el final
|
|
|
|
// Calcula si debe mostrar textura de invulnerabilidad basado en el ciclo temporal
|
|
float cycle_position = fmod(invulnerable_time_accumulator_, BLINK_PERIOD_S) / BLINK_PERIOD_S;
|
|
bool should_show_invulnerable = cycle_position < white_proportion;
|
|
size_t target_texture = should_show_invulnerable ? INVULNERABLE_TEXTURE : coffees_;
|
|
|
|
// Solo cambia textura si es diferente (optimización)
|
|
if (player_sprite_->getActiveTexture() != target_texture) {
|
|
player_sprite_->setActiveTexture(target_texture);
|
|
}
|
|
} else {
|
|
// Fin de invulnerabilidad
|
|
invulnerable_time_accumulator_ = 0;
|
|
setInvulnerable(false);
|
|
player_sprite_->setActiveTexture(coffees_);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Establece el valor de la variable
|
|
void Player::setPowerUp() {
|
|
power_up_ = true;
|
|
power_up_counter_ = POWERUP_COUNTER;
|
|
power_up_time_accumulator_ = static_cast<float>(POWERUP_COUNTER) / 60.0f; // Convert frames to seconds
|
|
power_sprite_visible_ = true; // Inicialmente visible cuando se activa el power-up
|
|
in_power_up_ending_phase_ = false; // Empezar en fase normal
|
|
bullet_color_toggle_ = false; // Resetear toggle
|
|
}
|
|
|
|
// Actualiza el valor de la variable (time-based)
|
|
void Player::updatePowerUp(float deltaTime) {
|
|
if (playing_state_ == State::PLAYING) {
|
|
if (power_up_) {
|
|
power_up_time_accumulator_ -= deltaTime;
|
|
power_up_ = power_up_time_accumulator_ > 0;
|
|
if (!power_up_) {
|
|
power_up_time_accumulator_ = 0;
|
|
power_sprite_visible_ = false;
|
|
in_power_up_ending_phase_ = false;
|
|
bullet_color_toggle_ = false;
|
|
// Los colores ahora se manejan dinámicamente en getNextBulletColor()
|
|
} else {
|
|
// Calcular visibilidad del power sprite
|
|
const float TOTAL_POWERUP_TIME_S = static_cast<float>(POWERUP_COUNTER) / 60.0f;
|
|
const float QUARTER_TIME_S = TOTAL_POWERUP_TIME_S / 4.0f;
|
|
|
|
if (power_up_time_accumulator_ > QUARTER_TIME_S) {
|
|
// En los primeros 75% del tiempo, siempre visible
|
|
power_sprite_visible_ = true;
|
|
in_power_up_ending_phase_ = false;
|
|
} else {
|
|
// En el último 25%, parpadea cada 20 frames (≈0.333s)
|
|
constexpr float BLINK_PERIOD_S = 20.0f / 60.0f;
|
|
constexpr float VISIBLE_PROPORTION = 4.0f / 20.0f;
|
|
|
|
float cycle_position = fmod(power_up_time_accumulator_, BLINK_PERIOD_S) / BLINK_PERIOD_S;
|
|
power_sprite_visible_ = cycle_position >= VISIBLE_PROPORTION;
|
|
in_power_up_ending_phase_ = true; // Activar modo alternancia de colores de balas
|
|
}
|
|
}
|
|
} else {
|
|
power_sprite_visible_ = false;
|
|
in_power_up_ending_phase_ = false;
|
|
bullet_color_toggle_ = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Concede un toque extra al jugador
|
|
void Player::giveExtraHit() {
|
|
extra_hit_ = true;
|
|
if (coffees_ < 2) {
|
|
coffees_++;
|
|
player_sprite_->setActiveTexture(coffees_);
|
|
}
|
|
}
|
|
|
|
// Quita el toque extra al jugador
|
|
void Player::removeExtraHit() {
|
|
if (coffees_ > 0) {
|
|
coffees_--;
|
|
setInvulnerable(true);
|
|
player_sprite_->setActiveTexture(coffees_);
|
|
}
|
|
|
|
extra_hit_ = coffees_ != 0;
|
|
}
|
|
|
|
// Actualiza el circulo de colisión a la posición del jugador
|
|
void Player::shiftColliders() {
|
|
collider_.x = static_cast<int>(pos_x_ + (WIDTH / 2));
|
|
collider_.y = (pos_y_ + (HEIGHT / 2));
|
|
}
|
|
|
|
// Pone las texturas del jugador
|
|
void Player::setPlayerTextures(const std::vector<std::shared_ptr<Texture>>& texture) {
|
|
player_sprite_->setTexture(texture[0]);
|
|
power_sprite_->setTexture(texture[1]);
|
|
}
|
|
|
|
// Actualiza el contador de continue (time-based)
|
|
void Player::updateContinueCounter(float deltaTime) {
|
|
if (playing_state_ == State::CONTINUE) {
|
|
continue_time_accumulator_ += deltaTime;
|
|
constexpr float CONTINUE_INTERVAL_S = 1.0f; // 1 segundo
|
|
if (continue_time_accumulator_ >= CONTINUE_INTERVAL_S) {
|
|
continue_time_accumulator_ -= CONTINUE_INTERVAL_S;
|
|
decContinueCounter();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Actualiza el contador de entrar nombre (time-based)
|
|
void Player::updateEnterNameCounter(float deltaTime) {
|
|
if (playing_state_ == State::ENTERING_NAME || playing_state_ == State::ENTERING_NAME_GAME_COMPLETED) {
|
|
name_entry_time_accumulator_ += deltaTime;
|
|
constexpr float NAME_ENTRY_INTERVAL_S = 1.0f; // 1 segundo
|
|
if (name_entry_time_accumulator_ >= NAME_ENTRY_INTERVAL_S) {
|
|
name_entry_time_accumulator_ -= NAME_ENTRY_INTERVAL_S;
|
|
decNameEntryCounter();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Actualiza el estado de SHOWING_NAME (time-based)
|
|
void Player::updateShowingName(float deltaTime) {
|
|
if (playing_state_ == State::SHOWING_NAME) {
|
|
showing_name_time_accumulator_ += deltaTime;
|
|
constexpr float SHOWING_NAME_DURATION_S = 5.0f; // 5 segundos
|
|
if (showing_name_time_accumulator_ >= SHOWING_NAME_DURATION_S) {
|
|
game_completed_ ? setPlayingState(State::LEAVING_SCREEN) : setPlayingState(State::CONTINUE);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Decrementa el contador de continuar
|
|
void Player::decContinueCounter() {
|
|
continue_time_accumulator_ = 0.0f; // Reset time accumulator
|
|
--continue_counter_;
|
|
if (continue_counter_ < 0) {
|
|
setPlayingState(State::CONTINUE_TIME_OUT);
|
|
} else {
|
|
playSound("continue_clock.wav");
|
|
}
|
|
}
|
|
|
|
// Decrementa el contador de entrar nombre
|
|
void Player::decNameEntryCounter() {
|
|
name_entry_time_accumulator_ = 0.0f; // Reset time accumulator
|
|
|
|
// Incrementa acumuladores de tiempo (1 segundo)
|
|
name_entry_idle_time_accumulator_ += 1.0f;
|
|
name_entry_total_time_accumulator_ += 1.0f;
|
|
|
|
if ((name_entry_total_time_accumulator_ >= param.game.name_entry_total_time) ||
|
|
(name_entry_idle_time_accumulator_ >= param.game.name_entry_idle_time)) {
|
|
name_entry_total_time_accumulator_ = 0.0f;
|
|
name_entry_idle_time_accumulator_ = 0.0f;
|
|
if (playing_state_ == State::ENTERING_NAME) {
|
|
last_enter_name_ = getRecordName();
|
|
setPlayingState(State::SHOWING_NAME);
|
|
} else {
|
|
setPlayingState(State::LEAVING_SCREEN);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Recoloca los sprites
|
|
void Player::shiftSprite() {
|
|
player_sprite_->setPosX(pos_x_);
|
|
player_sprite_->setPosY(pos_y_);
|
|
power_sprite_->setPosX(getPosX() - power_up_x_offset_);
|
|
}
|
|
|
|
// Hace sonar un sonido
|
|
void Player::playSound(const std::string& name) const {
|
|
if (demo_) {
|
|
return;
|
|
}
|
|
|
|
static auto* audio_ = Audio::get();
|
|
audio_->playSound(name);
|
|
}
|
|
|
|
// Indica si se puede dibujar el objeto
|
|
auto Player::isRenderable() const -> bool {
|
|
return !isTitleHidden();
|
|
};
|
|
|
|
// Devuelve el color actual de bala según el estado
|
|
auto Player::getBulletColor() const -> Bullet::Color {
|
|
return power_up_ ? bullet_colors_.powered_color : bullet_colors_.normal_color;
|
|
}
|
|
|
|
// Devuelve el color para la próxima bala (alterna si está en modo toggle)
|
|
auto Player::getNextBulletColor() -> Bullet::Color {
|
|
if (in_power_up_ending_phase_) {
|
|
// En fase final: alternar entre colores powered y normal
|
|
bullet_color_toggle_ = !bullet_color_toggle_;
|
|
return bullet_color_toggle_ ? bullet_colors_.powered_color : bullet_colors_.normal_color;
|
|
}
|
|
// Modo normal: sin power-up = normal_color, con power-up = powered_color
|
|
return power_up_ ? bullet_colors_.powered_color : bullet_colors_.normal_color;
|
|
}
|
|
|
|
// Establece los colores de bala para este jugador
|
|
void Player::setBulletColors(Bullet::Color normal, Bullet::Color powered) {
|
|
bullet_colors_.normal_color = normal;
|
|
bullet_colors_.powered_color = powered;
|
|
}
|
|
|
|
// Establece el archivo de sonido de bala para este jugador
|
|
void Player::setBulletSoundFile(const std::string& filename) {
|
|
bullet_sound_file_ = filename;
|
|
}
|
|
|
|
// Añade una puntuación a la tabla de records
|
|
void Player::addScoreToScoreBoard() const {
|
|
if (hi_score_table_ == nullptr) {
|
|
return; // Verificar esto antes de crear el manager
|
|
}
|
|
|
|
const auto ENTRY = HiScoreEntry(trim(getLastEnterName()), getScore(), get1CC());
|
|
auto manager = std::make_unique<ManageHiScoreTable>(*hi_score_table_);
|
|
|
|
if (glowing_entry_ != nullptr) {
|
|
*glowing_entry_ = manager->add(ENTRY);
|
|
}
|
|
|
|
manager->saveToFile(Asset::get()->get("score.bin"));
|
|
}
|
|
|
|
void Player::addCredit() {
|
|
++credits_used_;
|
|
playSound("credit.wav");
|
|
}
|
|
|
|
// ========================================
|
|
// SISTEMA DE DISPARO DE DOS LÍNEAS
|
|
// ========================================
|
|
|
|
// Método principal del sistema de disparo
|
|
void Player::updateFireSystem(float deltaTime) {
|
|
updateFunctionalLine(deltaTime); // Línea 1: CanFire
|
|
updateVisualLine(deltaTime); // Línea 2: Animaciones
|
|
}
|
|
|
|
// LÍNEA 1: Sistema Funcional (CanFire)
|
|
void Player::updateFunctionalLine(float deltaTime) {
|
|
if (fire_cooldown_timer_ > 0) {
|
|
fire_cooldown_timer_ -= deltaTime;
|
|
can_fire_new_system_ = false;
|
|
} else {
|
|
fire_cooldown_timer_ = 0; // Evitar valores negativos
|
|
can_fire_new_system_ = true;
|
|
}
|
|
}
|
|
|
|
// LÍNEA 2: Sistema Visual (Animaciones)
|
|
void Player::updateVisualLine(float deltaTime) {
|
|
if (visual_fire_state_ == VisualFireState::NORMAL) {
|
|
return; // No hay temporizador activo en estado NORMAL
|
|
}
|
|
|
|
visual_state_timer_ -= deltaTime;
|
|
|
|
switch (visual_fire_state_) {
|
|
case VisualFireState::AIMING:
|
|
if (visual_state_timer_ <= 0) {
|
|
transitionToRecoilingNew();
|
|
}
|
|
break;
|
|
|
|
case VisualFireState::RECOILING:
|
|
if (visual_state_timer_ <= 0) {
|
|
transitionToThreatPose();
|
|
}
|
|
break;
|
|
|
|
case VisualFireState::THREAT_POSE:
|
|
if (visual_state_timer_ <= 0) {
|
|
transitionToNormalNew();
|
|
}
|
|
break;
|
|
|
|
case VisualFireState::NORMAL:
|
|
// Ya manejado arriba
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Inicia un disparo en ambas líneas
|
|
void Player::startFiringSystem(int cooldown_frames) {
|
|
// LÍNEA 1: Inicia cooldown funcional
|
|
fire_cooldown_timer_ = static_cast<float>(cooldown_frames) / 60.0f; // Convertir frames a segundos
|
|
can_fire_new_system_ = false;
|
|
|
|
// LÍNEA 2: Resetea completamente el estado visual
|
|
aiming_duration_ = fire_cooldown_timer_ * AIMING_DURATION_FACTOR; // 50% del cooldown
|
|
recoiling_duration_ = aiming_duration_ * RECOILING_DURATION_MULTIPLIER; // 4 veces la duración de aiming
|
|
|
|
visual_fire_state_ = VisualFireState::AIMING;
|
|
visual_state_timer_ = aiming_duration_;
|
|
|
|
updateFiringStateFromVisual(); // Sincroniza firing_state_ para animaciones
|
|
}
|
|
|
|
// Sincroniza firing_state_ con visual_fire_state_
|
|
void Player::updateFiringStateFromVisual() {
|
|
// Mantener la dirección actual del disparo
|
|
State base_state = State::FIRING_NONE;
|
|
|
|
if (firing_state_ == State::FIRING_LEFT || firing_state_ == State::RECOILING_LEFT || firing_state_ == State::COOLING_LEFT) {
|
|
base_state = State::FIRING_LEFT;
|
|
} else if (firing_state_ == State::FIRING_RIGHT || firing_state_ == State::RECOILING_RIGHT || firing_state_ == State::COOLING_RIGHT) {
|
|
base_state = State::FIRING_RIGHT;
|
|
} else if (firing_state_ == State::FIRING_UP || firing_state_ == State::RECOILING_UP || firing_state_ == State::COOLING_UP) {
|
|
base_state = State::FIRING_UP;
|
|
}
|
|
|
|
switch (visual_fire_state_) {
|
|
case VisualFireState::NORMAL:
|
|
firing_state_ = State::FIRING_NONE;
|
|
break;
|
|
|
|
case VisualFireState::AIMING:
|
|
firing_state_ = base_state; // FIRING_LEFT/RIGHT/UP
|
|
break;
|
|
|
|
case VisualFireState::RECOILING:
|
|
switch (base_state) {
|
|
case State::FIRING_LEFT:
|
|
firing_state_ = State::RECOILING_LEFT;
|
|
break;
|
|
case State::FIRING_RIGHT:
|
|
firing_state_ = State::RECOILING_RIGHT;
|
|
break;
|
|
case State::FIRING_UP:
|
|
firing_state_ = State::RECOILING_UP;
|
|
break;
|
|
default:
|
|
firing_state_ = State::RECOILING_UP;
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case VisualFireState::THREAT_POSE:
|
|
switch (base_state) {
|
|
case State::FIRING_LEFT:
|
|
firing_state_ = State::COOLING_LEFT;
|
|
break;
|
|
case State::FIRING_RIGHT:
|
|
firing_state_ = State::COOLING_RIGHT;
|
|
break;
|
|
case State::FIRING_UP:
|
|
firing_state_ = State::COOLING_UP;
|
|
break;
|
|
default:
|
|
firing_state_ = State::COOLING_UP;
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Transiciones del sistema visual
|
|
void Player::transitionToRecoilingNew() {
|
|
visual_fire_state_ = VisualFireState::RECOILING;
|
|
visual_state_timer_ = recoiling_duration_;
|
|
updateFiringStateFromVisual();
|
|
}
|
|
|
|
void Player::transitionToThreatPose() {
|
|
visual_fire_state_ = VisualFireState::THREAT_POSE;
|
|
|
|
// Calcular threat_pose_duration ajustada:
|
|
// Duración original (833ms) menos el tiempo extra que ahora dura recoiling
|
|
float original_recoiling_duration = fire_cooldown_timer_; // Era 100% del cooldown
|
|
float new_recoiling_duration = aiming_duration_ * RECOILING_DURATION_MULTIPLIER; // Ahora es más del cooldown
|
|
float extra_recoiling_time = new_recoiling_duration - original_recoiling_duration;
|
|
float adjusted_threat_duration = THREAT_POSE_DURATION - extra_recoiling_time;
|
|
|
|
// Asegurar que no sea negativo
|
|
visual_state_timer_ = std::max(adjusted_threat_duration, MIN_THREAT_POSE_DURATION);
|
|
|
|
updateFiringStateFromVisual();
|
|
}
|
|
|
|
void Player::transitionToNormalNew() {
|
|
visual_fire_state_ = VisualFireState::NORMAL;
|
|
visual_state_timer_ = 0;
|
|
updateFiringStateFromVisual();
|
|
} |