ONO::FW::Edu::Apps::Modules::UfoAttack

package ONO::FW::Edu::Apps::Modules::UfoAttack;
################################################################################
# COPYRIGHT / LICENSE #
################################################################################
#
# This file is part of the ONO Software Project.
#
# Copyright (C) 2022-2026 Jos Kirps and The Joopita Project [ASBL]
# Copyright (C) 2011-2022 Jos Kirps and Joopita Research ASBL
# Copyright (C) 2006-2011 Jos Kirps and The Joopita Project
# Copyright (C) 2001-2006 Jos Kirps and The WobbleWolf Project
#
# This file, as well as many other parts of the ONO Software Project or
# related elements, are FREE SOFTWARE available under the ARTISTIC LICENSE 2.0.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# For the full license, see /ono/osr/license/LICENSE.txt, or write to
# jos_AT_kirps_DOT_com or contact_AT_joopita_DOT_com.
#
################################################################################
# END OF COPYRIGHT / LICENSE, HERE COMES THE CODE ... #
################################################################################

#: This module is a front end application module wrapper that has been
#: created for the Morzino Project (morzino.com) and that has been released
#: as Open Source software.
#:
#: Note that this is NOT a self-contained application, it is only a wrapper
#: that makes heavy use of the ONO Perl Framework application engine.

use strict;

use Encode;

use ONO::Render;

use ONO::ToolBox::Auth;
use ONO::Lib::Lang::LangKitEdu;

use ONO::Lib::DateTime::ToolBox;

use ONO::FW::Apps::Core;
use ONO::FW::Apps::Core::Graphics;

use ONO::FW::Edu::Apps::ToolBox;
use ONO::FW::User::Init;

use ONO::IO;
use ONO::DB;
use ONO::Lib::Basic;

use ONO::FW::Game::Nav;

