version "4.10" const HDLD_KIRI_JUMPERCABLES = "jmp"; const jumperCablesRaycastRange = 48; // Same range as the DERP. const jumperCablesFixRange = 48; // Maximum distance to a broken linedef to fix, from the raycast hit. class JumperCablesMapLoader : EventHandler { Map originalLinedefSpecials2; Array deployedCables; bool worldScanCompleted; static JumperCablesMapLoader Get() { JumperCablesMapLoader loader; loader = JumperCablesMapLoader(EventHandler.Find("JumperCablesMapLoader")); return loader; } override void WorldUnloaded(WorldEvent e) { JumperCablesMapLoader loader = Get(); // Clear out anything from the last level. deployedCables.Clear(); loader.originalLinedefSpecials2.Clear(); worldScanCompleted = false; } override void WorldTick() { JumperCablesMapLoader loader = Get(); if(!loader.worldScanCompleted) { // Go through all the linedefs and make note of what they're // supposed to do. for(int k = 0; k < level.Lines.size(); k++) { Line line = level.Lines[k]; if(line.special != 0) { loader.originalLinedefSpecials2.Insert(line.index(), line.special); } } loader.worldScanCompleted = true; } } override void WorldLineActivated(WorldEvent e) { // Is this line affected by a battery? for(int cableIndex = 0; cableIndex < deployedCables.size(); cableIndex++) { if(deployedCables[cableIndex].lineDefIndex == e.ActivatedLine.index()) { // Yes. Deplete the battery. deployedCables[cableIndex].DecrementBattery(); break; } } } void RegisterCables(JumperCablesDeployed cableActor) { if(deployedCables.Find(cableActor) == deployedCables.size()) { deployedCables.Push(cableActor); } } void UnregisterCables(JumperCablesDeployed cableActor) { int index = deployedCables.find(cableActor); if(index != deployedCables.size()) { deployedCables.delete(index); } } } class JumperCablesDeployed : HDUPK { int lineDefIndex; int charges; default { radius 2; height 4; +SpriteAngle; Scale 0.4; } states { spawn: JMPR CABD -1; stop; } override void PostBeginPlay() { bNoGravity = true; // Register us with the global event handler so we can // respond to the linedef getting used. JumperCablesMapLoader loader = JumperCablesMapLoader.Get(); loader.RegisterCables(self); UpdateSprite(); } void SetWallSprite(Vector2 lineDir) { bFlatSprite = false; bWallSprite = true; A_SetAngle(atan2(lineDir) + 90); } void SetFlatSprite(float angle) { bFlatSprite = true; bWallSprite = false; A_SetAngle(angle); } void UpdateSprite() { // Charge levels correspond to battery sprite charge levels. if(charges > 13) { frame = 0; } else if(charges > 6) { frame = 1; } else if(charges > 0) { frame = 2; } else { frame = 3; } } void DecrementBattery() { charges -= 1; // Console.printf("battery used, now: %d", charges); if(charges <= 0) { charges = 0; DespawnToBattery(); } UpdateSprite(); } void DespawnToBattery() { // Re-break the linedef. level.Lines[lineDefIndex].special = 0; // Spawn a battery, with the same level of charge. HDMagAmmo.SpawnMag(self, "HDBattery", self.charges); // Spawn some smoke. int smokeCount = random(1, 4); float smokeVel = 20.0; for(int k = 0; k < smokeCount; k++) { A_SpawnItemEx( "HDSmokeChunk", 0.0, 0.0, 0.0, frandom(-smokeVel, smokeVel), frandom(-smokeVel, smokeVel), frandom(-smokeVel, smokeVel)); } // Spawn a bunch of sparks. int sparkAmount = random(3,6); for(int k = 0; k < sparkAmount; k++) { SpawnSpark(); } JumperCablesMapLoader loader = JumperCablesMapLoader.Get(); loader.UnregisterCables(self); Destroy(); } override bool OnGrab(Actor grabber) { DespawnToBattery(); return false; } void SpawnSpark() { float sparkSpeed = 5.0; float sparkOffset = 2.0; A_StartSound("misc/arccrackle", CHAN_AUTO); A_SpawnParticle( "white", SPF_RELATIVE | SPF_FULLBRIGHT, random(1, 10), // lifetime frandom(1, 7), // size 0, // angle // offset... frandom(-sparkOffset, sparkOffset), frandom(-sparkOffset, sparkOffset), frandom(-sparkOffset, sparkOffset), // vel... frandom(-sparkSpeed, sparkSpeed), frandom(-sparkSpeed, sparkSpeed), frandom(-sparkSpeed, sparkSpeed)); } override void Tick() { if(frandom(0.0, 1.0) < 0.02) { SpawnSpark(); } // Despawn to battery if the sector gets re-destroyed. if(level.lines[lineDefIndex].special == 0) { DespawnToBattery(); } } } class JumperCablesUsable : HDWeapon { int lastIndicatorStatus; default { +weapon.wimpy_weapon; +inventory.invbar; +hdweapon.droptranslation; +hdweapon.fitsinbackpack; hdweapon.barrelsize 0,0,0; weapon.selectionorder 1014; scale 0.6; inventory.icon "JMPKA0"; inventory.pickupmessage "Picked up a wiring bypass kit."; inventory.pickupsound "derp/crawl"; translation 0; tag "Wiring Bypass Kit"; hdweapon.refid HDLD_KIRI_JUMPERCABLES; } override double WeaponBulk() { return 7; } override bool AddSpareWeapon(actor newowner) { return AddSpareWeaponRegular(newowner); } override hdweapon GetSpareWeapon(actor newowner,bool reverse,bool doselect) { return GetSpareWeaponRegular(newowner,reverse,doselect); } override void DrawSightPicture( HDStatusBar statusBar, HDWeapon weapon, HDPlayerPawn playerPawn, bool sightbob, Vector2 bob, double fov, bool scopeview, Actor hpc, String whichdot) { // Switch reticle colors depending on the LED status. String filename = String.Format("graphics/jmpr_reticle%d.png", lastIndicatorStatus + 1); statusBar.drawImage( filename, (0, 0) + bob, statusBar.DI_SCREEN_CENTER | statusBar.DI_ITEM_TOP, scale : (0.5, 0.5)); } states { spawn: JMPP # 0 offset(0, -32); JMPK A -1; stop; select: JMPP A 1; // Raise it back up... JMPP A 1 offset(0, 128); JMPP A 1 offset(0, 96); JMPP A 1 offset(0, 72); JMPP A 1 offset(0, 64); JMPP A 1 offset(0, 48); JMPP A 1 offset(0, 32); JMPP A 1 offset(0, 16); JMPP A 1 offset(0, 0); goto super::select; // LCD overlay states. ledd_left_indicator: JMIL A 1 offset(0, -32); wait; ledd_right_indicator: JMIR A 1 offset(0, -32); wait; ready: JMPP # 1 { if(PressingFire()) { SetWeaponState("deploy"); } A_WeaponReady(WRF_NOFIRE | WRF_ALLOWUSER3); // USER3 = MagManager. // Line trace to see what we might be inspecting. FLineTraceData linetraceData; DoLineTrace(HDPlayerPawn(invoker.owner), linetraceData); // Overlay setup. A_Overlay(355, "ledd_left_indicator"); A_Overlay(356, "ledd_right_indicator"); // Update displays. UpdateVisualsForLineTrace( HDPlayerPawn(invoker.owner), linetraceData); } goto readyend; readyend: #### # 0 A_ReadyEnd(); #### # 0 A_Jump(256,"ready"); deploy: // Lower it down... JMPP A 1 offset(0, 16); JMPP A 1 offset(0, 32); JMPP A 1 offset(0, 48); JMPP A 1 offset(0, 64); JMPP A 1 offset(0, 72); JMPP A 1 offset(0, 96); JMPP A 1 offset(0, 128); // Play some sounds. JMPP A 0 A_JumpIf(!pressingfire(),"ready"); JMPP A 4 A_StartSound("weapons/pismagclick",CHAN_WEAPON); JMPP A 2 A_StartSound("derp/crawl",CHAN_WEAPON,CHANF_OVERLAP); // Actual cable attachment happens here. JMPP A 1 { AttachCables(HDPlayerPawn(invoker.owner)); } // Raise it back up... JMPP A 1 offset(0, 128); JMPP A 1 offset(0, 96); JMPP A 1 offset(0, 72); JMPP A 1 offset(0, 64); JMPP A 1 offset(0, 48); JMPP A 1 offset(0, 32); JMPP A 1 offset(0, 16); JMPP A 1 offset(0, 0); JMPP A 1 { A_WeaponReady(WRF_NOFIRE | WRF_ALLOWRELOAD | WRF_ALLOWUSER4); } goto ready; user3: #### # 0 A_MagManager("HDBattery"); goto ready; } action void UpdateVisualsForLineTrace(HDPlayerPawn pawn, FLineTraceData linetraceData) { Line hitLine = linetraceData.hitLine; String particleColor = "darkred"; Line brokenLine = LineTraceFindbrokenLine(linetraceData); // Set main frame (LEDs) based on raytrace status. if(brokenLine) { // Found somethine we can fix! particleColor = "green"; player.getpsprite(PSP_WEAPON).frame = 1; } else { if(linetraceData.hitType == TRACE_HitNone) { // Didn't hit anything (yellow light). player.getpsprite(PSP_WEAPON).frame = 0; } else{ // Hit something, but it's not broken (green light). player.getpsprite(PSP_WEAPON).frame = 2; } } // Set display overlay frames. // Default to random numbers. int frame_left = random(0, 15); int frame_right = random(0, 15); Line lineForDisplay = brokenLine; if(!lineForDisplay) { lineForDisplay = hitLine; } if(lineForDisplay) { JumperCablesMapLoader loader = JumperCablesMapLoader.Get(); int originalSpecial = loader.originalLinedefSpecials2.Get(lineForDisplay.index()); frame_right = originalSpecial % 16; frame_left = originalSpecial / 16; } player.getpsprite(355).frame = frame_left; player.getpsprite(356).frame = frame_right; // Play a sound if we just discovered a broken line. if(invoker.lastIndicatorStatus != player.getpsprite(PSP_WEAPON).frame) { if(invoker.lastIndicatorStatus != 1 && player.getpsprite(PSP_WEAPON).frame == 1) { A_StartSound("herp/beep", CHAN_WEAPON); } } // Set indicator status fo the HUD. invoker.lastIndicatorStatus = player.getpsprite(PSP_WEAPON).frame; // Debug particle display for the linetrace. if(hd_debug > 1) { if(linetraceData.hitType != TRACE_HitNone) { Vector3 hitPos = linetraceData.hitLocation - Vec3Offset(0, 0, 0); A_SpawnParticle( particleColor, 0, 10, 2, 0, hitPos.x, hitPos.y, hitPos.z); } } } action float Vec2Mag(Vector2 v) { return sqrt(v.x * v.x + v.y * v.y); } action float GetDistanceToLine(Line testLine, Vector3 position) { Vector2 pos2d; pos2d.x = position.x; pos2d.y = position.y; Vector2 origin = testLine.v1.p; // Get the normalized direction from v1 to v2. Vector2 direction = testLine.v2.p - origin; float direction_magnitude = Vec2Mag(direction); direction = direction / direction_magnitude; // Get the position relative to the origin. pos2d -= origin; // Project pos2d onto direction, to get a position along the line. double t = direction dot pos2d; // Clamp position along line to start/end. if(t < 0.0) { t = 0; } if(t > direction_magnitude) { t = direction_magnitude; } // Closest point on the line (as an offset from origin). Vector2 position_on_line = direction * t; Vector2 delta_to_line = position_on_line - pos2d; float final_distance = Vec2Mag(delta_to_line); return final_distance; } action void DoLineTrace(HDPlayerPawn pawn, FLineTraceData linetraceData) { pawn.LineTrace( pawn.angle, 48, pawn.pitch, flags : TRF_THRUACTORS | TRF_ABSOFFSET, offsetz : pawn.gunPos.z - 1, offsetforward : pawn.gunPos.x, offsetside : pawn.gunPos.y, data : linetraceData); } action Line LineTraceFindbrokenLine(FLineTraceData linetraceData) { JumperCablesMapLoader loader = JumperCablesMapLoader.Get(); // If we're placing this on a wall, then we'll just look at the wall's special. if(linetraceData.hitType == TRACE_HitWall) { if(linetraceData.hitLine) { Line hitLine = linetraceData.hitLine; if(loader.originalLinedefSpecials2.CheckKey(hitLine.index())) { int originalSpecial = loader.originalLinedefSpecials2.Get(hitLine.index()); int currentSpecial = hitLine.special; if(originalSpecial != currentSpecial) { return hitLine; } } } } // If we hit a sector *or* got close enough to another linedef // attached to the sector that we're in, then let's find the // closest linedef within range that we know is damaged. if(linetraceData.hitType == TRACE_HitFloor || linetraceData.hitType == TRACE_HitCeiling || linetraceData.hitType == TRACE_HitWall) { Sector sec = linetraceData.hitSector; Line closestLineInRange = null; float closestLineDistance = jumperCablesFixRange; // Iterate through all linedefs connected to the sector. for(int secLineIndex = 0; secLineIndex < sec.lines.size(); secLineIndex++) { Line secLine = sec.lines[secLineIndex]; // If this is currently special, we don't care about // it (it's working fine, presumably). if(secLine.special == 0) { // If this *used* to be special, we DO care about it! if(loader.originalLinedefSpecials2.CheckKey(secLine.index())) { // Make sure it's actually changed. int originalSpecial = loader.originalLinedefSpecials2.Get(secLine.index()); int currentSpecial = secLine.special; if(originalSpecial != currentSpecial) { // See if this is the closest one we've found so far. float distanceToLine = GetDistanceToLine( secLine, linetraceData.hitLocation); if(distanceToLine <= closestLineDistance) { closestLineInRange = secLine; closestLineDistance = distanceToLine; } } } } } // If we found something broken, then return the closest // thing we found. if(closestLineInRange) { return closestLineInRange; } } return null; } action void AttachCables(HDPlayerPawn pawn) { // Do we have enough ammo? HDMagAmmo batteryAmmo = HDMagAmmo(FindInventory("HDBattery")); if(!batteryAmmo || batteryAmmo.amount < 1) { invoker.owner.A_Log("You need a battery to use this."); return; } JumperCablesMapLoader loader = JumperCablesMapLoader.Get(); FLineTraceData linetraceData; DoLineTrace(pawn, linetraceData); // Are we even close enough to a line? Line brokenLine = LineTraceFindbrokenLine(linetraceData); if(!brokenLine) { invoker.owner.A_Log("Not close enough to repair."); return; } if(!loader.originalLinedefSpecials2.CheckKey(brokenLine.index())) { invoker.owner.A_Log("No mechanism present to repair."); return; } // Check to see if it's something that actually needs fixing. int originalSpecial = loader.originalLinedefSpecials2.Get(brokenLine.index()); int currentSpecial = brokenLine.special; if(originalSpecial == currentSpecial) { invoker.owner.A_Log("This is already functioning as normal."); return; } // Remove battery from inventory, but make sure we're actually // getting a charged one. int batteryCharges = batteryAmmo.TakeMag(true); if(batteryCharges < 1) { invoker.owner.A_Log("No charged batteries available to use."); batteryAmmo.AddAMag(batteryCharges); // Just give it back. return; } // Actually spawn the deployed item. JumperCablesDeployed deployed = JumperCablesDeployed( Spawn( "JumperCablesDeployed", linetraceData.hitLocation - linetraceData.hitDir * 4)); deployed.lineDefIndex = brokenLine.index(); deployed.bNoGravity = true; deployed.charges = batteryCharges; // Use a different kind of flat sprite and angle depending on // if we're attaching to wall or floor/ceiling. if(linetraceData.hitType == TRACE_HitFloor || linetraceData.hitType == TRACE_HitCeiling) { deployed.SetFlatSprite(invoker.owner.angle); } else { Vector2 lineDir = linetraceData.hitLine.v2.p - linetraceData.hitLine.v1.p; deployed.SetWallSprite(lineDir); } // Restore the damn line special already. brokenLine.special = originalSpecial; } override void DrawHUDStuff(HDStatusBar statusBar, HDWeapon weapon, HDPlayerPawn pawn) { // Copied and modified from ThunderBuster display. if(statusBar.hudlevel == 1) { statusBar.DrawBattery( -54, -4, statusBar.DI_SCREEN_CENTER_BOTTOM, reloadorder : true); statusBar.DrawNum( pawn.CountInv("HDBattery"), -46, -8, statusBar.DI_SCREEN_CENTER_BOTTOM); } } override String GetHelpText() { return WEPHELP_FIRE.." Deploy\n"; } }