449 lines
10 KiB
Ucode
449 lines
10 KiB
Ucode
Class MX_PongGame extends MX_MiniGameBase;
|
|
|
|
var localized string PressToStartText;
|
|
var localized string ScoreText;
|
|
var localized string PlaysText;
|
|
|
|
var int Score,NumPlays;
|
|
|
|
var vector PlayerPad,EnemyPad,BallPos,BallVel,BallTrajectory;
|
|
var float ScreenHeight,BallWidth;
|
|
var enum EBallHeading
|
|
{
|
|
BH_ToPlayer,
|
|
BH_ToEnemy,
|
|
BH_None,
|
|
} BallHeading;
|
|
var array<SoundCue> HitSoundsA,HitSoundsB;
|
|
var SoundCue MissSound[2];
|
|
var int HitSoundIndex[2];
|
|
|
|
var float EnemyPadVel,AITactic,AITacticTimer,AITrajOffset;
|
|
var bool bAIRandom;
|
|
|
|
const PadWidth=0.015;
|
|
const PadHeight=0.15;
|
|
const LevelBoarderSize=0.05;
|
|
const PadMoveLimit=0.125;
|
|
const BallHeight=0.03;
|
|
const InverseXOr=879379227;
|
|
|
|
function Init()
|
|
{
|
|
local int i;
|
|
|
|
Super.Init();
|
|
if (Data!=0)
|
|
i = Data ^ InverseXOr;
|
|
Score = (i >> 16) & 32767;
|
|
if (Score>31767)
|
|
Score = Score-32768;
|
|
NumPlays = i & 65535;
|
|
SetAIRating();
|
|
}
|
|
|
|
final function UpdateScores()
|
|
{
|
|
Data = (((Max(Score,-1000) & 32767) << 16) | (NumPlays & 65535)) ^ InverseXOr;
|
|
SaveConfig();
|
|
}
|
|
|
|
function SetFXTrack(Object O)
|
|
{
|
|
local ObjectReferencer R;
|
|
local int i;
|
|
|
|
if (SoundCue(O)!=None)
|
|
{
|
|
HitSoundsA.AddItem(SoundCue(O));
|
|
HitSoundsB.AddItem(SoundCue(O));
|
|
}
|
|
else if (ObjectReferencer(O)!=None)
|
|
{
|
|
R = ObjectReferencer(O);
|
|
if (R.ReferencedObjects.Length<2)
|
|
return;
|
|
MissSound[0] = SoundCue(R.ReferencedObjects[0]);
|
|
MissSound[1] = SoundCue(R.ReferencedObjects[1]);
|
|
for (i=2; i<R.ReferencedObjects.Length; ++i)
|
|
{
|
|
if ((i & 1)==0)
|
|
HitSoundsA.AddItem(SoundCue(R.ReferencedObjects[i]));
|
|
else HitSoundsB.AddItem(SoundCue(R.ReferencedObjects[i]));
|
|
}
|
|
}
|
|
}
|
|
|
|
function StartGame()
|
|
{
|
|
Super.StartGame();
|
|
SetTimer(2,false,'RespawnBall',Self);
|
|
}
|
|
|
|
function Render(float XPos, float YPos, float XSize, float YSize)
|
|
{
|
|
local float H,W;
|
|
|
|
ScreenHeight = YSize;
|
|
|
|
// Score
|
|
H = WorldInfo.RealTimeSeconds * 0.6;
|
|
Canvas.Font = Font(DynamicLoadObject("UI_Canvas_Fonts.Font_Main",class'Font'));
|
|
W = FMin(YSize/200.f,3.f);
|
|
if (!bGameStarted)
|
|
{
|
|
Canvas.SetDrawColor(128,64,64,Abs(Sin(H))*96.f+128);
|
|
Canvas.SetPos(XPos+XSize*0.4,YPos+YSize*0.2);
|
|
Canvas.DrawText(PressToStartText,,W,W);
|
|
}
|
|
else
|
|
{
|
|
Canvas.SetDrawColor(255,255,128,Abs(Sin(H))*96.f);
|
|
Canvas.SetPos(XPos+XSize*0.2,YPos+YSize*0.22);
|
|
Canvas.DrawText(ScoreText@string(Score),,W,W);
|
|
Canvas.SetPos(XPos+XSize*0.2,YPos+YSize*0.68);
|
|
Canvas.DrawText(PlaysText@string(NumPlays),,W,W);
|
|
}
|
|
|
|
// Borders
|
|
Canvas.SetDrawColor(Abs(Sin(H))*255.f,Abs(Sin(H+1.25))*255.f,Abs(Sin(H+2.35))*255.f,255);
|
|
Canvas.SetPos(XPos,YPos);
|
|
Canvas.DrawTile(Canvas.DefaultTexture,XSize,YSize*LevelBoarderSize,0,0,1,1);
|
|
Canvas.SetPos(XPos,YPos+YSize*(1.f-LevelBoarderSize));
|
|
Canvas.DrawTile(Canvas.DefaultTexture,XSize,YSize*LevelBoarderSize,0,0,1,1);
|
|
|
|
// Player
|
|
H = PadHeight*YSize;
|
|
W = PadWidth*XSize;
|
|
Canvas.SetDrawColor(128,255,128,255);
|
|
Canvas.SetPos(XPos+PlayerPad.X*XSize,YPos+PlayerPad.Y*YSize-H*0.5);
|
|
Canvas.DrawTile(Canvas.DefaultTexture,W,H,0,0,1,1);
|
|
|
|
// Enemy
|
|
Canvas.SetDrawColor(255,68,68,255);
|
|
Canvas.SetPos(XPos+EnemyPad.X*XSize-W,YPos+EnemyPad.Y*YSize-H*0.5);
|
|
Canvas.DrawTile(Canvas.DefaultTexture,W,H,0,0,1,1);
|
|
|
|
// Pong ball
|
|
Canvas.SetDrawColor(255,255,86,255);
|
|
BallWidth = BallHeight*(YSize/XSize);
|
|
H = BallHeight*YSize;
|
|
W = H*0.5;
|
|
Canvas.SetPos(XPos+BallPos.X*XSize-W,YPos+BallPos.Y*YSize-W);
|
|
Canvas.DrawTile(Canvas.DefaultTexture,H,H,0,0,1,1);
|
|
|
|
// Trajectory preview ball
|
|
/*Canvas.SetDrawColor(255,255,86,64);
|
|
Canvas.SetPos(XPos+BallTrajectory.X*XSize-W,YPos+BallTrajectory.Y*YSize-W);
|
|
Canvas.DrawTile(Canvas.DefaultTexture,H,H,0,0,1,1);*/
|
|
}
|
|
|
|
function UpdateMouse(float X, float Y)
|
|
{
|
|
Y /= (ScreenHeight*8/Sensitivity);
|
|
PlayerPad.Y = FClamp(PlayerPad.Y-Y,PadMoveLimit,1.f-PadMoveLimit);
|
|
}
|
|
|
|
function SetMouse(float X, float Y)
|
|
{
|
|
PlayerPad.Y = FClamp(Y/ScreenHeight,PadMoveLimit,1.f-PadMoveLimit);
|
|
}
|
|
|
|
final function RespawnBall()
|
|
{
|
|
BallVel.X = -1;
|
|
BallVel.Y = 0.5-FRand();
|
|
BallVel = Normal2D(BallVel)*0.35;
|
|
BallPos = Default.BallPos;
|
|
BallHeading = BH_ToPlayer;
|
|
EnemyPadVel = 0.f;
|
|
}
|
|
|
|
final function NewRound()
|
|
{
|
|
BallVel = vect(0,0,0);
|
|
BallPos = Default.BallPos;
|
|
BallHeading = BH_None;
|
|
SetTimer(1,false,'RespawnBall',Self);
|
|
}
|
|
|
|
final function PlayHitSound(bool bPlayer)
|
|
{
|
|
if (bPlayer)
|
|
{
|
|
if (HitSoundsA.Length==0)
|
|
return;
|
|
if (HitSoundsA[HitSoundIndex[0]]!=None)
|
|
PlaySound(HitSoundsA[HitSoundIndex[0]],true);
|
|
if (++HitSoundIndex[0]==HitSoundsA.Length)
|
|
HitSoundIndex[0] = 0;
|
|
}
|
|
else
|
|
{
|
|
if (HitSoundsB.Length==0)
|
|
return;
|
|
if (HitSoundsB[HitSoundIndex[1]]!=None)
|
|
PlaySound(HitSoundsB[HitSoundIndex[1]],true);
|
|
if (++HitSoundIndex[1]==HitSoundsB.Length)
|
|
HitSoundIndex[1] = 0;
|
|
}
|
|
}
|
|
|
|
final function PlayerScored(bool bPlayer)
|
|
{
|
|
++NumPlays;
|
|
if (bPlayer)
|
|
{
|
|
++Score;
|
|
if (MissSound[1]!=None)
|
|
PlaySound(MissSound[1],true);
|
|
}
|
|
else
|
|
{
|
|
--Score;
|
|
if (MissSound[0]!=None)
|
|
PlaySound(MissSound[0],true);
|
|
}
|
|
HitSoundIndex[0] = 0;
|
|
HitSoundIndex[1] = 0;
|
|
UpdateScores();
|
|
SetAIRating();
|
|
|
|
BallHeading = BH_None;
|
|
SetTimer(2.5,false,'NewRound',Self);
|
|
}
|
|
|
|
// Calculate where the ball is going to hit on in enemy side.
|
|
final function CalcEndPosition()
|
|
{
|
|
local float T,DY;
|
|
local vector P,V;
|
|
|
|
if (BallVel.X<=0.f) // Never.
|
|
return;
|
|
|
|
V = BallVel;
|
|
P = BallPos;
|
|
|
|
// Get hit time.
|
|
T = (EnemyPad.X - PadWidth - (BallWidth*0.5) - P.X) / V.X;
|
|
|
|
// Now take bounces into account.
|
|
while (true)
|
|
{
|
|
if (V.Y<0.f) // Bottom.
|
|
{
|
|
DY = (LevelBoarderSize + (BallHeight*0.5) - P.Y) / V.Y; // Calc intersection time.
|
|
if (DY<T)
|
|
{
|
|
P+=(V*DY);
|
|
V.Y = -V.Y;
|
|
T-=DY;
|
|
}
|
|
else break; // No more wallhits.
|
|
}
|
|
else if (V.Y>0.f) // Top.
|
|
{
|
|
DY = (1.f - LevelBoarderSize - (BallHeight*0.5) - P.Y) / V.Y;
|
|
if (DY<T)
|
|
{
|
|
P+=(V*DY);
|
|
V.Y = -V.Y;
|
|
T-=DY;
|
|
}
|
|
else break; // No more wallhits.
|
|
}
|
|
else break; // No wallhits!
|
|
}
|
|
BallTrajectory = P+(V*T);
|
|
}
|
|
|
|
function Tick(float Delta)
|
|
{
|
|
local vector V,HN,ExtA,ExtB;
|
|
local float DY;
|
|
local bool bTraj,bRand;
|
|
|
|
// Check collision unless out of bounds already.
|
|
V = BallVel*Delta;
|
|
if (BallHeading!=BH_None)
|
|
{
|
|
// Check paddles
|
|
switch (BallHeading)
|
|
{
|
|
case BH_ToPlayer:
|
|
if (BallPos.X<0.f)
|
|
PlayerScored(false);
|
|
else if ((BallPos.X+V.X)<0.05)
|
|
{
|
|
ExtA.X = PadWidth*0.5;
|
|
ExtA.Y = PadHeight*0.5;
|
|
ExtB.X = BallWidth*0.5;
|
|
ExtB.Y = BallHeight*0.5;
|
|
if (Box8DirTrace(BallPos,V,PlayerPad+vect(0.5,0,0)*PadWidth,ExtB,ExtA,HN,DY))
|
|
{
|
|
BallPos+=(V*DY);
|
|
V = vect(0,0,0);
|
|
|
|
if (HN.X<0.25) // Hit edge of the paddle
|
|
{
|
|
PlayerScored(false);
|
|
BallVel = MirrorVectorByNormal(BallVel,HN);
|
|
}
|
|
else
|
|
{
|
|
AITrajOffset = 0.f;
|
|
if (AITactic>3)
|
|
AITrajOffset = FMin((AITactic-3)*0.35,0.97)*(0.5-FRand())*(PadHeight+BallWidth); // Randomly chose to throw ball in the corners to give angular momentum.
|
|
BallHeading = BH_ToEnemy;
|
|
BallVel.X *= -1.05;
|
|
BallVel.Y = (BallPos.Y-PlayerPad.Y) / PadHeight * Abs(BallVel.X) * 4.5;
|
|
CalcEndPosition();
|
|
PlayHitSound(true);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case BH_ToEnemy:
|
|
if (BallPos.X>1.f)
|
|
PlayerScored(true);
|
|
else if ((BallPos.X+V.X)>0.95)
|
|
{
|
|
ExtA.X = PadWidth*0.5;
|
|
ExtA.Y = PadHeight*0.5;
|
|
ExtB.X = BallWidth*0.5;
|
|
ExtB.Y = BallHeight*0.5;
|
|
if (Box8DirTrace(BallPos,V,EnemyPad-vect(0.5,0,0)*PadWidth,ExtB,ExtA,HN,DY))
|
|
{
|
|
BallPos+=(V*DY);
|
|
V = vect(0,0,0);
|
|
|
|
if (HN.X>-0.25) // Hit edge of the paddle
|
|
{
|
|
PlayerScored(true);
|
|
BallVel = MirrorVectorByNormal(BallVel,HN);
|
|
}
|
|
else
|
|
{
|
|
BallHeading = BH_ToPlayer;
|
|
BallVel.X = -BallVel.X;
|
|
BallVel.Y = (BallPos.Y-EnemyPad.Y) / PadHeight * Abs(BallVel.X) * 4.5;
|
|
PlayHitSound(false);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
// Check edges
|
|
// Top.
|
|
if (V.Y<0.f)
|
|
{
|
|
DY = LevelBoarderSize + (BallHeight*0.5) - BallPos.Y;
|
|
if (DY>V.Y)
|
|
{
|
|
DY = DY / V.Y; // Calc intersection time.
|
|
BallPos+=(V*DY);
|
|
V = vect(0,0,0);
|
|
BallVel.Y = -BallVel.Y;
|
|
BallPos.Y = FMax(BallPos.Y,LevelBoarderSize+(BallHeight*0.5));
|
|
CalcEndPosition();
|
|
}
|
|
}
|
|
// Bottom
|
|
if (V.Y>0.f)
|
|
{
|
|
DY = 1.f - LevelBoarderSize - (BallHeight*0.5) - BallPos.Y;
|
|
if (DY<V.Y)
|
|
{
|
|
DY = DY / V.Y;
|
|
BallPos+=(V*DY);
|
|
V = vect(0,0,0);
|
|
BallVel.Y = -BallVel.Y;
|
|
BallPos.Y = FMin(BallPos.Y,1.f-LevelBoarderSize-(BallHeight*0.5));
|
|
CalcEndPosition();
|
|
}
|
|
}
|
|
|
|
bRand = true;
|
|
if (AITactic>0.f) // Directly follow ball
|
|
{
|
|
bTraj = false;
|
|
if (AITactic<1.f)
|
|
{
|
|
if (BallHeading==BH_ToEnemy)
|
|
bRand = BallPos.X>AITactic;
|
|
}
|
|
else
|
|
{
|
|
bRand = false;
|
|
if (AITactic>2.f && BallHeading==BH_ToEnemy)
|
|
bTraj = (AITactic>=4.f || BallPos.X>(2.f - AITactic*0.5));
|
|
}
|
|
|
|
if (!bRand)
|
|
{
|
|
if (bTraj)
|
|
{
|
|
if (BallPos.X>0.5)
|
|
HN.Y = BallTrajectory.Y+AITrajOffset-EnemyPad.Y;
|
|
else HN.Y = BallTrajectory.Y-EnemyPad.Y;
|
|
HN.X = FMin(2.f + ((AITactic-1.f)*3.f),13.f); // Calc paddle changespeed rate
|
|
DY = FMin(0.15 + (AITactic*0.02f),2.f); // Calc paddle max speed
|
|
}
|
|
else
|
|
{
|
|
HN.Y = BallPos.Y-EnemyPad.Y;
|
|
HN.X = FMin(3.f + (AITactic*6.f),15.f); // Calc paddle changespeed rate
|
|
DY = FMin(0.25 + (AITactic*0.025f),2.f); // Calc paddle max speed
|
|
}
|
|
EnemyPadVel *= (1.f-Delta*HN.X); // Deaccel all the time.
|
|
EnemyPadVel = FClamp(EnemyPadVel+(HN.Y*Delta*HN.X*6.f),-DY,DY);
|
|
}
|
|
}
|
|
|
|
// Update AI
|
|
if (bRand) // Random motion.
|
|
{
|
|
if (AITacticTimer<WorldInfo.TimeSeconds)
|
|
{
|
|
bAIRandom = (Rand(2)==0);
|
|
AITacticTimer = WorldInfo.TimeSeconds+FRand();
|
|
}
|
|
DY = FMax(FMin(Delta,0.65f-Abs(EnemyPadVel)),0.f);
|
|
if (bAIRandom)
|
|
EnemyPadVel += DY;
|
|
else EnemyPadVel -= DY;
|
|
}
|
|
|
|
// Apply by velocity and limit movement.
|
|
EnemyPad.Y = EnemyPad.Y+(EnemyPadVel*Delta);
|
|
if (EnemyPad.Y<PadMoveLimit)
|
|
{
|
|
EnemyPad.Y = PadMoveLimit;
|
|
EnemyPadVel = FMax(EnemyPadVel,0.f);
|
|
}
|
|
else if (EnemyPad.Y>(1.f-PadMoveLimit))
|
|
{
|
|
EnemyPad.Y = 1.f-PadMoveLimit;
|
|
EnemyPadVel = FMin(EnemyPadVel,0.f);
|
|
}
|
|
}
|
|
BallPos+=V;
|
|
}
|
|
|
|
final function SetAIRating()
|
|
{
|
|
AITactic = float(Score)*0.1+0.5;
|
|
}
|
|
|
|
defaultproperties
|
|
{
|
|
PlayerPad=(X=0.005,Y=0.5)
|
|
EnemyPad=(X=0.995,Y=0.5)
|
|
ScreenHeight=800
|
|
BallPos=(X=0.75,Y=0.5)
|
|
BallTrajectory=(X=1,Y=0.5)
|
|
BallHeading=BH_None
|
|
}
|