I am an Infrastructure Engineer and Gameplay Programmer with a B.S. in Software Engineering from Western Governors University.
My work ranges from building reliable systems for enterprise environments to creating engaging, player-driven gameplay experiences.
I enjoy leading projects, collaborating with teams, and bringing technical and creative perspectives together to make great games.
LinkedIn: https://www.linkedin.com/in/aedev98
This Unreal Engine 5 tech demo showcases an ability-casting system built in C++. The system simulates behavior found in many tab-target MMORPGs, where initiating an ability such as a Fireball temporarily places the character into a casting state and displays a progress bar that fills over the duration.
I designed and programmed every gameplay component for this feature, including character state handling, UI behavior, and animation logic. Moving, jumping, entering an anti-magic zone, or dying cancels the cast and resets the animation and UI. Successful casts automatically spawn and fire the selected spell.
All functionality is written in C++, with the core logic housed in a custom Actor Component. Key variables—such as cast time, the animation montage, and the spell VFX—are exposed to the component’s Blueprint, allowing designers to easily configure and reuse the system without modifying code.
Tools & Languages:
bool UAbilityComponent::CanCastAbility()
{
return (!(IsCharInAntiMagicZone || IsMovingOrJumping()));
}
bool UAbilityComponent::IsMovingOrJumping()
{
if (MoveComp == nullptr || CharacterOwner == nullptr) return false;
bool isCharMoving = false;
FVector InputVector = MoveComp->GetLastInputVector();
if (MoveComp->IsFalling() || CharacterOwner->IsJumpProvidingForce() || !InputVector.IsNearlyZero())
{
isCharMoving = true;
}
return isCharMoving;
}
void UAbilityComponent::StartAbilityCast()
{
// Do nothing if character does not meet requirements to start casting an ability.
if (!GetIsCasting() && CanCastAbility())
{
IsCasting = true;
//GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Green, TEXT("Ability cast successfully started."));
// Broadcast ability cast started, seen by CastBarWidget.
OnCastStarted.Broadcast();
// Play the selected ability cast animation montage.
AnimInstance = CharacterOwner->GetMesh()->GetAnimInstance();
if (CastMontage && AnimInstance)
{
float MontageLength = CastMontage->GetPlayLength();
float PlayRate = MontageLength > 0 ? MontageLength / GoalCastTime : 1.0f;
UE_LOG(LogTemp, Warning, TEXT("Montage length: %f"), MontageLength);
AnimInstance->Montage_Play(CastMontage, PlayRate);
}
}
else
{
UE_LOG(LogTemp, Warning, TEXT("Cannot overlap casts or cast while moving or in amz."));
}
}
void UAbilityComponent::CompleteAbilityCast()
{
IsCasting = false;
// Broadcast ability cast completed.
OnCastCompleted.Broadcast();
// Reset cast time.
CurrentCastTime = 0.f;
// Display VFX. (i.e., fireball).
if (SpellToSpawn) {
FVector CharacterLocation = CharacterOwner->GetActorLocation();
FRotator CharacterRotation = CharacterOwner->GetActorRotation();
FVector SpellSpawnLocation = CharacterLocation + CharacterOwner->GetActorForwardVector() * 200;
FTransform SpawnTransform = FTransform(CharacterRotation, SpellSpawnLocation);
GetWorld()->SpawnActor(SpellToSpawn, SpawnTransform);
}
}
void UAbilityComponent::InterruptAbilityCast()
{
IsCasting = false;
OnCastInterrupted.Broadcast();
CurrentCastTime = 0.f;
if (AnimInstance)
{
AnimInstance->Montage_Stop(0.1f, CastMontage); // Stop the ability cast animation montage.
AnimInstance->Montage_Play(InterruptMontage, 1.0f); // Play the ability interrupted animation montage.
}
}
void UAbilityComponent::ProgressCast(float DeltaTime)
{
if (!GetIsCasting()) return;
if (CharacterOwner)
{
CurrentCastTime += DeltaTime;
float castProgress = (CurrentCastTime / GoalCastTime) * 100;
OnCastProgress.Broadcast(castProgress);
// Interrupt ability cast if character is moving or jumping.
if (!CanCastAbility()) {
//UE_LOG(LogTemp, Warning, TEXT("Character is moving or jumping, or in anti magic zone."));
InterruptAbilityCast();
}
if (castProgress >= 100) CompleteAbilityCast();
}
}
// NOTE: Some functions, #includes and boilerplate code removed for brevity.
// Forward declare the character class.
class ACastBarDemoCharacter;
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class CASTBARDEMO_API UAbilityComponent : public UActorComponent
{
GENERATED_BODY()
public:
// Sets default values for this component's properties
UAbilityComponent();
// Public blueprint properties.
//
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float GoalCastTime = 3.0f; // Ability cast time. How long it takes for the cast to complete. Animations will adjust to play at this length.
UPROPERTY(EditAnywhere, BlueprintReadOnly)
bool IsCharInAntiMagicZone = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
UAnimMontage* CastMontage; // The animation that will play when the ability cast starts.
UPROPERTY(EditAnywhere, BlueprintReadWrite)
UAnimMontage* InterruptMontage; // The animation that will play when the ability is interrupted.
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TSubclassOf SpellToSpawn;
private:
// Return true if character is in a state that allows casting. Casting not allowed if character is moving, jumping, or in anti-magic zone.
bool CanCastAbility();
// Return true if character is moving or jumping.
bool IsMovingOrJumping();
public:
// Called every frame. In this demo, serves as wrapper for the ProgressCast function. DeltaTime is passed to ProgressCast.
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
// Start casting an ability. Plays cast animation and notifies HUD subscriber.
UFUNCTION(BlueprintCallable)
void StartAbilityCast();
// Process spell vfx when cast is completed.
UFUNCTION(BlueprintCallable)
void CompleteAbilityCast();
// Interrupt ability cast. Stops ability from casting and resets progress values.
UFUNCTION(BlueprintCallable)
void InterruptAbilityCast();
// Progresses the ability cast by DeltaTime seconds. Updates current cast time & notifies delegate subscribers to perform actions for current cast %. Called on tick.
UFUNCTION()
void ProgressCast(float DeltaTime);
// Returns true if IsCasting.
UFUNCTION(BlueprintCallable)
bool GetIsCasting();
};
void AMyHUD::BeginPlay()
{
Super::BeginPlay();
//GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, TEXT("HUD constructor called."));
// Create castbar widget based on selected castbar widget class.
if (CastBarWidgetClass)
{
CastBarWidget = CreateWidget(GetWorld(), CastBarWidgetClass); // Instantiate the widget.
if (CastBarWidget)
{
CastBarWidget->AddToViewport(); // Parent created widget to the viewport.
CastBarWidget->HideWidget(); // Hides widget.
//GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Green, TEXT("CastBar Widget created"));
// Bind castbar widget functions to ability component event broadcasts.
ACastBarDemoCharacter* MyChar = Cast(GetOwningPawn());
if (MyChar && MyChar->AbilityComponent)
{
MyChar->AbilityComponent->OnCastStarted.AddDynamic(CastBarWidget, &UCastBarWidget::HandleCastStarted);
MyChar->AbilityComponent->OnCastProgress.AddDynamic(CastBarWidget, &UCastBarWidget::HandleCastProgress);
MyChar->AbilityComponent->OnCastCompleted.AddDynamic(CastBarWidget, &UCastBarWidget::HandleCastCompleted);
MyChar->AbilityComponent->OnCastInterrupted.AddDynamic(CastBarWidget, &UCastBarWidget::HandleCastInterrupted);
}
}
else
{
GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, TEXT("CastBar Widget NOT created"));
}
}
else
{
GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Yellow, TEXT("CastBarWidgetClass does not exist."));
}
}
void UCastBarWidget::HandleCastStarted()
{
ShowWidget();
//GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Green, TEXT("Cast started, showing widget."));
}
void UCastBarWidget::HandleCastProgress(float Progress)
{
//FString DebugMessage = FString::Printf(TEXT("Cast time: %.2f percent."), Progress);
//GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Yellow, DebugMessage);
castProgress = Progress;
}
void UCastBarWidget::HandleCastCompleted()
{
HideWidget();
castProgress = 0.f;
}
void UCastBarWidget::HandleCastInterrupted()
{
HideWidget();
castProgress = 0.f;
}
void UCastBarWidget::ShowWidget()
{
SetVisibility(ESlateVisibility::Visible); // Display castbar widget.
}
void UCastBarWidget::HideWidget()
{
SetVisibility(ESlateVisibility::Hidden); // Hides castbar widget.
}
float UCastBarWidget::GetCastProgress()
{
return castProgress;
}
Bubblegotchi is a 3D virtual pet game, developed in Unity for the 2025 Global Game Jam, where players care for a fragile, floating bubble. Through simple yet engaging mechanics, players must keep their companion safe by gently blowing on it to prevent it from touching the ground and providing it company when it feels lonely—while ensuring it doesn’t pop!
As a gameplay programmer on a team of five, I contributed to designing and developing core mechanics, implementing Unity’s Input System for responsive controls, and programmatically managing game object lifecycles. I also created and integrated 3D animations to enhance player feedback, ensuring a polished and immersive experience. Additionally, I handled version control in Git, validating pull requests and resolving merge conflicts to maintain a smooth development process.
Tools & Languages:
In addition to leading a team of 4 for this project, I am the main system designer and programmer, working on all parts of the game. This includes level design, character models and abilities, UX/UI, and gameplay systems.
This is a game that is built on Roblox’s internal engine. Leveraging Lua, I am coding all of the game’s core systems, from NPC pathfinding to ability selection and attack patterns, hitbox detection, client-server data replication, projectile physics simulation, calculating collision detections, and finite-state machines for combat states.
Players will progress vertically by defeating mobs in the open world, leveling their character as they gain exp. At any point in time, players can engage in PvP combat and earn additional rewards. Additionally, C: KoM features a racing system where players can challenge each other in “flying broom” races for rewards.
Tools & Languages:
--[[ Applies a given velocity to the target's abilityLV instance.
This function was also used to develop and test another method of movement involving tweening an anchored HRP, which has since been deprecated. ]]
function physicsModule.applyVelocity(character:Model, physData:{})
local effectsFolder = basicFunctions.getEffectsFolderFromCharacter(character)
local humrp = character:FindFirstChild("HumanoidRootPart")
local humanoid:Humanoid = character:FindFirstChild("Humanoid")
if humrp and humanoid and humanoid.Health > 0 then
local useLinearVelocity = true -- Set to false when testing non-lv movement
local distance:number = physData.knockbackDistance
local duration:number = physData.knockbackDuration
local direction = physData.knockbackDirection
local speed = distance/duration
-- Formula for init velocity is U = 2 * (d / t) - Vf
-- Initial velocity is calculated here so that we can tween down to the final velocity (0) with given duration, while still covering given distance.
local finalVelocity = humanoid.WalkSpeed * (direction).Unit
local initialVelocity = (2 * (distance/duration) - finalVelocity.Magnitude) * (direction).Unit
--print("DEBUG: Applying velocity", initialVelocity, "to", character.Name, ". Final velocity:", finalVelocity)
--print("DEBUG: Final velocity:", finalVelocity)
if duration > 0 then
local lockPart = effectsFolder:FindFirstChild("CharacterLockPart")
lockPart.AlignOrientation.Enabled = false
if useLinearVelocity == true then
local lv = humrp:FindFirstChild("abilityLV")
local timeDelta = 0
local timer = lv.Timer
local maxTime = timer.Max
local startTime = timer.Start
startTime.Value = basicFunctions.convertTo2f(tick())
if timer.Value <= 0 then
-- This is a fresh status, first enable Linear Velocity instance.
lv.VectorVelocity = initialVelocity
lv.Enabled = true
-- Set timer vals
timer.Value = duration
maxTime.Value = duration
-- Start the timer
local timerConn = nil
local function stopTimer()
if lv.Parent == nil then
timerConn:Disconnect()
timerConn = nil
return
end
--print("DEBUG: Clearing timer for physics effect. Timer val left:", timer.Value, "Current time:", tick())
--print("DEBUG Physics effect timer ended. Final vector velocity value:", lv.VectorVelocity)
timer.Value = 0
maxTime.Value = 0
lv.Enabled = false
timerConn:Disconnect()
timerConn = nil
return
end
local elapsed = 0
timerConn = RunService.Heartbeat:Connect(function(dt)
elapsed += dt
if timer.Value > 0 then
if character.Parent == nil or (not humanoid or humanoid.Health <= 0) then
stopTimer()
return
end
timeDelta = elapsed
timer.Value = basicFunctions.convertTo2f(maxTime.Value - timeDelta)
if timer.Value <= 0 then
stopTimer()
return
end
local alphaValue = ts:GetValue(math.min(elapsed / duration, 1), Enum.EasingStyle.Linear, Enum.EasingDirection.Out)
local calculatedVelocity = basicFunctions.lerp(initialVelocity, finalVelocity, alphaValue)
lv.VectorVelocity = calculatedVelocity
else
stopTimer()
end
end)
else
--print("DEBUG: Updating timer for NEW vector.")
-- Reset the timer if new effect is being applied.
timer.Value = duration
maxTime.Value = duration
-- Set new velocity val
lv.VectorVelocity = initialVelocity
--print(timer.Value)
--print(maxTime.Value)
end
else -- Tween HRP directly to a set position.
local targetPart = Instance.new("Part")
targetPart.Name = "Target Part"
targetPart.CanCollide = false
targetPart.CanQuery = false
targetPart.CanTouch = false
targetPart.Size = Vector3.new(3, 3, 3)
targetPart.Transparency = 0.4
targetPart.Anchored = true
targetPart.Color = Color3.new(0, 0.517647, 1)
targetPart.CFrame = humrp.CFrame
targetPart.Position = targetPart.CFrame.Position + (direction * distance)
targetPart.Parent = effectsFolder
Debris:AddItem(targetPart, duration + 1)
print("DEBUG: Tweening", character.Name, "to", targetPart.CFrame)
local hrpTween = ts:Create(humrp, TweenInfo.new(duration),{["CFrame"] = targetPart.CFrame})
local tweenCompleted;
if physicsModule.characterList[character] == nil then
physicsModule.characterList[character] = {
isMoving = true
}
else
physicsModule.characterList[character] = {
isMoving = true
}
end
tweenCompleted = hrpTween.Completed:Connect(function(playbackState)
physicsModule.characterList[character].isMoving = false
humrp.Anchored = false
tweenCompleted:Disconnect()
end)
humrp.Anchored = true
hrpTween:Play()
end
end
end
end
--[[Function 'castProjectile' called by casting ability. Performs some of the setup for 'fireProjectile'. Determines what happens when the projectile has reached the end of its path, through the 'castEnded' function.]]
toolsModule.castProjectile = function(stringID:string, abilityPhase:string, abilityData:{}, states:{}, spellVars:{}, keyfrConns:{}, hitboxPart:Part, craterParams:{}, castRotation:CFrame)
-- Enable damage detection
spellVars.canDmg = true
-- Will shoot a projectile so init projectile terminated to false.
spellVars.projectileTerminated = false
-- Face character toward cursor before firing projectile.
if abilityData.followMouse == nil or abilityData.followMouse == true then
local rootPos = humrp.Position
humrp.CFrame = CFrame.new(rootPos, Vector3.new(calculatedCrosshair.X, rootPos.Y, calculatedCrosshair.Z))
end
-- calculate raycast info
local origin, direction = sendRaycastData(spellVars.renderedSpell.PrimaryPart)
local pathBlocked = hitboxModule.detectPartsAtOrigin(player, spellVars.renderedSpell.PrimaryPart)
-- Render on other clients.
reAbilityPhases:FireServer(stringID, abilityData.inputKey, "executeAbility", origin, calculatedCrosshair, pathBlocked, "dmgOn", spellVars)
-- Initialize hit entities data for hitbox
local touchedEntities = {}
local function castEnded(raycastResult:RaycastResult)
--print("DEBUG: Cast terminating.")
spellVars.projectileTerminated = true
if not touchedEntities.Break then
--print("DEBUG: This spell collided with the environment. Generating crater. Total length travelled:", distTravelled, "last point:", lastProjectilePoint)
--print("DEBUG: last position:", cast:GetPosition())
local segments = craterParams.segments
local radius = craterParams.radius
local height = craterParams.height
local width = craterParams.width
local xAngle = craterParams.xAngle
local speed = craterParams.speed
local lifetime = craterParams.lifetime
--spellVars.craterParams = {origin = origin, direction = direction, segments = segments, radius = radius, height = height, width = width, xAngle = xAngle, speed = speed, lifetime = lifetime}
rockModule.crater(nil, nil, segments, radius, height, width, xAngle, speed, lifetime, raycastResult)
else
spellVars.breakEarly = true
end
toolsModule.fadeOut(stringID, abilityPhase, abilityData, true, states, spellVars, keyfrConns)
end
local function lengthChanged(projectileConn:RBXScriptConnection)
if hitboxModule.findInstanceInEffectsFolder(spellVars.renderedSpell, player) then -- Verify that the instance being manipulated is not already destroyed before attempting to move it.
hitboxModule.createHitbox(player, hitboxPart, touchedEntities, stringID, abilityData, origin, direction)
if touchedEntities.Break then
if projectileConn ~= nil then
projectileConn:Disconnect()
projectileConn = nil
end
castEnded()
end
end
end
local maxDistance = 0
if pathBlocked then
--print("DEBUG: Path was blocked.")
else
maxDistance = abilityData["hitbox1"].maxDistance
end
spellVars.projectileFired = true
local raycastParams = RaycastParams.new()
raycastParams.CollisionGroup = "Spells" -- adds the raycast to the "Spells" collision group so that it cannot collide with anything unless specified in the collision group's settings
raycastParams.FilterType = Enum.RaycastFilterType.Exclude
raycastParams.FilterDescendantsInstances = {character, effectsFolder}
raycastParams.IgnoreWater = true
raycastParams.RespectCanCollide = true
projectileModule.fireProjectile(spellVars.renderedSpell, origin, direction, maxDistance, abilityData["hitbox1"].speed, castRotation, lengthChanged, castEnded, raycastParams)
end
--[[
Primary projectile mover. Used to move a given model (param_1 projectile) over the length of a specified direction and max distance.
This function creates an update event that fires each frame. The delta in OS time is measured between each frame to ensure that the projectile's speed is not effected by poor framerate.
Additionally, this function uses raycasts to check if the projectile has hit a wall or other object. If so, the projectile is destroyed and the update event is disconnected.
Optionally pass in functions to perform on frame update and on projectile reached last point.
Function "fireProjectile" is called from the toolsModule library "castProjectile" function. castProjectile performs preliminary steps like calculating origin, direction, and determining raycast parameters.
]]
projectileModule.fireProjectile = function(projectile:Model, origin:Vector3, direction:Vector3, maxDistance:number, speed:number, projectileOrientation:CFrame, lengthChangedFunc:"Func", castEndedFunc:"Func", raycastParams:RaycastParams)
for i, child in workspace.Testing.junk:GetChildren() do
child:Destroy()
end
-- Calculate goal position based on given origin, direction, and max distance.
local goalPosition = origin + direction * maxDistance
if projectileModule.debugMode then
-- Create a small part at the point
local smallPart = Instance.new("Part")
smallPart.CanCollide = false
smallPart.CanQuery = false
smallPart.Size = Vector3.new(5, 5, 5) -- Size of the small part
smallPart.Color = Color3.new(1, 0, 0.0156863)
smallPart.Position = goalPosition
smallPart.Anchored = true
smallPart.Parent = workspace.Testing.junk
end
local timeToReachGoal = maxDistance / speed -- Time to reach goal is checked on each frame update. If time elapsed > timeToReachGoal, projectile will stop moving.
-- Look @ goal posiiton before firing projectile.
if projectileOrientation ~= nil then
projectile:PivotTo(CFrame.lookAt(projectile.PrimaryPart.Position, goalPosition) * projectileOrientation)
else
projectile:PivotTo(CFrame.lookAt(projectile.PrimaryPart.Position, goalPosition))
end
local elapsed = 0
local originalCFrame = projectile:GetPivot()
local numSteps = 0
--print("DEBUG: Firing projectile. Time to reach goal:", timeToReachGoal, "seconds.")
local conn = nil
conn = runService.Heartbeat:Connect(function(dt) -- Called after every frame update. 'dt' is the time delta (in seconds) between last update and current update.
elapsed += dt
if elapsed < timeToReachGoal then
if projectile ~= nil and projectile.Parent ~= nil then
local currentPosition = projectile.PrimaryPart.Position
local distanceTravelled = (currentPosition - origin).Magnitude
local distanceLeft = maxDistance - distanceTravelled
local alphaValue = ts:GetValue(math.min(elapsed / timeToReachGoal, 1), Enum.EasingStyle.Linear, Enum.EasingDirection.Out) -- Get the alpha val to determine how far the projectile should move along its determined trajectory.
local lerpPos = basicFunctions.lerp(originalCFrame.Position, goalPosition, alphaValue) -- Translate alpha value to world position.
--print("DEBUG: Elapsed time:", elapsed, "alphaValue:", alphaValue, "lerpPos:", lerpPos)
local raycastDistance = (lerpPos - currentPosition).Magnitude -- Determine how many studs to raycast out in front of the projectile's root part.
-- Fire the raycast
local raycastResult:RaycastResult = workspace:Raycast(currentPosition, direction * raycastDistance, raycastParams)
if raycastResult then -- If there is a result, that means there is an obstacle between the projectile and its intended destination. Simulate collision here.
if projectileModule.debugMode then
print("DEBUG: Found object to collide with. I had this much distance left on the projectile cast:", distanceLeft)
print("DEBUG: Raycast hit something. \n", raycastResult)
end
projectile:PivotTo(originalCFrame - originalCFrame.Position + raycastResult.Position)
conn:Disconnect()
conn = nil
if lengthChangedFunc ~= nil then
lengthChangedFunc(conn)
end
--[[Any necessary projectile cleanup not already handled by the parent castProjectile function happens here.
Common use case for this function is to include the crater spawning logic for when a projectile hits a wall. ]]
if castEndedFunc ~= nil then
castEndedFunc(raycastResult)
end
else
projectile:PivotTo(originalCFrame - originalCFrame.Position + lerpPos)
if lengthChangedFunc ~= nil then
lengthChangedFunc(conn)
end
end
if projectileModule.debugMode then
-- Show points along path
local stepSize = 1 -- Distance between each small part
numSteps += 1
local point = origin + direction * (numSteps * stepSize)
-- Create a small part at the point
local smallPart = Instance.new("Part")
smallPart.CanCollide = false
smallPart.CanQuery = false
smallPart.Size = Vector3.new(0.2, 0.2, 0.2) -- Size of the small part
smallPart.CFrame = projectile:GetPivot() --point
smallPart.Anchored = true
smallPart.Parent = workspace.Testing.junk
end
end
else
-- The time to reach the goal has elapsed, so the projectile has reached its destination (or should be terminated to avoid incorrect positioning due to lag.)
conn:Disconnect()
conn = nil
-- After connection is disconnected, run this.
if castEndedFunc ~= nil then
castEndedFunc()
end
end
end)
end
This effort was accomplished with the help of a team of 4 other staff, who helped to design, manage and communicate updates to our player base.
At its core, the server was themed around a “Dragon Ball Z” Minecraft mod. Players could create their own character, follow a “Dragon Ball” – themed questline, level up, spend stat points on new abilities, and participate in both PvP and PvE activities.
Snippet from the server’s voting platform: “Saiyan Craft is a 24/7 Dragon Block C server. Dragon Block C is a mod for Minecraft based on the popular show and Manga Dragon Ball Z. You can become a Human, Namekian, or even a Saiyan like Goku and fight against enemies like Frieza and Saibamen in order to train and become strong enough to use powerful Ki blasts! The mod also includes a saga feature which allows you to complete sagas while you train and fight enemies like Vegeta and the Androids. (The Dragon Block C mod and its components are required to join the server.)”
Tools & Languages:
// ## Created by Kish
// ## © SAIYANCRAFT 2020
// ## FOR USE ON SAIYAN CRAFT OFFICIAL SERVER ****ONLY****
/* ## Labyrinth customnpc mini-boss for SC Halloween event 2020.*/
// ** INIT **
//Attack and Health
npc.setMeleeStrength(14750000);
npc.setRangedStrength(14750000);
npc.setMaxHealth(2000000000);
npc.setHealth(2000000000);
npc.setTempData("halfHpLineSaid", false);
npc.setTempData("nearDeathLineSaid", false);
npc.setTempData("transformed", false);
npc.setTempData("dead", false);
npc.setTempData("hitsTracker", []);
npc.setTempData("damageTracker", []);
npc.setTempData("lastHp", npc.getHealth());
var x = npc.x;
var y = npc.y;
var z = npc.z;
world.spawnClone(x-5, y, z-5, 6, "MinionFinale");
world.spawnClone(x-5, y, z+5, 6, "MinionFinale");
world.spawnClone(x+5, y, z+5, 6, "MinionFinale");
world.spawnClone(x+5, y, z-5, 6, "MinionFinale");
world.spawnClone(x-10, y, z-10, 6, "MinionFinale");
world.spawnClone(x-10, y, z+10, 6, "MinionFinale");
world.spawnClone(x+10, y, z+10, 6, "MinionFinale");
world.spawnClone(x+10, y, z-10, 6, "MinionFinale");
world.spawnClone(x-15, y, z-15, 6, "MinionFinale");
world.spawnClone(x-15, y, z+15, 6, "MinionFinale");
world.spawnClone(x+15, y, z+15, 6, "MinionFinale");
world.spawnClone(x+15, y, z-15, 6, "MinionFinale");
// ** UPDATE **
// ## Created by Kish
// ## © SAIYANCRAFT 2020
// ## FOR USE ON SAIYAN CRAFT OFFICIAL SERVER ****ONLY****
var rand = Math.floor(Math.random() * 10);
var x = (npc.x+(" "));
var y = (npc.y+(" "));
var z = (npc.z+(" "));
var coords = (x+y+z);
var dead = npc.getTempData("dead");
// Variables for damage tracker.
var dmg = npc.getTempData("lastHp") - npc.getHealth();
var playerName = npc.getTempData("damagedBy");
var inHitsTracker = false;
var inDamageTracker = false;
var hitsTracker = npc.getTempData("hitsTracker");
var damageTracker = npc.getTempData("damageTracker");
// DAMAGE TRACKER CODE
// Prevents npc from adding to damage trackers if it hasn't been hit.
if (npc.getTempData("addToDamage"))
{
//Check for player name in hits tracker. If player has damaged npc, add +1 to total hits. If it hasn't, add player name to list of hits tracked.
for (var i in hitsTracker){
if (i == playerName)
{
hitsTracker[i] += 1;
npc.setTempData("hitsTracker", hitsTracker);
inHitsTracker = true;
//npc.say("Player " + playerName + " found in hit tracker. Adding 1 hit."); DEBUG
}
}
if (!inHitsTracker)
{
hitsTracker[playerName] = 1;
npc.setTempData("hitsTracker", hitsTracker);
//npc.say("Player " + playerName + " NOT found in hit tracker. Adding 1 hit."); DEBUG
}
// Check for player name in damage tracker. If player has damaged npc, add dmg amount to total damage. If it hasn't, add player name and current damage to damage tracked.
for (var i in damageTracker){
if (i == playerName)
{
damageTracker[i] += dmg;
npc.setTempData("damageTracker", damageTracker);
inDamageTracker = true;
//npc.say("Player " + playerName + " found in damage tracker. Adding " + dmg + " damage."); DEBUG
}
}
if (!inDamageTracker)
{
damageTracker[playerName] = dmg;
npc.setTempData("damageTracker", damageTracker);
//npc.say("Player " + playerName + " NOT found in damage tracker. Adding " + dmg + " damage."); DEBUG
}
}
npc.setTempData("addToDamage", false);
// Initialize target var
var target = npc.getTempData("target");
// ** NOTE TO SELF: NPC'S SET TO "DEFENSE FACTION MEMBERS" WILL BREAK THE INSTANT TRANSMISSION SCRIPT. THIS FACTION SETTING SEEMS TO HANDLE TARGETING DIFFERENTLY.
// Instant transmission
function instantTransmission(x, y, z){
// Teleports close to player.
var newX = Math.random()*2 + x;
var newZ = Math.random()*2 + z;
npc.setPosition(newX, y, newZ);
npc.executeCommand("playsound mob.wither.shoot " + target.getName());
}
if((npc.isAttacking()) && (dead != true) && (target != null))
{
// If npc isn't within range of the player, teleport to player OR shoot ki blast (lower chance for blast). This npc randomly picks between both options.
if((Math.abs(target.x - npc.x) > 20) || (Math.abs(target.y - npc.y) > 10) || (Math.abs(target.z - npc.z) > 20))
{
if(rand <= 3) /// Lower # = lower chance (range from 0 - 10)
{
npc.executeCommand("/dbcspawnki 1 1 9000000 0 5 1 1 100 " +coords);
}
else
{
instantTransmission(target.x, target.y, target.z);
}
}
}
// ** TARGET **
npc.setTempData("target", event.getTarget());
// ** DAMAGED **
// ## Created by Kish
// ## © SAIYANCRAFT 2020
// ## FOR USE ON SAIYAN CRAFT OFFICIAL SERVER ****ONLY****
var source = event.getSource();
var dmg = npc.getMaxHealth() - npc.getHealth();
var halfHpLineSaid = npc.getTempData("halfHpLineSaid");
var nearDeathLineSaid = npc.getTempData("nearDeathLineSaid");
var rand = Math.floor(Math.random() * 10);
// Start gathering information for damage tracker when damaged.
var playerName = source.getName();
npc.setTempData("lastHp", npc.getHealth());
npc.setTempData("damagedBy", playerName);
npc.setTempData("addToDamage", true);
// Say half hp & near-death lines
if ((dmg >= npc.getMaxHealth() / 2) && !halfHpLineSaid)
{
npc.say("This world will be mine. Bow before greatness.");
halfHpLineSaid = true;
npc.setTempData("halfHpLineSaid", halfHpLineSaid);
}
else if ((dmg >= npc.getMaxHealth() / 1.25) && !nearDeathLineSaid)
{
npc.say(".");
nearDeathLineSaid = true;
npc.setTempData("nearDeathLineSaid", nearDeathLineSaid);
}
// ** KILLED **
// ## Created by Kish
// ## © SAIYANCRAFT 2020
// ## FOR USE ON SAIYAN CRAFT OFFICIAL SERVER ****ONLY****
var source = event.getSource();
var playerName = source.getName();
var oneShotKill = true;
// Send message on death
//source.sendMessage("Goku: (Thank you...)");
npc.setTempData("dead", true);
// Reward players based on their contribution to the kill.
var hitsTracker = npc.getTempData("hitsTracker");
var damageTracker = npc.getTempData("damageTracker");
var maxHealth = npc.getMaxHealth();
var minReward = maxHealth / 100; // 1% of boss's hp.
var regReward = maxHealth / 20; // 5% of boss's hp.
var goldReward = maxHealth / 10; // 10% of boss's hp.
var diamondReward = maxHealth / 3; // 30% of boss's hp.
var legendaryReward = maxHealth / 2; // half of boss's hp.
for (var i in hitsTracker)
{
// If the player that kills this NPC isn't found in the damage tracker (because they got last hit or killed it in one hit,) oneShotKill flag stays true, and they are rewarded after players who contributed damage are rewarded.
if (i == playerName)
{
oneShotKill = false;
}
if (damageTracker[i] >= legendaryReward)
{
npc.executeCommand("/citems give -p " + i + " 500milticket -a 10");
npc.executeCommand("/citems give -p " + i + " SoulSack -a 10");
npc.executeCommand("/citems give -p " + i + " immortalsoul -a 10");
npc.executeCommand("/citems give -p " + i + " DemonSlayerGlaive -a 1");
npc.executeCommand("/msg " + i + " You have received the &4Legendary &flevel reward for your contribution in defeating " + npc.getName() + ".");
}
else if (damageTracker[i] >= diamondReward)
{
npc.executeCommand("/citems give -p " + i + " 500milticket -a 8");
npc.executeCommand("/citems give -p " + i + " SoulSack -a 7");
npc.executeCommand("/citems give -p " + i + " immortalsoul -a 5");
npc.executeCommand("/citems give -p " + i + " DemonSlayerGlaive -a 1");
npc.executeCommand("/msg " + i + " You have received the &bDiamond &flevel reward for your contribution in defeating " + npc.getName() + ".");
}
else if (damageTracker[i] >= goldReward)
{
npc.executeCommand("/citems give -p " + i + " 500milticket -a 7");
npc.executeCommand("/citems give -p " + i + " SoulSack -a 5");
npc.executeCommand("/citems give -p " + i + " immortalsoul -a 4");
npc.executeCommand("/citems give -p " + i + " DemonSlayerGlaive -a 1");
npc.executeCommand("/msg " + i + " You have been rewarded for your contribution in defeating " + npc.getName() + ".");
}
else if (damageTracker[i] >= regReward)
{
npc.executeCommand("/citems give -p " + i + " 500milticket -a 6");
npc.executeCommand("/citems give -p " + i + " SoulSack -a 2");
npc.executeCommand("/citems give -p " + i + " immortalsoul -a 4");
npc.executeCommand("/citems give -p " + i + " DemonSlayerGlaive -a 1");
npc.executeCommand("/msg " + i + " You have been rewarded for your contribution in defeating " + npc.getName() + ".");
}
else if ((hitsTracker[i] >= 15) || (damageTracker[i] >= minReward))
{
npc.executeCommand("/citems give -p " + i + " 500milticket -a 4");
npc.executeCommand("/citems give -p " + i + " SoulSack -a 1");
npc.executeCommand("/citems give -p " + i + " immortalsoul -a 4");
npc.executeCommand("/citems give -p " + i + " DemonSlayerGlaive -a 1");
npc.executeCommand("/msg " + i + " You have been rewarded for your contribution in defeating " + npc.getName() + ".");
}
}
if (oneShotKill)
{
npc.executeCommand("/citems give -p " + playerName + " 500milticket -a 4");
npc.executeCommand("/citems give -p " + playerName + " SoulSack -a 1");
npc.executeCommand("/citems give -p " + playerName + " immortalsoul -a 4");
npc.executeCommand("/citems give -p " + playerName + " DemonSlayerGlaive -a 1");
npc.executeCommand("/msg " + playerName + " You have been rewarded for your contribution in defeating " + npc.getName() + ".");
}
else
{
// npc.executeCommand("/msg " + playerName + " Debug! OneShotKill was " + oneShotKill + "!");
}
Au Luxe 79 was a high-end custom “Travel Box” store. I was hired as a full-stack developer to help design, develop and implement a full web application. The site was designed based on customer feedback and intent.
I developed the entire solution, both front-end and backend, and utilized a dedicated web server host to run the site. JavaScript, HTML5 and CSS were used on the front-end to style the web pages, while PHP and SQL were utilized to communicate information to and from the site’s database.
Au Luxe 79 featured a custom “buzz-feed” style product purchase flow. Customers would create an account, choose a product, and then fill out a stylized survey. The answers to the survey were attached to the customer’s order, and sent to the store owner on product purchase. The answers were then used to create a personalized product. Users could sign in and view the answers to their past surveys.
Portfolio page designed for and by Adnan el-Bedawi.
Contact:
Email: elbedawia@yahoo.com
LinkedIn: https://www.linkedin.com/in/aedev98/