sub play {

my (
$self,
$option,
$vars_ref,
$HTTP,
$BASE,
) = @_;

################################################################
# INIT - BEGIN
################################################################

my (
$db,
$community,
$action_url,
$form,
$format,
$canvas,
$pdfkey,
$sec,$min,$hour,
$mday,$mon,$year,
$wday,$yday,$timestamp,
$lang,
$BLK_ref,
$vars_ref,
) = ONO::FW::Apps::Core->init($vars_ref);

my %vars = %$vars_ref;
my %BLK = %$BLK_ref;

my ($HEAD,$WEB);

my (
$vpath,
$lang,$domain,$community,
$sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$timestamp,
$script_url,$action_url,$form
) = ONO::FW::User::Init->getvar(\%vars);

ONO::FW::Apps::Core->now("","chess",$vars{'app_lang'});

my $action_url = qq~$ENV{'SCRIPT_NAME'}?app_lang=$vars{'app_lang'}&~;

###################################################################
# GAME
###################################################################

my $CSSJS = ONO::Render->cssjs($BASE);

my $HOST = ONO::IO->http_domain_base();

my $level = 1;

my $enemy_lines = 1+$vars{'level'}; # 1 = min, 8 = max
my $enemy_fire_rate = int(11-$vars{'level'}/2); # 10 = slow, 1 = fast

my $ememy_move_down = 5+int($vars{'level'}/2);

my $shield_strength = 8; # 1 = min, 8 = max

if ($enemy_lines > 8) {
$enemy_lines = 8;
}
if ($enemy_fire_rate < 1) {
$enemy_fire_rate = 1;
}

if ($vars{'level'} > 5) {
$shield_strength = $shield_strength+1-($vars{'level'}/3);
}
if ($shield_strength < 1) {
$shield_strength = 1;
}
if ($ememy_move_down > 40) {
$ememy_move_down = 40;
}
my $speed = 25-int($vars{'level'}/3);
if ($speed < 5) {
$speed = 5;
}

my $LEVEL;
for (my $i = 1; $i < 21; $i++) {
my $sel;
if ($i == 5) {
$sel = " selected";
}
$LEVEL .= qq~<option value="$i"$sel>$i</option>~;
}

my $GAME = qq~<div style="width:920px;height:480px;background-color:#000000;background-image:url('$HOST/images/games/ufo_attack/background_intro.jpg');">
<div class="p20">
<div class="box_fabric" style="max-width:480px">
<h3>$BLK{'Start'}...</h3>
<div class="box_paper">
<$form>
<table class="default_table">
<tr>
<td>$BLK{'level'}:</td>
<td>
<div class="select"><select name="level">
$LEVEL
</select></div>
</td>
</tr>
<tr>
<td></td>
<td>$BLK{'submit_start'}</td>
</tr>
</table>
</form>
</div>
</div>
</div>
</div>
~;

if ($vars{'level'}) {

my $next = $vars{'level'}+1;

my %nav;

# $nav{'button_u_icon'} = "game/rot_right";

# $nav{'button_u_id'} = "tetris-keyboard-up";
# $nav{'button_d_id'} = "tetris-keyboard-down";

$nav{'button_l_id'} = "left";
$nav{'button_r_id'} = "right";

$nav{'button_l_onclick'} = "player_move_left();";
$nav{'button_r_onclick'} = "player_move_right();";

$nav{'button_f_onclick'} = "player_fire();";
$nav{'button_F_onclick'} = "player_fire();";

$nav{'button_l_pos'} = "top:290px;left:14px";
$nav{'button_r_pos'} = "top:290px;left:840px";

$nav{'button_f_pos'} = "top:220px;left:14px";
$nav{'button_F_pos'} = "top:220px;left:840px";

$nav{'button_f_color'} = "bg_red";
$nav{'button_F_color'} = "bg_red";

#$nav{'button_D_onclick'} = "tetris.space();";

my $switches = "fF-lr-m";

my $NAV = ONO::FW::Game::Nav->nav("main",\%nav,$switches).ONO::FW::Game::Nav->script("main",$switches);

$GAME = qq~<div class="rel noselect">
<canvas id="canvas" width="920" height="480"></canvas>
<div id="div_game_over" class="abs noselect hide" style="top:0px;left:0px;width:920px;z-index:999">
<div class="box_fabric rel" style="max-width:480px;margin-top:40px;left:210px">
<div class="box_paper">
<div class="xlarge bold lightred center p20">GAME OVER</div>
<div class="inline auto"><a href="$action_url&level=$vars{'level'}" class="button_green mb10">$BLK{'continue'}...</a></div>
</div>
</div>
</div>
<div id="div_game_next" class="abs noselect hide" style="top:0px;left:0px;width:920px;z-index:999">
<div class="box_fabric rel" style="max-width:480px;margin-top:40px;left:210px">
<div class="box_paper">
<div class="xlarge bold lightgreen center p20">YOU WON!</div>
<div class="inline auto"><a href="$action_url&level=$next" class="button_green mb10">next level: $next...</a></div>
</div>
</div>
</div>
<div class="abs bold cole large" style="top:10px;left:10px">
$BLK{'Level'} $vars{'level'}
</div>
<div class="abs noselect" style="top:0px;left:0px">$NAV</div>
</div>

<script>

var img_space = new Image();
img_space.src = '$HOST/images/gameplay/backgrounds/space.jpg';
var img_earth = new Image();
img_earth.src = '$HOST/images/gameplay/backgrounds/special_earth_large.png';
var img_player = new Image();
img_player.src = '$HOST/images/gameplay/player/spaceship.png';
var img_enemy1 = new Image();
img_enemy1.src = '$HOST/images/gameplay/sprites128_64/ufo.png';

// Defines a general class used to specify game objects.
class GameObject {
constructor(x, y, width, height, color) {
// Define the object's position
this.x = x;
this.y = y;

// Define the object's size
this.width = width;
this.height = height;

// Define the object's color
this.color = color;
}

// Draw the object on the canvas
draw(ctx) {
ctx.fillStyle = this.color;
ctx.fillRect(this.x, this.y, this.width, this.height);
}

drawplayer(ctx) {
ctx.drawImage(img_player,this.x, this.y, this.width, this.height);
}

drawenemy(ctx) {
ctx.drawImage(img_enemy1,this.x, this.y, this.width, this.height);
ctx.fillStyle = '#ff0000';
ctx.fillRect(this.x+3+Math.random()*47, this.y+11, this.width/5, this.height/5);
}

// Update the object's position
update(dx, dy) {
this.x += dx;
this.y += dy;
}

// Check if the object is colliding with another object
collidesWith(obj) {
return (this.x < obj.x + obj.width
&& this.x + this.width > obj.x
&& this.y < obj.y + obj.height
&& this.y + this.height > obj.y);
}
}

// The bullet class defines the properties and behavior of bullets.
class Bullet extends GameObject {
constructor(x, y, width, height, color, dy) {
super(x, y, width, height, color);
// Set the bullet's y direction.
this.dy = dy;
}

update(x, y) {
this.y += this.dy;
}
}

// The spaceship class defines the general properties and behavior of the player and enemies.
class SpaceShip extends GameObject {
constructor(x, y, width, height, color, canvasHeight) {
super(x, y, width, height, color);
// Set canvas height.
this.canvasHeight = canvasHeight;
// Set the spaceship's bullet size.
this.bulletWidth = 4;
this.bulletHeight = 16;
// Set the spaceship's bullet color.
this.bulletColor = "#ff0000";
// Bullets fired by the spaceship
this.bullets = [];
}

// Override the draw method to also draw the spaceship's bullets.
draw(ctx) {
super.draw(ctx);
// Draw the spaceship's bullets.
for (var i = 0; i < this.bullets.length; i++) {
this.bullets[i].draw(ctx);
this.bullets[i].update(0, 0);

// Check if the bullet is out of bounds.
if (this.bullets[i].y < 0 || this.bullets[i].y > this.canvasHeight) {
// Remove the bullet from the array.
this.bullets.splice(i, 1);
}
}
}

// A method used to fire bullets from a spaceship
shoot(dy) {
this.bullets.push(new Bullet(
this.x + this.width / 2 - this.bulletWidth / 2,
this.y - this.bulletHeight,
this.bulletWidth,
this.bulletHeight,
this.bulletColor,
dy
));
}
}

// The Player class defines the properties and behavior of the player.
class Player extends SpaceShip {
constructor(x, y, width, height, color, canvasHeight, canvasWidth) {
super(x, y, width, height, color, canvasHeight);
this.canvasWidth = canvasWidth;
}

// Update the player's position
update(dx, dy) {
super.update(dx, dy);

// Keep the player within the canvas
if (this.x < 0) {
this.x = 0;
} else if (this.x + this.width > this.canvasWidth) {
this.x = this.canvasWidth - this.width;
}
}
}

// The Asteroid class defines the properties and behavior of the asteroids.
class Asteroid {
constructor(x, y, width, height, color, noParts) {
// Set an empty array for asteroid parts.
this.parts = [];
// Create the asteroid's parts.
for (var i = 0; i < noParts; i++) {
for (var j = 0; j < noParts; j++) {
this.parts.push(new GameObject(
x + i * width,
y + j * height,
width,
height,
color
));
}
}
}

// Draw the asteroid on the canvas.
draw(ctx) {
for (var i = 0; i < this.parts.length; i++) {
this.parts[i].draw(ctx);
}
}

// Check if the asteroid is colliding with another object.
collidesWith(obj) {
for (var i = 0; i < this.parts.length; i++) {
if (this.parts[i].collidesWith(obj)) {
return true;
}
}
return false;
}

// Remove sub object on collide.
removeOnCollide(obj) {
for (var i = 0; i < this.parts.length; i++) {
if (this.parts[i].collidesWith(obj)) {
this.parts.splice(i, 1);
break;
}
}
}
}

// Defines an empty object used to specify game properties and behavior.
var game = {};

// Define canvas and context
game.canvas = document.getElementById('canvas');
game.ctx = game.canvas.getContext('2d');

// Define background color
game.backgroundColor = '#000000';

// Setup asteroids array
game.asteroidsParts = $shield_strength;
game.noOfAsteroids = 6;
game.asteroidsSpace = 125;

// Setup enemies
game.enemiesEachLine = 8;
game.enemyLines = $enemy_lines;
game.enemySpaceX = 80;
game.enemySpaceY = 35;
game.enemyFireRate = $enemy_fire_rate*16;
game.enemyFireTimer = 0;
game.enemyDirection = 1;
game.enemyStep = $ememy_move_down;

// Setup player
game.playerBulletSpeed = 10;

// Defines a function to handle the game loop
game.update = function() {
// Draw canvas background
game.ctx.fillStyle = game.backgroundColor;
game.ctx.fillRect(0, 0, game.canvas.width, game.canvas.height);

game.ctx.drawImage(img_space, 0,0,920,480);
game.ctx.globalAlpha = 0.8;
game.ctx.drawImage(img_earth, -896*1.5,30,896*4,1000);
game.ctx.globalAlpha = 1.0;

// Draw player
game.player.draw(game.ctx);
game.player.drawplayer(game.ctx);

// Draw asteroids
for (var i = 0; i < game.asteroids.length; i++) {
game.asteroids[i].draw(game.ctx);
}

// Draw enemies
for (var i = 0; i < game.enemies.length; i++) {
game.enemies[i].draw(game.ctx);
game.enemies[i].drawenemy(game.ctx);
game.enemies[i].update(game.enemyDirection, 0);
}

// Check if the player has destroyed all enemies
if (game.enemies.length == 0) {
// Reset the game
game.won();
}

// Check if the enemies are out of bounds.
if (game.enemyDirection == 1)
{
// Find the enemy closest to the right side of the screen
var closestToRightSideEnemy = game.enemies[0];
for (var i = 1; i < game.enemies.length; i++) {
if (game.enemies[i].x > closestToRightSideEnemy.x) {
closestToRightSideEnemy = game.enemies[i];
}
}

// Check if the enemy closest to the right side of
// the screen has reached the right side of the screen.
if (closestToRightSideEnemy.x +
closestToRightSideEnemy.width > game.canvas.width-80) {
// Reverse the direction of the enemies.
game.enemyDirection = -1;
// Move the enemies down.
for (var i = 0; i < game.enemies.length; i++) {
game.enemies[i].update(0, game.enemyStep);
}
}
}
else if (game.enemyDirection == -1)
{
// Find the enemy closest to the left side of the screen
var closestToLeftSideEnemy = game.enemies[0];
for (var i = 1; i < game.enemies.length; i++) {
if (game.enemies[i].x < closestToLeftSideEnemy.x) {
closestToLeftSideEnemy = game.enemies[i];
}
}

// Check if the enemy closest to the left side of
// the screen has reached the left side of the screen.
if (closestToLeftSideEnemy.x < 80) {
// Reverse the direction of the enemies.
game.enemyDirection = 1;
// Move the enemies down.
for (var i = 0; i < game.enemies.length; i++) {
game.enemies[i].update(0, game.enemyStep);
}
}
}

// Enemy fire counter
game.enemyFireTimer += Math.random() * 10;
if (game.enemyFireTimer > game.enemyFireRate) {
game.enemyFireTimer = 0;
// Fire enemy bullet
game.enemies[Math.floor(Math.random() * game.enemies.length)].shoot(5);
}

// Check if player bullet collides with asteroid
for (var i = 0; i < game.player.bullets.length; i++) {
for (var j = 0; j < game.asteroids.length; j++) {
if (game.asteroids[j].collidesWith(game.player.bullets[i])) {
game.asteroids[j].removeOnCollide(game.player.bullets[i]);
game.player.bullets.splice(i, 1);
break;
}
}
}

// Check if enemy bullet collides with asteroid
for (var i = 0; i < game.enemies.length; i++) {
for (var j = 0; j < game.enemies[i].bullets.length; j++) {
for (var k = 0; k < game.asteroids.length; k++) {
if (game.asteroids[k].collidesWith(game.enemies[i].bullets[j])) {
game.asteroids[k].removeOnCollide(game.enemies[i].bullets[j]);
game.enemies[i].bullets.splice(j, 1);
break;
}
}
}
}

// Check if player bullet collides with enemy
for (var i = 0; i < game.player.bullets.length; i++) {
for (var j = 0; j < game.enemies.length; j++) {
if (game.enemies[j].collidesWith(game.player.bullets[i])) {
game.enemies.splice(j, 1);
game.player.bullets.splice(i, 1);
break;
}
}
}

// Check if enemy bullet collides with player
for (var i = 0; i < game.enemies.length; i++) {
for (var j = 0; j < game.enemies[i].bullets.length; j++) {
if (game.player.collidesWith(game.enemies[i].bullets[j])) {
// Reset the game
game.over();
break;
}
}
}

// Check if an enemy has reached the player's y position.
for (var i = 0; i < game.enemies.length; i++) {
if (game.enemies[i].y + game.enemies[i].height > game.player.y) {
game.over();
break;
}
}
}

// Defines a function to handle key events
game.keydown = function(e) {
// If the left arrow key is pressed, move the player left.
if (e.keyCode == 37 || e.keyCode == 65) {
player_move_left();
}
// If the right arrow key is pressed, move the player right.
else if (e.keyCode == 39 || e.keyCode == 68) {
player_move_right();
}
// If the space bar is pressed, fire a bullet.
else if (e.keyCode == 32) {
if (game.player.bullets.length < 4) {
player_fire();
}
}
}

function player_move_left() {
game.player.update(-15, 0);
}
function player_move_right() {
game.player.update(15, 0);
}
function player_fire() {
game.player.shoot(-game.playerBulletSpeed);
}

// Defines a function to start the game loop
game.init = function() {
// Set the game loop
game.interval = setInterval(game.update, $speed);

// Setup player
game.player = new Player(
game.canvas.width / 2 - 80,
game.canvas.height - 50,
64,
64,
'transparent',
game.canvas.width,
);

// Setup asteroids
game.asteroids = [];
for (var i = 0; i < game.noOfAsteroids; i++) {
game.asteroids.push(new Asteroid(
game.asteroidsSpace + i * game.asteroidsSpace,
game.canvas.height - 100,
64/$shield_strength,
4,
'#cccc00',
game.asteroidsParts
));
}

// Setup enemies
game.enemies = [];
for (var i = 0; i < game.enemyLines; i++) {
for (var j = 0; j < game.enemiesEachLine; j++) {
game.enemies.push(new SpaceShip(
game.enemySpaceX + j * game.enemySpaceX,
game.enemySpaceY + i * game.enemySpaceY,
60,
25,
'transparent',
));
}
}
}

// Defines a function to stop the game loop
game.stop = function() {
clearInterval(game.interval);
}

// Defines a function to restart the game
game.restart = function() {
game.stop();
game.init();
}

game.over = function() {
game.stop();
onojs_block('div_game_over');
}

game.won = function() {
game.stop();
onojs_block('div_game_next');
}

// Start the game on window load
window.onload = game.init;

// Detect keydown events
window.onkeydown = game.keydown;
</script>
~;
}

print qq~Content-Type: text/html; charset=UTF-8\n\n
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="Generator" content="ONO App Engine">
$CSSJS
</head>
<body style="margin:0px;padding:0px">
<div style="width:920px;height:480px;overflow:hidden">$GAME</div>
</body>
</html>
~;

}

###################################################################
#### THAT'S IT :-D
###################################################################

1;

__END__