diff --git a/server/src/main/resources/db/migration/V008__Scoring.sql b/server/src/main/resources/db/migration/V008__Scoring.sql
index 40987c497..3453da654 100644
--- a/server/src/main/resources/db/migration/V008__Scoring.sql
+++ b/server/src/main/resources/db/migration/V008__Scoring.sql
@@ -17,9 +17,10 @@ CREATE TABLE IF NOT EXISTS "sessionnumber" (
CREATE TABLE IF NOT EXISTS "killactivity" (
"index" SERIAL PRIMARY KEY NOT NULL,
- "victim_id" INT NOT NULL REFERENCES avatar (id),
"killer_id" INT NOT NULL REFERENCES avatar (id),
+ "victim_id" INT NOT NULL REFERENCES avatar (id),
"victim_exosuit" SMALLINT NOT NULL,
+ "victim_mounted" INT NOT NULL DEFAULT 0,
"weapon_id" SMALLINT NOT NULL,
"zone_id" SMALLINT NOT NULL,
"px" INT NOT NULL,
@@ -33,7 +34,7 @@ CREATE TABLE IF NOT EXISTS "kda" (
"avatar_id" INT NOT NULL REFERENCES avatar (id),
"kills" INT NOT NULL DEFAULT 0,
"deaths" INT NOT NULL DEFAULT 0,
- "assists" INT NOT NULL DEFAULT 0,
+ "revives" INT NOT NULL DEFAULT 0,
UNIQUE(avatar_id)
);
@@ -42,7 +43,7 @@ CREATE TABLE IF NOT EXISTS "kdasession" (
"session_id" INT NOT NULL,
"kills" INT NOT NULL DEFAULT 0,
"deaths" INT NOT NULL DEFAULT 0,
- "assists" INT NOT NULL DEFAULT 0,
+ "revives" INT NOT NULL DEFAULT 0,
UNIQUE(avatar_id, session_id)
);
@@ -73,89 +74,144 @@ CREATE TABLE IF NOT EXISTS "legacykills" (
UNIQUE(avatar_id)
);
-CREATE VIEW kdacampaign AS (
- SELECT
- session.avatar_id,
- SUM(session.kills) AS kills,
- SUM(session.deaths) AS deaths,
- SUM(session.assists) AS assists,
- COUNT(session.avatar_id) AS numberOfSessions
- FROM (
- SELECT avatar_id, session_id, kills, deaths, assists
- FROM kdasession
- UNION ALL
- SELECT avatar_id, 0, kills, deaths, assists
- FROM kda
- ) AS session
- LEFT JOIN kda
- ON kda.avatar_id = session.avatar_id
- GROUP BY session.avatar_id
+CREATE TABLE IF NOT EXISTS "machinedestroyed" (
+ "index" SERIAL PRIMARY KEY NOT NULL,
+ "avatar_id" INT NOT NULL REFERENCES avatar (id),
+ "weapon_id" INT NOT NULL,
+ "machine_type" INT NOT NULL,
+ "machine_faction" SMALLINT NOT NULL,
+ "hacked_faction" SMALLINT NOT NULL,
+ "as_cargo" BOOLEAN NOT NULL,
+ "zone_num" SMALLINT NOT NULL,
+ "px" INT NOT NULL,
+ "py" INT NOT NULL,
+ "pz" INT NOT NULL,
+ "timestamp" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-CREATE VIEW weaponstatcampaign AS (
- SELECT
- weaponstat.avatar_id,
- weaponstat.weapon_id,
- SUM(session.shots_fired) AS shots_fired,
- SUM(session.shots_landed) AS shots_landed,
- SUM(session.kills) AS kills,
- SUM(session.assists) AS assists,
- COUNT(session.session_id) AS numberOfSessions
- FROM (
- SELECT avatar_id, weapon_id, session_id, shots_fired, shots_landed, kills, assists
- FROM weaponstatsession
- UNION ALL
- SELECT avatar_id, weapon_id, 0, shots_fired, shots_landed, kills, assists
- FROM weaponstat
- ) AS session
- LEFT JOIN weaponstat
- ON weaponstat.avatar_id = session.avatar_id
- GROUP BY weaponstat.avatar_id, weaponstat.weapon_id
+CREATE TABLE IF NOT EXISTS "assistactivity" (
+ "index" SERIAL PRIMARY KEY NOT NULL,
+ "victim_id" INT NOT NULL REFERENCES avatar (id),
+ "attacker_id" INT NOT NULL REFERENCES avatar (id),
+ "weapon_id" SMALLINT NOT NULL,
+ "zone_id" SMALLINT NOT NULL,
+ "px" INT NOT NULL,
+ "py" INT NOT NULL,
+ "pz" INT NOT NULL,
+ "exp" INT NOT NULL,
+ "timestamp" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-/*
-Procedure for initializing and increasing the session number.
-Always indexes a new session.
-*/
-CREATE OR REPLACE PROCEDURE proc_sessionnumber_initAndOrIncrease
-(avatarId IN Int, number OUT Int)
-AS
-$$
-BEGIN
- SELECT (MAX(session_id,0)+1) INTO number
- FROM sessionnumber
- WHERE avatar_id = avatarId;
- INSERT INTO sessionnumber
- VALUES (avatarId, number);
-END;
-$$ LANGUAGE plpgsql;
+CREATE TABLE IF NOT EXISTS "supportactivity" (
+ "index" SERIAL PRIMARY KEY NOT NULL,
+ "user_id" INT NOT NULL REFERENCES avatar (id), -- player that provides the support
+ "target_id" INT NOT NULL REFERENCES avatar (id), -- benefactor of the support
+ "target_exosuit" SMALLINT NOT NULL, -- benefactor's exo-suit
+ "interaction_type" SMALLINT NOT NULL, -- classification of support
+ "intermediate_type" INT DEFAULT 0, -- through what medium user_id supports target_id
+ "implement_type" INT DEFAULT 0, -- tool utilized by user_id to support target_id, potentially via interaction with intermediate_type
+ "exp" INT NOT NULL,
+ "timestamp" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE IF NOT EXISTS "ntuactivity" (
+ "avatar_id" INT NOT NULL REFERENCES avatar (id),
+ "zone_id" INT NOT NULL,
+ "building_id" INT NOT NULL,
+ "exp" INT NOT NULL,
+ UNIQUE(avatar_id, zone_id, building_id)
+);
+
+CREATE TABLE IF NOT EXISTS "buildingcapture" (
+ "index" SERIAL PRIMARY KEY NOT NULL,
+ "avatar_id" INT NOT NULL REFERENCES avatar (id),
+ "zone_id" INT NOT NULL,
+ "building_id" INT NOT NULL,
+ "exp" INT NOT NULL,
+ "exp_type" CHAR(3) NOT NULL, /* bep, cep, llu */
+ "timestamp" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE IF NOT EXISTS "expbuildingcapture" (
+ "avatar_id" INT NOT NULL REFERENCES avatar (id),
+ "zone_id" INT NOT NULL,
+ "building_id" INT NOT NULL,
+ "captures" INT NOT NULL,
+ "bep" INT NOT NULL,
+ "cep" INT NOT NULL,
+ UNIQUE(avatar_id, zone_id, building_id)
+);
+
+CREATE TABLE IF NOT EXISTS "llubuildingcapture" (
+ "avatar_id" INT NOT NULL REFERENCES avatar (id),
+ "zone_id" INT NOT NULL,
+ "building_id" INT NOT NULL,
+ "captures" INT NOT NULL,
+ "exp" INT NOT NULL,
+ UNIQUE(avatar_id, zone_id, building_id)
+);
+
+CREATE TABLE IF NOT EXISTS "progressiondebt" (
+ "avatar_id" INT NOT NULL REFERENCES avatar (id),
+ "experience" INT NOT NULL DEFAULT 0,
+ UNIQUE(avatar_id)
+);
/*
Procedure for initializing and increasing the session number.
Index for a new session only if last session was created more than one hour ago.
*/
-CREATE OR REPLACE PROCEDURE proc_sessionnumber_initAndOrIncreasePerHour
-(avatarId IN Int, number OUT Int, nextNumber OUT Int)
+CREATE OR REPLACE FUNCTION proc_sessionnumber_test
+(avatarId integer)
+RETURNS integer
AS
$$
DECLARE time TIMESTAMP;
+DECLARE number integer;
BEGIN
- SELECT MAX(session_id,0) INTO number
+ SELECT MAX(session_id) INTO number
FROM sessionnumber
WHERE avatar_id = avatarId;
- SELECT timestamp INTO time
+ SELECT COALESCE(timestamp) INTO time
FROM sessionnumber
WHERE avatar_id = avatarId AND session_id = number;
- IF (CAST(CURRENT_TIMESTAMP AS FLOAT) > CAST(DATE_ADD('hour', 1, DATE_TRUNC('hour', time)) AS FLOAT)) THEN
+ IF (time IS null) THEN
+ number := 0;
+ END IF;
+ RETURN number;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION proc_sessionnumber_initAndOrIncreasePerHour
+(avatarId integer)
+RETURNS integer
+AS
+$$
+DECLARE time TIMESTAMP;
+DECLARE number integer;
+DECLARE nextNumber integer;
+BEGIN
+ SELECT MAX(session_id) INTO number
+ FROM sessionnumber
+ WHERE avatar_id = avatarId;
+ SELECT COALESCE(timestamp) INTO time
+ FROM sessionnumber
+ WHERE avatar_id = avatarId AND session_id = number;
+ IF (time IS null) THEN
+ nextNumber := 1;
+ INSERT INTO sessionnumber
+ VALUES (avatarId, nextNumber);
+ ELSIF (CURRENT_TIMESTAMP > DATE_TRUNC('hour', time) + interval '1' hour) THEN
nextNumber := number + 1;
INSERT INTO sessionnumber
- VALUES (avatarId, number);
+ VALUES (avatarId, nextNumber);
ELSE
nextNumber := number;
UPDATE sessionnumber
SET timestamp = CURRENT_TIMESTAMP
WHERE avatar_id = avatarId AND session_id = number;
END IF;
+ RETURN nextNumber;
END;
$$ LANGUAGE plpgsql;
@@ -163,24 +219,35 @@ $$ LANGUAGE plpgsql;
Procedure for accessing any existing session number.
Actually polls the previous session number from the account table.
*/
-CREATE OR REPLACE PROCEDURE proc_sessionnumber_get
-(avatarId IN Int, number OUT Int)
+CREATE OR REPLACE FUNCTION proc_sessionnumber_get
+(avatarId integer)
+RETURNS integer
AS
$$
+DECLARE sessionId integer;
+DECLARE number integer;
BEGIN
- SELECT COALESCE(session_id,0) INTO number
+ SELECT COALESCE(session_id) INTO sessionId
FROM account
WHERE avatar_logged_in = avatarId;
+ IF sessionId IS NULL THEN
+ number := 0;
+ ELSE
+ number := sessionId;
+ END IF;
+ RETURN number;
END;
$$ LANGUAGE plpgsql;
/*
Procedures for ensuring that row entries can be found in specified tables before some other DML operation updates those row entries.
*/
-CREATE OR REPLACE PROCEDURE proc_kda_addEntryIfNone
-(avatarId IN Int)
+CREATE OR REPLACE FUNCTION proc_kda_addEntryIfNone
+(avatarId integer)
+RETURNS integer
AS
$$
+DECLARE out integer;
BEGIN
IF EXISTS(
SELECT *
@@ -189,43 +256,115 @@ BEGIN
HAVING COUNT(*) = 0) THEN
INSERT INTO kda (avatar_id)
VALUES (avatarId);
+ out := 1;
+ ELSE
+ out := 0;
END IF;
+ RETURN out;
END;
$$ LANGUAGE plpgsql;
-CREATE OR REPLACE PROCEDURE proc_weaponstat_addEntryIfNone
-(avatarId IN Int, weaponId IN Int)
+CREATE OR REPLACE FUNCTION proc_weaponstat_addEntryIfNone
+(avatarId integer, weaponId integer)
+RETURNS integer
AS
$$
-DECLARE sessionId Int;
+DECLARE out integer;
BEGIN
- SELECT proc_sessionnumber_get(avatarId, sessionId);
- IF EXISTS(
- SELECT *
+ IF NOT EXISTS(
+ SELECT avatar_id, weapon_id
FROM weaponstat
WHERE avatar_id = avatarId AND weapon_id = weaponId
- HAVING COUNT(*) = 0) THEN
- INSERT INTO weaponstat (avatar_id, session_id, weapon_id)
- VALUES (avatarId, sessionId, weaponId);
+ GROUP BY avatar_id, weapon_id
+ ) THEN
+ INSERT INTO weaponstat (avatar_id, weapon_id)
+ VALUES (avatarId, weaponId);
+ out := 1;
+ ELSE
+ out := 0;
END IF;
+ RETURN out;
END;
$$ LANGUAGE plpgsql;
-CREATE OR REPLACE PROCEDURE proc_weaponstatsession_addEntryIfNone
-(avatarId IN Int, weaponId IN Int)
+CREATE OR REPLACE FUNCTION proc_weaponstatsession_addEntryIfNoneWithSessionId
+(avatarId integer, weaponId integer, sessionId integer)
+RETURNS integer
+AS
+$$
+DECLARE out integer;
+BEGIN
+ IF NOT EXISTS(
+ SELECT avatar_id, session_id, weapon_id
+ FROM weaponstatsession
+ WHERE avatar_id = avatarId AND weapon_id = weaponId AND session_id = sessionId
+ GROUP BY avatar_id, session_id, weapon_id
+ ) THEN
+ INSERT INTO weaponstatsession (avatar_id, session_id, weapon_id)
+ VALUES (avatarId, sessionId, weaponId);
+ out := 1;
+ ELSE
+ out := 0;
+ END IF;
+ RETURN out;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION proc_weaponstatsession_addEntryIfNone
+(avatarId integer, weaponId integer)
+RETURNS integer
AS
$$
DECLARE sessionId Int;
BEGIN
- SELECT proc_sessionnumber_get(avatarId, sessionId);
- IF EXISTS(
- SELECT *
- FROM weaponstatsession
- WHERE avatar_id = avatarId AND weapon_id = weaponId
- HAVING COUNT(*) = 0) THEN
- INSERT INTO weaponstatsession (avatar_id, session_id, weapon_id)
- VALUES (avatarId, sessionId, weaponId);
+ sessionId := proc_sessionnumber_get(avatarId);
+ RETURN proc_weaponstatsession_addEntryIfNoneWithSessionId(avatarId, weaponId, sessionId);
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION proc_expbuildingcapture_addEntryIfNone
+(avatarId integer, buildingId integer, zoneId integer)
+RETURNS integer
+AS
+$$
+DECLARE out integer;
+BEGIN
+ IF NOT EXISTS(
+ SELECT avatar_id, building_id, zone_id
+ FROM expbuildingcapture
+ WHERE avatar_id = avatarId AND building_id = buildingId AND zone_id = zoneId
+ GROUP BY avatar_id, building_id, zone_id
+ ) THEN
+ INSERT INTO expbuildingcapture (avatar_id, building_id, zone_id, captures, bep, cep)
+ VALUES (avatarId, building_id, zone_id, 0, 0, 0);
+ out := 1;
+ ELSE
+ out := 0;
END IF;
+ RETURN out;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION proc_llubuildingcapture_addEntryIfNone
+(avatarId integer, buildingId integer, zoneId integer)
+RETURNS integer
+AS
+$$
+DECLARE out integer;
+BEGIN
+ IF NOT EXISTS(
+ SELECT avatar_id, building_id, zone_id
+ FROM llubuildingcapture
+ WHERE avatar_id = avatarId AND building_id = buildingId AND zone_id = zoneId
+ GROUP BY avatar_id, building_id, zone_id
+ ) THEN
+ INSERT INTO llubuildingcapture (avatar_id, building_id, zone_id, captures, exp)
+ VALUES (avatarId, building_id, zone_id, 0, 0);
+ out := 1;
+ ELSE
+ out := 0;
+ END IF;
+ RETURN out;
END;
$$ LANGUAGE plpgsql;
@@ -243,11 +382,12 @@ DECLARE sessionId Int;
DECLARE oldSessionId Int;
BEGIN
avatarId := NEW.avatar_logged_in;
- SELECT proc_sessionnumber_initAndOrIncreasePerHour(avatarId, oldSessionId, sessionId);
+ oldSessionId := proc_sessionnumber_test(avatarId);
+ sessionId := proc_sessionnumber_initAndOrIncreasePerHour(avatarId);
IF (sessionId > oldSessionId) THEN
BEGIN
UPDATE account
- SET sessionId = sessionId
+ SET session_id = sessionId
WHERE id = OLD.id;
INSERT INTO kdasession (avatar_id, session_id)
VALUES (avatarId, sessionId);
@@ -258,7 +398,7 @@ END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE TRIGGER psf_account_newSession
-BEFORE UPDATE
+AFTER UPDATE
OF avatar_logged_in
ON account
FOR EACH ROW
@@ -281,12 +421,13 @@ DECLARE victimSessionId Int;
DECLARE killerId Int;
DECLARE victimId Int;
DECLARE weaponId Int;
+DECLARE out integer;
BEGIN
killerId := NEW.killer_id;
victimId := NEW.victim_id;
weaponId := NEW.weapon_id;
- SELECT proc_sessionnumber_get(killerId, killerSessionId);
- SELECT proc_sessionnumber_get(victimId, victimSessionId);
+ killerSessionId := proc_sessionnumber_get(killerId);
+ victimSessionId := proc_sessionnumber_get(victimId);
BEGIN
UPDATE kdasession
SET kills = kills + 1
@@ -298,6 +439,7 @@ BEGIN
WHERE avatar_id = victimId AND session_id = victimSessionId;
END;
BEGIN
+ out := proc_weaponstatsession_addEntryIfNoneWithSessionId(killerId, weaponId, killerSessionId);
UPDATE weaponstatsession
SET kills = kills + 1
WHERE avatar_id = killerId AND session_id = killerSessionId AND weapon_id = weaponId;
@@ -323,10 +465,12 @@ AS
$$
DECLARE avatarId Int;
DECLARE weaponId Int;
+DECLARE out integer;
BEGIN
avatarId := NEW.avatar_id;
weaponId := NEW.weapon_id;
- SELECT proc_weaponstatsession_addEntryIfNone(avatarId, weaponId);
+ out := proc_weaponstatsession_addEntryIfNone(avatarId, weaponId);
+ RETURN NEW;
END;
$$ LANGUAGE plpgsql;
@@ -346,9 +490,11 @@ RETURNS TRIGGER
AS
$$
DECLARE avatarId Int;
+DECLARE out integer;
BEGIN
avatarId := NEW.avatar_id;
- SELECT proc_kda_addEntryIfNone(avatarId);
+ out := proc_kda_addEntryIfNone(avatarId);
+ RETURN NEW;
END;
$$ LANGUAGE plpgsql;
@@ -369,10 +515,12 @@ AS
$$
DECLARE avatarId Int;
DECLARE weaponId Int;
+DECLARE out integer;
BEGIN
avatarId := NEW.avatar_id;
weaponId := NEW.weapon_id;
- SELECT proc_weaponstat_addEntryIfNone(avatarId, weaponId);
+ out := proc_weaponstat_addEntryIfNone(avatarId, weaponId);
+ RETURN NEW;
END;
$$ LANGUAGE plpgsql;
@@ -382,10 +530,40 @@ ON weaponstat
FOR EACH ROW
EXECUTE FUNCTION fn_weaponstat_addEntryIfNone();
+/*
+Before attempting to update the revival data in a character's session kda column,
+set the session id to the current value then
+manually update the column, overriding the process.
+*/
+CREATE OR REPLACE FUNCTION fn_kdasession_updateRevives()
+RETURNS TRIGGER
+AS
+$$
+DECLARE avatarId Int;
+DECLARE sessionId Int;
+DECLARE newRevives Int;
+BEGIN
+ avatarId := NEW.avatar_id;
+ sessionId := proc_sessionnumber_get(avatarId);
+ newRevives := NEW.revives;
+ if (newRevives > 0) THEN
+ UPDATE kdasession
+ SET revives = revives + newRevives
+ WHERE avatar_id = avatarId AND session_id = sessionId;
+ END IF;
+ RETURN NULL;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE TRIGGER psf_kdasession_updateRevives
+BEFORE INSERT
+ON kdasession
+FOR EACH ROW
+EXECUTE FUNCTION fn_kdasession_updateRevives();
+
/*
Upon deletion of row entries for a character's session KDA,
the values are copied over to the campaign KDA record for that character.
-This will fire mainly when called by the trigger for login (above).
*/
CREATE OR REPLACE FUNCTION fn_kdasession_updateOnDelete()
RETURNS TRIGGER
@@ -394,16 +572,18 @@ $$
DECLARE avatarId Int;
DECLARE oldKills Int;
DECLARE oldDeaths Int;
-DECLARE oldAssists Int;
+DECLARE oldRevives Int;
+DECLARE out integer;
BEGIN
avatarId := OLD.avatar_id;
oldKills := OLD.kills;
oldDeaths := OLD.deaths;
- oldAssists := OLD.assists;
+ oldRevives := OLD.Revives;
+ out := proc_kda_addEntryIfNone(avatarId);
UPDATE kda
SET kills = kills + oldKills,
deaths = deaths + oldDeaths,
- assists = assists + oldAssists
+ revives = revives + oldRevives
WHERE avatar_id = avatarId;
RETURN OLD;
END;
@@ -418,7 +598,6 @@ EXECUTE FUNCTION fn_kdasession_updateOnDelete();
/*
Upon deletion of row entries for a character's session weapon stats,
the values are copied over to the campaign weapon stats record for that character.
-This will fire mainly when called by the trigger for login (above).
*/
CREATE OR REPLACE FUNCTION fn_weaponstatsession_updateOnDelete()
RETURNS TRIGGER
@@ -430,6 +609,7 @@ DECLARE oldKills Int;
DECLARE oldAssists Int;
DECLARE oldFired Int;
DECLARE oldLanded Int;
+DECLARE out integer;
BEGIN
avatarId := OLD.avatar_id;
weaponId := OLD.weapon_id;
@@ -437,12 +617,14 @@ BEGIN
oldAssists := OLD.assists;
oldFired := OLD.shots_fired;
oldLanded := OLD.shots_landed;
+ out := proc_weaponstat_addEntryIfNone(avatarId, weaponId);
UPDATE weaponstat
SET kills = kills + oldKills,
assists = assists + oldAssists,
shots_fired = shots_fired + oldFired,
shots_landed = shots_landed + oldLanded
WHERE avatar_id = avatarId AND weapon_id = weaponId;
+ RETURN OLD;
END;
$$ LANGUAGE plpgsql;
@@ -451,3 +633,152 @@ BEFORE DELETE
ON weaponstatsession
FOR EACH ROW
EXECUTE FUNCTION fn_weaponstatsession_updateOnDelete();
+
+/*
+Before inserting a value into the weaponstatsession table to session id -1,
+correct the session id to the most current session id,
+and invalidate the attempted insertion at that -1 session id.
+*/
+CREATE OR REPLACE FUNCTION fn_weaponstatsession_beforeInsert()
+RETURNS TRIGGER
+AS
+$$
+DECLARE avatarId Int;
+DECLARE weaponId Int;
+DECLARE sessionId Int;
+DECLARE oldAssists Int;
+DECLARE oldFired Int;
+DECLARE oldLanded Int;
+DECLARE out integer;
+BEGIN
+ avatarId := NEW.avatar_id;
+ weaponId := NEW.weapon_id;
+ oldAssists := NEW.assists;
+ oldFired := NEW.shots_fired;
+ oldLanded := NEW.shots_landed;
+ sessionId := proc_sessionnumber_get(avatarId);
+ out := proc_weaponstatsession_addEntryIfNoneWithSessionId(avatarId, weaponId, sessionId);
+ BEGIN
+ UPDATE weaponstatsession
+ SET
+ assists = assists + oldAssists,
+ shots_fired = shots_fired + oldFired,
+ shots_landed = shots_landed + oldLanded
+ WHERE avatar_id = avatarId AND session_id = sessionId AND weapon_id = weaponId;
+ END;
+ RETURN NULL;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE TRIGGER psf_weaponstatsession_beforeInsert
+BEFORE INSERT
+ON weaponstatsession
+FOR EACH ROW
+WHEN (NEW.session_id = -1)
+EXECUTE FUNCTION fn_weaponstatsession_beforeInsert();
+
+/*
+A kill assist activity causes a major update to weapon stats:
+the weapon that was used in the activity has the kills count for the killer updated/increased.
+*/
+CREATE OR REPLACE FUNCTION fn_assistactivity_updateRelatedStats()
+RETURNS TRIGGER
+AS
+$$
+DECLARE killerSessionId Int;
+DECLARE killerId Int;
+DECLARE weaponId Int;
+DECLARE out integer;
+BEGIN
+ killerId := NEW.killer_id;
+ weaponId := NEW.weapon_id;
+ killerSessionId := proc_sessionnumber_get(killerId);
+ out := proc_weaponstatsession_addEntryIfNone(killerId, killerSessionId, weaponId);
+ BEGIN
+ UPDATE weaponstatsession
+ SET assists = assists + 1
+ WHERE avatar_id = killerId AND session_id = killerSessionId AND weapon_id = weaponId;
+ END;
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE TRIGGER psf_assistactivity_updateRelatedStats
+AFTER INSERT
+ON killactivity
+FOR EACH ROW
+EXECUTE FUNCTION fn_assistactivity_updateRelatedStats();
+
+/*
+Upon deletion of row entries for a character's building capture table,
+the values are copied over to a total capture record for that character.
+This will sort the values into one of two tables, and,
+for one table, it will add to either one column or to a different column.
+*/
+CREATE OR REPLACE FUNCTION fn_buildingcapture_updateOnDelete()
+RETURNS TRIGGER
+AS
+$$
+DECLARE oldAvatarId Int;
+DECLARE oldZoneId Int;
+DECLARE oldBuildingId Int;
+DECLARE oldExp Int;
+DECLARE oldType CHAR(3);
+DECLARE out integer;
+BEGIN
+ oldAvatarId := NEW.avatar_id;
+ oldZoneId := NEW.zone_id;
+ oldBuildingId := NEW.building_id;
+ oldExp := NEW.exp;
+ oldType := NEW.exp_type;
+ BEGIN
+ IF (oldType LIKE "bep") THEN
+ out := proc_expbuildingcapture_addEntryIfNone(avatarId, buildingId, zoneId);
+ UPDATE expbuildingcapture
+ SET bep = bep + oldExp
+ WHERE avatar_id = oldAvatarId AND zone_id = oldZOneId AND building_id = oldBuildingId;
+ ELSIF (oldType LIKE "cep") THEN
+ out := proc_expbuildingcapture_addEntryIfNone(avatarId, buildingId, zoneId);
+ UPDATE expbuildingcapture
+ SET cep = cep + oldExp
+ WHERE avatar_id = oldAvatarId AND zone_id = oldZOneId AND building_id = oldBuildingId;
+ ELSIF (oldType LIKE "llu") THEN
+ out := proc_llubuildingcapture_addEntryIfNone(avatarId, buildingId, zoneId);
+ UPDATE llubuildingcapture
+ SET exp = exp + oldExp
+ WHERE avatar_id = oldAvatarId AND zone_id = oldZOneId AND building_id = oldBuildingId;
+ END IF;
+ END;
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE TRIGGER psf_buildingcapture_updateOnDelete
+BEFORE DELETE
+ON buildingcapture
+FOR EACH ROW
+EXECUTE FUNCTION fn_buildingcapture_updateOnDelete();
+
+/*
+If a new avatar is created, a corresponding progression debt entry is also created by default.
+*/
+CREATE OR REPLACE FUNCTION fn_avatar_addDebtEntry()
+RETURNS TRIGGER
+AS
+$$
+DECLARE newAvatarId Int;
+BEGIN
+ newAvatarId := NEW.avatar_id;
+ BEGIN
+ INSERT INTO progressiondebt (avatar_id)
+ VALUES (newAvatarId);
+ END;
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE TRIGGER psf_avatar_addDebtEntry
+AFTER INSERT
+ON avatar
+FOR EACH ROW
+EXECUTE FUNCTION fn_avatar_addDebtEntry();
diff --git a/server/src/main/resources/db/migration/V010__Scoring2.sql b/server/src/main/resources/db/migration/V010__Scoring2.sql
deleted file mode 100644
index 56e82eba9..000000000
--- a/server/src/main/resources/db/migration/V010__Scoring2.sql
+++ /dev/null
@@ -1,380 +0,0 @@
-/* changes to objects from V008__Scoring.sql */
-ALTER TABLE killactivity
-ADD COLUMN victim_mounted INT NOT NULL DEFAULT 0;
-
-DROP PROCEDURE IF EXISTS proc_sessionnumber_initAndOrIncrease;
-
-CREATE OR REPLACE PROCEDURE proc_sessionnumber_initAndOrIncreasePerHour
-(avatarId IN Int, number OUT Int, nextNumber OUT Int)
-AS
-$$
-DECLARE time TIMESTAMP;
-BEGIN
- SELECT MAX(session_id) INTO number
- FROM sessionnumber
- WHERE avatar_id = avatarId;
- SELECT COALESCE(timestamp) INTO time
- FROM sessionnumber
- WHERE avatar_id = avatarId AND session_id = number;
- IF (time IS null) THEN
- number := 0;
- nextNumber := 1;
- INSERT INTO sessionnumber
- VALUES (avatarId, nextNumber);
- ELSIF (CURRENT_TIMESTAMP > DATE_TRUNC('hour', time) + interval '1' hour) THEN
- nextNumber := number + 1;
- INSERT INTO sessionnumber
- VALUES (avatarId, nextNumber);
- ELSE
- nextNumber := number;
- UPDATE sessionnumber
- SET timestamp = CURRENT_TIMESTAMP
- WHERE avatar_id = avatarId AND session_id = number;
- END IF;
-END;
-$$ LANGUAGE plpgsql;
-
-CREATE OR REPLACE PROCEDURE proc_sessionnumber_get
-(avatarId IN Int, number OUT Int)
-AS
-$$
-DECLARE sessionId Int;
-BEGIN
- SELECT COALESCE(session_id) INTO sessionId
- FROM account
- WHERE avatar_logged_in = avatarId;
- IF sessionId IS NULL THEN
- number := 0;
- ELSE
- number := sessionId;
- END IF;
-END;
-$$ LANGUAGE plpgsql;
-
-CREATE OR REPLACE PROCEDURE proc_weaponstat_addEntryIfNone
-(avatarId IN Int, weaponId IN Int)
-AS
-$$
-BEGIN
- IF NOT EXISTS(
- SELECT avatar_id, weapon_id
- FROM weaponstat
- WHERE avatar_id = avatarId AND weapon_id = weaponId
- GROUP BY avatar_id, weapon_id
- ) THEN
- INSERT INTO weaponstat (avatar_id, weapon_id)
- VALUES (avatarId, weaponId);
- END IF;
-END;
-$$ LANGUAGE plpgsql;
-
-CREATE OR REPLACE PROCEDURE proc_weaponstatsession_addEntryIfNoneWithSessionId
-(avatarId IN Int, sessionId IN Int, weaponId IN Int)
-AS
-$$
-BEGIN
- IF NOT EXISTS(
- SELECT avatar_id, session_id, weapon_id
- FROM weaponstatsession
- WHERE avatar_id = avatarId AND weapon_id = weaponId AND session_id = sessionId
- GROUP BY avatar_id, session_id, weapon_id
- ) THEN
- INSERT INTO weaponstatsession (avatar_id, session_id, weapon_id)
- VALUES (avatarId, sessionId, weaponId);
- END IF;
-END;
-$$ LANGUAGE plpgsql;
-
-CREATE OR REPLACE PROCEDURE proc_weaponstatsession_addEntryIfNone
-(avatarId IN Int, weaponId IN Int)
-AS
-$$
-DECLARE sessionId Int;
-BEGIN
- CALL proc_sessionnumber_get(avatarId, sessionId);
- CALL proc_weaponstatsession_addEntryIfNoneWithSessionId(avatarId, sessionId, weaponId);
-END;
-$$ LANGUAGE plpgsql;
-
-CREATE OR REPLACE FUNCTION fn_account_newSession()
-RETURNS TRIGGER
-AS
-$$
-DECLARE avatarId Int;
-DECLARE sessionId Int;
-DECLARE oldSessionId Int;
-BEGIN
- avatarId := NEW.avatar_logged_in;
- CALL proc_sessionnumber_initAndOrIncreasePerHour(avatarId, oldSessionId, sessionId);
- IF (sessionId > oldSessionId) THEN
- BEGIN
- UPDATE account
- SET session_id = sessionId
- WHERE id = OLD.id;
- INSERT INTO kdasession (avatar_id, session_id)
- VALUES (avatarId, sessionId);
- END;
- END IF;
- RETURN NEW;
-END;
-$$ LANGUAGE plpgsql;
-
-CREATE OR REPLACE TRIGGER psf_account_newSession
-AFTER UPDATE
-OF avatar_logged_in
-ON account
-FOR EACH ROW
-WHEN (OLD.avatar_logged_in = 0 AND NEW.avatar_logged_in > 0)
-EXECUTE FUNCTION fn_account_newSession();
-
-CREATE OR REPLACE FUNCTION fn_killactivity_updateRelatedStats()
-RETURNS TRIGGER
-AS
-$$
-DECLARE killerSessionId Int;
-DECLARE victimSessionId Int;
-DECLARE killerId Int;
-DECLARE victimId Int;
-DECLARE weaponId Int;
-BEGIN
- killerId := NEW.killer_id;
- victimId := NEW.victim_id;
- weaponId := NEW.weapon_id;
- CALL proc_sessionnumber_get(killerId, killerSessionId);
- CALL proc_sessionnumber_get(victimId, victimSessionId);
- BEGIN
- UPDATE kdasession
- SET kills = kills + 1
- WHERE avatar_id = killerId AND session_id = killerSessionId;
- END;
- BEGIN
- UPDATE kdasession
- SET deaths = deaths + 1
- WHERE avatar_id = victimId AND session_id = victimSessionId;
- END;
- BEGIN
- CALL proc_weaponstatsession_addEntryIfNoneWithSessionId(killerId, killerSessionId, weaponId);
- UPDATE weaponstatsession
- SET kills = kills + 1
- WHERE avatar_id = killerId AND session_id = killerSessionId AND weapon_id = weaponId;
- END;
- RETURN NEW;
-END;
-$$ LANGUAGE plpgsql;
-
-CREATE OR REPLACE FUNCTION fn_weaponstatsession_addEntryIfNone()
-RETURNS TRIGGER
-AS
-$$
-DECLARE avatarId Int;
-DECLARE weaponId Int;
-BEGIN
- avatarId := NEW.avatar_id;
- weaponId := NEW.weapon_id;
- CALL proc_weaponstatsession_addEntryIfNone(avatarId, weaponId);
- RETURN NEW;
-END;
-$$ LANGUAGE plpgsql;
-
-CREATE OR REPLACE FUNCTION fn_kda_addEntryIfNone()
-RETURNS TRIGGER
-AS
-$$
-DECLARE avatarId Int;
-BEGIN
- avatarId := NEW.avatar_id;
- CALL proc_kda_addEntryIfNone(avatarId);
- RETURN NEW;
-END;
-$$ LANGUAGE plpgsql;
-
-CREATE OR REPLACE FUNCTION fn_weaponstat_addEntryIfNone()
-RETURNS TRIGGER
-AS
-$$
-DECLARE avatarId Int;
-DECLARE weaponId Int;
-BEGIN
- avatarId := NEW.avatar_id;
- weaponId := NEW.weapon_id;
- CALL proc_weaponstat_addEntryIfNone(avatarId, weaponId);
- RETURN NEW;
-END;
-$$ LANGUAGE plpgsql;
-
-CREATE OR REPLACE FUNCTION fn_kdasession_updateOnDelete()
-RETURNS TRIGGER
-AS
-$$
-DECLARE avatarId Int;
-DECLARE oldKills Int;
-DECLARE oldDeaths Int;
-DECLARE oldAssists Int;
-BEGIN
- avatarId := OLD.avatar_id;
- oldKills := OLD.kills;
- oldDeaths := OLD.deaths;
- oldAssists := OLD.assists;
- CALL proc_kda_addEntryIfNone(avatarId);
- UPDATE kda
- SET kills = kills + oldKills,
- deaths = deaths + oldDeaths,
- assists = assists + oldAssists
- WHERE avatar_id = avatarId;
- RETURN OLD;
-END;
-$$ LANGUAGE plpgsql;
-
-CREATE OR REPLACE FUNCTION fn_weaponstatsession_updateOnDelete()
-RETURNS TRIGGER
-AS
-$$
-DECLARE avatarId Int;
-DECLARE weaponId Int;
-DECLARE oldKills Int;
-DECLARE oldAssists Int;
-DECLARE oldFired Int;
-DECLARE oldLanded Int;
-BEGIN
- avatarId := OLD.avatar_id;
- weaponId := OLD.weapon_id;
- oldKills := OLD.kills;
- oldAssists := OLD.assists;
- oldFired := OLD.shots_fired;
- oldLanded := OLD.shots_landed;
- CALL proc_weaponstat_addEntryIfNone(avatarId, weaponId);
- UPDATE weaponstat
- SET kills = kills + oldKills,
- assists = assists + oldAssists,
- shots_fired = shots_fired + oldFired,
- shots_landed = shots_landed + oldLanded
- WHERE avatar_id = avatarId AND weapon_id = weaponId;
- RETURN OLD;
-END;
-$$ LANGUAGE plpgsql;
-
-/* new objects */
-CREATE TABLE IF NOT EXISTS machinedestroyed (
- "index" SERIAL PRIMARY KEY NOT NULL,
- "avatar_id" INT NOT NULL REFERENCES avatar (id),
- "weapon_id" INT NOT NULL,
- "machine_type" INT NOT NULL,
- "machine_faction" SMALLINT NOT NULL,
- "hacked_faction" SMALLINT NOT NULL,
- "as_cargo" BOOLEAN NOT NULL,
- "zone_num" SMALLINT NOT NULL,
- "px" INT NOT NULL,
- "py" INT NOT NULL,
- "pz" INT NOT NULL,
- "timestamp" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-);
-
-CREATE TABLE IF NOT EXISTS "assistactivity" (
- "index" SERIAL PRIMARY KEY NOT NULL,
- "victim_id" INT NOT NULL REFERENCES avatar (id),
- "attacker_id" INT NOT NULL REFERENCES avatar (id),
- "weapon_id" SMALLINT NOT NULL,
- "zone_id" SMALLINT NOT NULL,
- "px" INT NOT NULL,
- "py" INT NOT NULL,
- "pz" INT NOT NULL,
- "exp" INT NOT NULL,
- "timestamp" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-);
-
-CREATE TABLE IF NOT EXISTS supportactivity (
- "index" SERIAL PRIMARY KEY NOT NULL,
- "user_id" INT NOT NULL REFERENCES avatar (id), -- player that provides the support
- "target_id" INT NOT NULL REFERENCES avatar (id), -- benefactor of the support
- "target_exosuit" SMALLINT NOT NULL, -- benefactor's exo-suit
- "interaction_type" SMALLINT NOT NULL, -- classification of support
- "intermediate_type" INT DEFAULT 0, -- through what medium user_id supports target_id
- "implement_type" INT DEFAULT 0, -- tool utilized by user_id to support target_id, potentially via interaction with intermediate_type
- "exp" INT NOT NULL,
- "timestamp" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-);
-
-CREATE TABLE IF NOT EXISTS respawnsession (
- "avatar_id" INT NOT NULL REFERENCES avatar (id),
- "session_id" INT NOT NULL,
- "respawn_count" INT NOT NULL DEFAULT 0,
- UNIQUE(avatar_id, session_id)
-);
-
-CREATE TABLE IF NOT EXISTS respawn (
- "avatar_id" INT NOT NULL REFERENCES avatar (id),
- "respawn_count" INT NOT NULL DEFAULT 0,
- UNIQUE(avatar_id)
-);
-
-/*
-Before inserting a value into the weaponstatsession table to session id -1,
-correct the session id to the most current session id,
-and invalidate the attempted insertion at that -1 session id.
-*/
-CREATE OR REPLACE FUNCTION fn_weaponstatsession_beforeInsert()
-RETURNS TRIGGER
-AS
-$$
-DECLARE avatarId Int;
-DECLARE weaponId Int;
-DECLARE sessionId Int;
-DECLARE oldAssists Int;
-DECLARE oldFired Int;
-DECLARE oldLanded Int;
-BEGIN
- avatarId := NEW.avatar_id;
- weaponId := NEW.weapon_id;
- oldAssists := NEW.assists;
- oldFired := NEW.shots_fired;
- oldLanded := NEW.shots_landed;
- CALL proc_sessionnumber_get(avatarId, sessionId);
- CALL proc_weaponstatsession_addEntryIfNoneWithSessionId(avatarId, sessionId, weaponId);
- BEGIN
- UPDATE weaponstatsession
- SET
- assists = assists + oldAssists,
- shots_fired = shots_fired + oldFired,
- shots_landed = shots_landed + oldLanded
- WHERE avatar_id = avatarId AND session_id = sessionId AND weapon_id = weaponId;
- END;
- RETURN NULL;
-END;
-$$ LANGUAGE plpgsql;
-
-CREATE OR REPLACE TRIGGER psf_weaponstatsession_beforeInsert
-BEFORE INSERT
-ON weaponstatsession
-FOR EACH ROW
-WHEN (NEW.session_id = -1)
-EXECUTE FUNCTION fn_weaponstatsession_beforeInsert();
-
-/*
-A kill assist activity causes a major update to weapon stats:
-the weapon that was used in the activity has the kills count for the killer updated/increased.
-*/
-CREATE OR REPLACE FUNCTION fn_assistactivity_updateRelatedStats()
-RETURNS TRIGGER
-AS
-$$
-DECLARE killerSessionId Int;
-DECLARE killerId Int;
-DECLARE weaponId Int;
-BEGIN
- killerId := NEW.killer_id;
- weaponId := NEW.weapon_id;
- CALL proc_sessionnumber_get(killerId, killerSessionId);
- BEGIN
- UPDATE weaponstatsession
- SET assists = assists + 1
- WHERE avatar_id = killerId AND session_id = killerSessionId AND weapon_id = weaponId;
- END;
- RETURN NEW;
-END;
-$$ LANGUAGE plpgsql;
-
-CREATE OR REPLACE TRIGGER psf_assistactivity_updateRelatedStats
-AFTER INSERT
-ON killactivity
-FOR EACH ROW
-EXECUTE FUNCTION fn_assistactivity_updateRelatedStats();
diff --git a/server/src/test/scala/actor/objects/VehicleSpawnPadTest.scala b/server/src/test/scala/actor/objects/VehicleSpawnPadTest.scala
index 86423a0e5..6ca49eca0 100644
--- a/server/src/test/scala/actor/objects/VehicleSpawnPadTest.scala
+++ b/server/src/test/scala/actor/objects/VehicleSpawnPadTest.scala
@@ -231,17 +231,17 @@ object VehicleSpawnPadControlTest {
override def SetupNumberPools(): Unit = {}
}
zone.GUID(guid)
- zone.actor = system.spawn(ZoneActor(zone), s"test-zone-${System.nanoTime()}")
+ zone.actor = system.spawn(ZoneActor(zone), s"test-zone-${System.currentTimeMillis()}")
// Hack: Wait for the Zone to finish booting, otherwise later tests will fail randomly due to race conditions
// with actor probe setting
// TODO(chord): Remove when Zone supports notification of booting being complete
Thread.sleep(5000)
- vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), s"vehicle-control-${System.nanoTime()}")
+ vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), s"vehicle-control-${System.currentTimeMillis()}")
val pad = VehicleSpawnPad(GlobalDefinitions.mb_pad_creation)
- pad.Actor = system.actorOf(Props(classOf[VehicleSpawnControl], pad), s"test-pad-${System.nanoTime()}")
+ pad.Actor = system.actorOf(Props(classOf[VehicleSpawnControl], pad), s"test-pad-${System.currentTimeMillis()}")
pad.Owner =
new Building("Building", building_guid = 0, map_id = 0, zone, StructureType.Building, GlobalDefinitions.building)
pad.Owner.Faction = faction
diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf
index 4e89670d5..d6cc43a9b 100644
--- a/src/main/resources/application.conf
+++ b/src/main/resources/application.conf
@@ -75,15 +75,6 @@ game {
third-party = no
}
- # Battle experience rate
- bep-rate = 1.0
-
- # Command experience rate
- cep-rate = 1.0
-
- # The maximum command experience that can be earned in a facility capture based on squad size
- maximum-cep-per-squad-size = [990, 1980, 3466, 4950, 6436, 7920, 9406, 10890, 12376, 13860]
-
# Modify the amount of mending per autorepair tick for facility amenities
amenity-autorepair-rate = 1.0
@@ -226,6 +217,163 @@ game {
# Don't ask.
doors-can-be-opened-by-med-app-from-this-distance = 5.05
+
+ # How the experience calculates
+ experience {
+ # The short contribution time when events are collected and evaluated.
+ short-contribution-time = 300000
+ # The long contribution time when events are collected and evaluated
+ # even factoring the same events from the short contribution time.
+ # As a result, when comparing the two event lists, similar actors may appear
+ # but their contributions may be different.
+ long-contribution-time = 600000
+
+ # Battle experience points
+ # BEP is to be calculated in relation to how valuable a kill is worth.
+ bep = {
+ # After all calculations are complete, multiple the result by this value
+ rate = 1.0
+ # These numbers are to determine the starting value for a particular kill
+ base = {
+ # Black Ops multiplies the base value by this much
+ bops-multiplier = 10.0
+ # If the player who died ever utilized a mechanized assault exo-suit
+ as-max = 250
+ # The player who died got at least one kill
+ with-kills = 100
+ # The player who died was mounted in a vehicle at the time of death
+ as-mounted = 100
+ # The player who died after having been in the game world for a while after spawning.
+ # Dying before this is often called a "spawn kill".
+ mature = 50
+ }
+ }
+
+ # Support experience points
+ # The events from which support experience rises are numerous.
+ # Calculation is determined by the selection of an "event" that decides how the values are combined.
+ sep = {
+ # After all calculations are complete, multiple the result by this value
+ rate = 1.0
+ # When using an advanced nanite transport to deposit into the resource silo of a major facility,
+ # for reaching the maximum amount of a single deposit,
+ # reward the user with this amount of support experience points.
+ # Small deposits reward only a percentage of this value.
+ ntu-silo-deposit-reward = 100
+ # When the event can not be found, this flat sum is rewarded.
+ # This should not be treated as a feature.
+ # It is a bug.
+ # Check your event label calls.
+ can-not-find-event-default-value = 15
+ # The events by which support experience calculation occurs.
+ # Events can be composed of three parts: a base value, a per-use (shots) value, and an active amount value.
+ # "Per-use" relies on knowledge from the server about the number of times this exact action occurred before the event.
+ # "Active amount" relies on knowledge from the server about how much of the changes for this event are still valid.
+ # Some changes can be undone by other events or other behavior.
+ #
+ # name - label by which this event is organized
+ # base - whole number value
+ # shots-multiplier - whether use count matters for this event
+ # - when set to 0.0 (default), it does not
+ # shots-limit - upper limit of use count
+ # - cap the count here, if higher
+ # shots-cutoff - if the use count exceeds this number, the event no longer applies
+ # - a hard limit that should zero the contribution reward
+ # - the *-cutoff should probably apply before *-limit, maybe
+ # shots-nat-log - when set, may the use count to a natural logarithmic curve
+ # - actually the exponent on the use count before the logarithm
+ # - similar to shots-limit, but the curve plateaus quickly
+ # amount-multiplier - whether active amount matters for this event
+ # - when set to 0.0 (default), it does not
+ events = [
+ {
+ name = "support-heal"
+ base = 10
+ shots-multiplier = 5.0
+ shots-limit = 100
+ amount-multiplier = 2.0
+ }
+ {
+ name = "support-repair"
+ base = 10
+ shots-multiplier = 5.0
+ shots-limit = 100
+ }
+ {
+ name = "support-repair-terminal"
+ base = 10
+ shots-multiplier = 5.0
+ shots-limit = 100
+ }
+ {
+ name = "support-repair-turret"
+ base = 10
+ shots-multiplier = 5.0
+ shots-limit = 100
+ }
+ {
+ name = "mounted-kill"
+ base = 25
+ }
+ {
+ name = "router"
+ base = 15
+ }
+ {
+ name = "hotdrop"
+ base = 25
+ }
+ {
+ name = "hack"
+ base = 5
+ amount-multiplier = 5.0
+ }
+ {
+ name = "ams-resupply"
+ base = 15
+ shots-multiplier = 1.0
+ shots-nat-log = 5.0
+ }
+ {
+ name = "lodestar-repair"
+ base = 10
+ shots-multiplier = 1.0
+ shots-nat-log = 5.0
+ shots-limit = 100
+ amount-multiplier = 1.0
+ }
+ {
+ name = "lodestar-rearm"
+ base = 10
+ shots-multiplier = 1.0
+ shots-nat-log = 5.0
+ }
+ {
+ name = "revival"
+ base = 0
+ shots-multiplier = 15.0
+ shots-nat-log = 5.0
+ shots-cutoff = 10
+ }
+ ]
+ }
+
+ # Support experience points
+ cep = {
+ # After all calculations are complete, multiple the result by this value
+ rate = 1.0
+ # When command experience points are rewarded to the lattice link unit carrier,
+ # modify the original value by this modifier.
+ llu-carrier-modifier = 0.5
+ # The maximum command experience that can be earned in a facility capture based on squad size
+ maximum-per-squad-size = [990, 1980, 3466, 4950, 6436, 7920, 9406, 10890, 12376, 13860]
+ # When the cep has to be capped for squad size, add a small value to the capped value
+ # -1 reuses the cep before being capped
+ squad-size-limit-overflow = -1
+ # When the cep has to be capped for squad size, calculate a small amount to add to the capped value
+ squad-size-limit-overflow-multiplier = 0.2
+ }
+ }
}
anti-cheat {
diff --git a/src/main/scala/net/psforever/actors/session/AvatarActor.scala b/src/main/scala/net/psforever/actors/session/AvatarActor.scala
index 26221e1c7..eb8848bdc 100644
--- a/src/main/scala/net/psforever/actors/session/AvatarActor.scala
+++ b/src/main/scala/net/psforever/actors/session/AvatarActor.scala
@@ -851,12 +851,18 @@ object AvatarActor {
)
}
- def updateToolDischargeFor(avatarId: Long, lives: Seq[Life]): Unit = {
- lives
- .flatMap { _.equipmentStats }
- .foreach { stat =>
- zones.exp.ToDatabase.reportToolDischarge(avatarId, stat)
- }
+ def updateToolDischargeFor(avatar: Avatar): Unit = {
+ updateToolDischargeFor(avatar.id, avatar.scorecard.CurrentLife)
+ }
+
+ def updateToolDischargeFor(avatarId: Long, life: Life): Unit = {
+ updateToolDischargeFor(avatarId, life.equipmentStats)
+ }
+
+ def updateToolDischargeFor(avatarId: Long, stats: Seq[EquipmentStat]): Unit = {
+ stats.foreach { stat =>
+ zones.exp.ToDatabase.reportToolDischarge(avatarId, stat)
+ }
}
def toAvatar(avatar: persistence.Avatar): Avatar = {
@@ -1091,7 +1097,6 @@ class AvatarActor(
.receiveSignal {
case (_, PostStop) =>
AvatarActor.avatarNoLongerLoggedIn(account.id)
- AvatarActor.updateToolDischargeFor(avatar.id.toLong, avatar.scorecard.CurrentLife +: avatar.scorecard.Lives)
Behaviors.same
}
}
@@ -1863,6 +1868,7 @@ class AvatarActor(
AvatarActor.setBepOnly(avatar.id, avatar.bep + supportExperiencePool)
}
saveLockerFunc()
+ AvatarActor.updateToolDischargeFor(avatar)
Behaviors.same
}
}
@@ -3084,6 +3090,7 @@ class AvatarActor(
}
def updateDeaths(deathStat: Death): Unit = {
+ AvatarActor.updateToolDischargeFor(avatar)
avatar.scorecard.rate(deathStat)
val _session = session.get
val zone = _session.zone
@@ -3138,13 +3145,13 @@ class AvatarActor(
def updateExperienceAndType(exp: Long): (Long, ExperienceType) = {
val _session = session.get
val player = _session.player
- val gameOpts = Config.app.game
+ val gameOpts = Config.app.game.experience.bep
val (modifier, msg) = if (player.Carrying.contains(SpecialCarry.RabbitBall)) {
(1.25f, ExperienceType.RabbitBall)
} else {
(1f, ExperienceType.Normal)
}
- ((exp * modifier * gameOpts.bepRate).toLong, msg)
+ ((exp * modifier * gameOpts.rate).toLong, msg)
}
def updateToolDischarge(stats: EquipmentStat): Unit = {
diff --git a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala
index 2af59a51e..f06347af6 100644
--- a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala
+++ b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala
@@ -5,6 +5,7 @@ import akka.actor.typed.scaladsl.adapter._
import akka.actor.{ActorContext, typed}
import net.psforever.packet.game.objectcreate.ConstructorData
import net.psforever.services.Service
+import net.psforever.objects.zones.exp
import scala.collection.mutable
import scala.concurrent.ExecutionContext.Implicits.global
@@ -225,8 +226,6 @@ class SessionAvatarHandlers(
case AvatarResponse.DestroyDisplay(killer, victim, method, unk)
if killer.CharId == avatar.id && killer.Faction != victim.Faction =>
sendResponse(sessionData.destroyDisplayMessage(killer, victim, method, unk))
- //TODO Temporary thing that should go somewhere else and use proper xp values
-// avatarActor ! AvatarActor.AwardCep((100 * Config.app.game.cepRate).toLong)
case AvatarResponse.Destroy(victim, killer, weapon, pos) =>
// guid = victim // killer = killer
@@ -398,71 +397,68 @@ class SessionAvatarHandlers(
case AvatarResponse.UpdateKillsDeathsAssists(_, kda) =>
avatarActor ! AvatarActor.UpdateKillsDeathsAssists(kda)
- case AvatarResponse.AwardBep(_, bep, expType) =>
- avatarActor ! AvatarActor.AwardBep(bep, expType)
+ case AvatarResponse.AwardBep(charId, bep, expType) =>
+ if (charId == player.CharId) {
+ avatarActor ! AvatarActor.AwardBep(bep, expType)
+ }
- case AvatarResponse.AwardCep(0, cep) =>
+ case AvatarResponse.AwardCep(charId, cep) =>
+ //if the target player, always award (some) CEP
+ if (charId == player.CharId) {
+ avatarActor ! AvatarActor.AwardCep(cep)
+ }
+
+ case AvatarResponse.FacilityCaptureRewards(buildingId, zoneNumber, cep) =>
//must be in a squad to earn experience
- val id = player.CharId
+ val cepConfig = Config.app.game.experience.cep
+ val charId = player.CharId
val squadUI = sessionData.squad.squadUI
val participation = continent
- .Buildings
- .values
- .collect { case building if {
- val soi = building.Definition.SOIRadius * building.Definition.SOIRadius
- val pos = player.Position.xy
- Vector3.DistanceSquared(building.Position.xy, pos) < soi
- } =>
+ .Building(buildingId)
+ .map { building =>
building.Participation.PlayerContribution()
}
squadUI
- .find { _._1 == id }
+ .find { _._1 == charId }
.collect {
case (_, elem) if elem.index == 0 =>
- //squad leader earns CEP, modified by squad effort
- val maxRate: Long = {
- val maxCepList = Config.app.game.maximumCepPerSquadSize
- val squadSize: Int = {
- val squadSizeList: Iterable[Int] = participation
- .map { facilityMap =>
- squadUI.count { case (id, _) => facilityMap.contains(id) }
- }
- if (squadSizeList.nonEmpty) {
- squadSizeList.max
- } else {
- 0
- }
- }
+ //squad leader earns CEP, modified by squad effort, capped by squad size present during the capture
+ val squadParticipation = participation match {
+ case Some(map) => map.filter { case (id, _) => squadUI.contains(id) }
+ case _ => Map.empty[Long, Float]
+ }
+ val maxCepBySquadSize: Long = {
+ val maxCepList = cepConfig.maximumPerSquadSize
+ val squadSize: Int = squadParticipation.size
maxCepList.lift(squadSize - 1).getOrElse(squadSize * maxCepList.head).toLong
}
- val groupContribution: Float = {
- val eachSquadMemberParticipation: Iterable[Float] = participation.map { facilityMap =>
- val foundSquadMemberParticipation: Iterable[Float] = squadUI
- .keys
- .flatMap { facilityMap.get }
- if (foundSquadMemberParticipation.nonEmpty) {
- foundSquadMemberParticipation.sum / 10f
- } else {
- 0f
- }
- }
- if (eachSquadMemberParticipation.nonEmpty) {
- eachSquadMemberParticipation.max
+ val groupContribution: Float = squadUI
+ .map { case (id, _) => (id, squadParticipation.getOrElse(id, 0f) / 10f) }
+ .values
+ .max
+ val modifiedExp: Long = (cep.toFloat * groupContribution).toLong
+ val cappedModifiedExp: Long = math.min(modifiedExp, maxCepBySquadSize)
+ val finalExp: Long = if (modifiedExp > cappedModifiedExp) {
+ val overLimitOverflow = if (cepConfig.squadSizeLimitOverflow == -1) {
+ cep.toFloat
} else {
- 0
+ cepConfig.squadSizeLimitOverflow.toFloat
}
+ cappedModifiedExp + (overLimitOverflow * cepConfig.squadSizeLimitOverflowMultiplier).toLong
+ } else {
+ cappedModifiedExp
}
- val modifiedExp: Long = math.min((cep.toFloat * groupContribution).toLong, maxRate)
- avatarActor ! AvatarActor.AwardCep(modifiedExp)
- Some(modifiedExp)
+ exp.ToDatabase.reportFacilityCapture(charId, buildingId, zoneNumber, finalExp, expType="cep")
+ avatarActor ! AvatarActor.AwardCep(finalExp)
+ Some(finalExp)
case _ =>
//squad member earns BEP based on CEP, modified by personal effort
val individualContribution = {
val contributionList = for {
facilityMap <- participation
- if facilityMap.contains(id)
- } yield facilityMap(id)
+ if facilityMap.contains(charId)
+ } yield facilityMap(charId)
if (contributionList.nonEmpty) {
contributionList.max
} else {
@@ -470,26 +466,11 @@ class SessionAvatarHandlers(
}
}
val modifiedExp = (cep * individualContribution).toLong
+ exp.ToDatabase.reportFacilityCapture(charId, buildingId, zoneNumber, modifiedExp, expType="bep")
avatarActor ! AvatarActor.AwardBep(modifiedExp, ExperienceType.Normal)
Some(modifiedExp)
}
- case AvatarResponse.AwardCep(charId, cep) =>
- //if the target player, always award (some) CEP
- val squadUI = sessionData.squad.squadUI
- if (charId == player.CharId) {
- val maxRate: Long = squadUI.find { _._1 == avatar.id } match {
- case Some((_, elem)) if elem.index == 0 =>
- val thisZone = continent.Number
- val squadSize = squadUI.count { case (_, e) => e.zone == thisZone } - 1
- val maxCepList = Config.app.game.maximumCepPerSquadSize
- maxCepList.lift(squadSize).getOrElse(squadSize * maxCepList.head).toLong
- case _ =>
- Config.app.game.maximumCepPerSquadSize.head.toLong
- }
- avatarActor ! AvatarActor.AwardCep(math.min(cep, maxRate))
- }
-
case AvatarResponse.SendResponse(msg) =>
sendResponse(msg)
@@ -526,6 +507,7 @@ class SessionAvatarHandlers(
sessionData.renewCharSavedTimer(fixedLen = 1800L, varLen = 0L)
//player state changes
+ AvatarActor.updateToolDischargeFor(avatar)
player.FreeHand.Equipment.foreach { item =>
DropEquipmentFromInventory(player)(item)
}
@@ -540,9 +522,7 @@ class SessionAvatarHandlers(
}
sessionData.playerActionsToCancel()
sessionData.terminals.CancelAllProximityUnits()
- sessionData.zoning
AvatarActor.savePlayerLocation(player)
- sessionData.shooting.reportOngoingShots(sessionData.shooting.reportOngoingShotsToDatabase)
sessionData.zoning.spawn.shiftPosition = Some(player.Position)
//respawn
diff --git a/src/main/scala/net/psforever/actors/session/support/SessionData.scala b/src/main/scala/net/psforever/actors/session/support/SessionData.scala
index 990ab35da..712b71dfe 100644
--- a/src/main/scala/net/psforever/actors/session/support/SessionData.scala
+++ b/src/main/scala/net/psforever/actors/session/support/SessionData.scala
@@ -2458,9 +2458,9 @@ class SessionData(
src: PlanetSideGameObject with TelepadLike,
dest: PlanetSideGameObject with TelepadLike
): Unit = {
- val time = System.nanoTime
+ val time = System.currentTimeMillis()
if (
- time - recentTeleportAttempt > (2 seconds).toNanos && router.DeploymentState == DriveState.Deployed &&
+ time - recentTeleportAttempt > 2000L && router.DeploymentState == DriveState.Deployed &&
internalTelepad.Active &&
remoteTelepad.Active
) {
diff --git a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala
index b88ae7316..3baf77f96 100644
--- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala
+++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala
@@ -7,6 +7,7 @@ import akka.actor.{ActorContext, ActorRef, Cancellable, typed}
import akka.pattern.ask
import akka.util.Timeout
import net.psforever.login.WorldSession
+import net.psforever.objects.avatar.scoring.ScoreCard
import net.psforever.objects.inventory.InventoryItem
import net.psforever.objects.serverobject.mount.Seat
import net.psforever.objects.serverobject.tube.SpawnTube
@@ -2342,6 +2343,7 @@ class ZoningOperations(
player.avatar = player.avatar.copy(stamina = avatar.maxStamina)
avatarActor ! AvatarActor.RestoreStamina(avatar.maxStamina)
avatarActor ! AvatarActor.ResetImplants()
+ zones.exp.ToDatabase.reportRespawns(tplayer.CharId, ScoreCard.reviveCount(player.avatar.scorecard.CurrentLife))
val obj = Player.Respawn(tplayer)
DefinitionUtil.applyDefaultLoadout(obj)
obj.death_by = tplayer.death_by
diff --git a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala
index a5910e792..77cb44d27 100644
--- a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala
+++ b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala
@@ -9847,6 +9847,7 @@ object GlobalDefinitions {
resource_silo.Damageable = false
resource_silo.Repairable = false
resource_silo.MaxNtuCapacitor = 1000
+ resource_silo.ChargeTime = 105.seconds //from 0-100% in roughly 105s on live (~20%-100% https://youtu.be/veOWToR2nSk?t=1402)
capture_terminal.Name = "capture_terminal"
capture_terminal.Damageable = false
@@ -9856,7 +9857,7 @@ object GlobalDefinitions {
secondary_capture.Name = "secondary_capture"
secondary_capture.Damageable = false
secondary_capture.Repairable = false
- secondary_capture.FacilityHackTime = 1.nanosecond
+ secondary_capture.FacilityHackTime = 1.millisecond
vanu_control_console.Name = "vanu_control_console"
vanu_control_console.Damageable = false
diff --git a/src/main/scala/net/psforever/objects/Player.scala b/src/main/scala/net/psforever/objects/Player.scala
index 963b10357..9b7f55181 100644
--- a/src/main/scala/net/psforever/objects/Player.scala
+++ b/src/main/scala/net/psforever/objects/Player.scala
@@ -110,6 +110,7 @@ class Player(var avatar: Avatar)
Health = Definition.DefaultHealth
Armor = MaxArmor
Capacitor = 0
+ avatar.scorecard.respawn()
released = false
}
isAlive
@@ -124,13 +125,16 @@ class Player(var avatar: Avatar)
def Revive: Boolean = {
Destroyed = false
Health = Definition.DefaultHealth
+ avatar.scorecard.revive()
released = false
true
}
def Release: Boolean = {
- released = true
- backpack = !isAlive
+ if (!released) {
+ released = true
+ backpack = !isAlive
+ }
true
}
@@ -626,6 +630,7 @@ object Player {
if (player.Release) {
val obj = new Player(player.avatar)
obj.Continent = player.Continent
+ obj.avatar.scorecard.respawn()
obj
} else {
player
diff --git a/src/main/scala/net/psforever/objects/Tool.scala b/src/main/scala/net/psforever/objects/Tool.scala
index 17ccd3ce5..0c8b0d286 100644
--- a/src/main/scala/net/psforever/objects/Tool.scala
+++ b/src/main/scala/net/psforever/objects/Tool.scala
@@ -99,7 +99,7 @@ class Tool(private val toolDef: ToolDefinition)
}
def Discharge(rounds: Option[Int] = None): Int = {
- lastDischarge = System.nanoTime()
+ lastDischarge = System.currentTimeMillis()
Magazine = FireMode.Discharge(this, rounds)
}
diff --git a/src/main/scala/net/psforever/objects/avatar/scoring/Life.scala b/src/main/scala/net/psforever/objects/avatar/scoring/Life.scala
index 05ce6f846..527332178 100644
--- a/src/main/scala/net/psforever/objects/avatar/scoring/Life.scala
+++ b/src/main/scala/net/psforever/objects/avatar/scoring/Life.scala
@@ -6,11 +6,14 @@ final case class Life(
assists: Seq[Assist],
death: Option[Death],
equipmentStats: Seq[EquipmentStat],
- supportExperience: Long
+ supportExperience: Long,
+ prior: Option[Life]
)
object Life {
- def apply(): Life = Life(Nil, Nil, None, Nil, 0)
+ def apply(): Life = Life(Nil, Nil, None, Nil, 0, None)
+
+ def revive(prior: Life): Life = Life(Nil, Nil, None, Nil, 0, Some(prior))
def bep(life: Life): Long = {
life.kills.foldLeft(0L)(_ + _.experienceEarned) + life.assists.foldLeft(0L)(_ + _.experienceEarned)
diff --git a/src/main/scala/net/psforever/objects/avatar/scoring/ScoreCard.scala b/src/main/scala/net/psforever/objects/avatar/scoring/ScoreCard.scala
index 8a0a29407..37e94ed80 100644
--- a/src/main/scala/net/psforever/objects/avatar/scoring/ScoreCard.scala
+++ b/src/main/scala/net/psforever/objects/avatar/scoring/ScoreCard.scala
@@ -18,6 +18,8 @@ class ScoreCard() {
def Lives: Seq[Life] = lives
+ def AllLives: Seq[Life] = curr +: lives
+
def Kills: Seq[Kill] = lives.flatMap { _.kills } ++ curr.kills
def KillStatistics: Map[Int, Statistic] = killStatistics.toMap
@@ -39,17 +41,41 @@ class ScoreCard() {
ScoreCard.updateStatisticsFor(assistStatistics, wid.equipment, faction)
}
case d: Death =>
- val expired = curr
- curr = Life()
- lives = expired.copy(death = Some(d)) +: lives
+ curr = curr.copy(death = Some(d))
case value: Long =>
curr = curr.copy(supportExperience = curr.supportExperience + value)
case _ => ()
}
}
+
+ def revive(): Unit = {
+ curr = Life.revive(curr)
+ }
+
+ def respawn(): Unit = {
+ val death = curr
+ curr = Life()
+ lives = death +: lives
+ }
}
object ScoreCard {
+ def respawnCount(card: ScoreCard): Int = {
+ card.AllLives.foldLeft(0)(_ + recursiveReviveCount(_, count = 0))
+ }
+
+ def reviveCount(life: Life): Int = {
+ recursiveReviveCount(life, count = 0)
+ }
+
+ @tailrec
+ private def recursiveReviveCount(life: Life, count: Int): Int = {
+ life.prior match {
+ case None => count
+ case Some(previousLife) => recursiveReviveCount(previousLife, count + 1)
+ }
+ }
+
private def updateEquipmentStat(curr: Life, entry: EquipmentStat): Life = {
updateEquipmentStat(curr, entry, entry.objectId, entry.kills, entry.assists)
}
diff --git a/src/main/scala/net/psforever/objects/definition/converter/CaptureFlagConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/CaptureFlagConverter.scala
index aee98e8ec..bdd7d4906 100644
--- a/src/main/scala/net/psforever/objects/definition/converter/CaptureFlagConverter.scala
+++ b/src/main/scala/net/psforever/objects/definition/converter/CaptureFlagConverter.scala
@@ -17,7 +17,7 @@ class CaptureFlagConverter extends ObjectCreateConverter[CaptureFlag]() {
case _ => Hackable.HackInfo(PlayerSource("", PlanetSideEmpire.NEUTRAL, Vector3.Zero), PlanetSideGUID(0), 0L, 0L)
}
- val millisecondsRemaining = TimeUnit.MILLISECONDS.convert(math.max(0, hackInfo.hackStartTime + hackInfo.hackDuration - System.nanoTime), TimeUnit.NANOSECONDS)
+ val millisecondsRemaining = math.max(0, hackInfo.hackStartTime + hackInfo.hackDuration - System.currentTimeMillis())
Success(
CaptureFlagData(
diff --git a/src/main/scala/net/psforever/objects/serverobject/hackable/Hackable.scala b/src/main/scala/net/psforever/objects/serverobject/hackable/Hackable.scala
index 16f12d5b4..a494f2d85 100644
--- a/src/main/scala/net/psforever/objects/serverobject/hackable/Hackable.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/hackable/Hackable.scala
@@ -25,14 +25,14 @@ trait Hackable {
def HackedBy_=(agent: Option[Player]): Option[HackInfo] = {
(hackedBy, agent) match {
case (None, Some(actor)) =>
- hackedBy = Some(HackInfo(PlayerSource(actor), actor.GUID, System.nanoTime, 0L))
+ hackedBy = Some(HackInfo(PlayerSource(actor), actor.GUID, System.currentTimeMillis(), 0L))
case (Some(info), Some(actor)) =>
if (actor.Faction == this.Faction) {
//hack cleared
hackedBy = None
} else if (actor.Faction != info.hackerFaction) {
//override the hack state with a new hack state if the new user has different faction affiliation
- hackedBy = Some(HackInfo(PlayerSource(actor), actor.GUID, System.nanoTime, 0L))
+ hackedBy = Some(HackInfo(PlayerSource(actor), actor.GUID, System.currentTimeMillis(), 0L))
}
case (_, None) =>
hackedBy = None
diff --git a/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala b/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala
index 0758f502f..a1cf825be 100644
--- a/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala
@@ -7,11 +7,13 @@ import net.psforever.actors.zone.BuildingActor
import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior}
import net.psforever.objects.serverobject.transfer.TransferBehavior
import net.psforever.objects.serverobject.structures.Building
+import net.psforever.objects.zones
import net.psforever.objects.{GlobalDefinitions, Ntu, NtuContainer, NtuStorageBehavior, Vehicle}
import net.psforever.types.{ExperienceType, PlanetSideEmpire}
import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
+import net.psforever.util.Config
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
@@ -188,11 +190,16 @@ class ResourceSiloControl(resourceSilo: ResourceSilo)
.map { v => (v, v.Owners) }
.collect { case (vehicle, Some(owner)) =>
//experience is reported as normal
- val deposit: Long = 100L * math.floor(amount).toLong / math.floor(resourceSilo.MaxNtuCapacitor / 105f).toLong
+ val deposit: Long =
+ (Config.app.game.experience.sep.ntuSiloDepositReward.toFloat *
+ math.floor(amount).toFloat /
+ math.floor(resourceSilo.MaxNtuCapacitor / resourceSilo.Definition.ChargeTime.toMillis.toFloat)
+ ).toLong
vehicle.Zone.AvatarEvents ! AvatarServiceMessage(
owner.name,
AvatarAction.AwardBep(0, deposit, ExperienceType.Normal)
)
+ zones.exp.ToDatabase.reportNtuActivity(owner.charId, resourceSilo.Zone.Number, resourceSilo.Owner.GUID.guid, deposit)
}
}
}
diff --git a/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloDefinition.scala b/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloDefinition.scala
index eda1b65ad..cb07b97a0 100644
--- a/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloDefinition.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloDefinition.scala
@@ -4,12 +4,16 @@ package net.psforever.objects.serverobject.resourcesilo
import net.psforever.objects.NtuContainerDefinition
import net.psforever.objects.serverobject.structures.AmenityDefinition
+import scala.concurrent.duration._
+
/**
* The definition for any `Resource Silo`.
* Object Id 731.
*/
class ResourceSiloDefinition extends AmenityDefinition(731)
with NtuContainerDefinition {
+ var ChargeTime: FiniteDuration = 0.seconds
+
Name = "resource_silo"
MaxNtuCapacitor = 1000
}
diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala b/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala
index dbf6336de..df9aa66c6 100644
--- a/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala
@@ -183,8 +183,7 @@ class Building(
case Some(obj: CaptureTerminal with Hackable) =>
obj.HackedBy match {
case Some(Hackable.HackInfo(p, _, start, length)) =>
- val hack_time_remaining_ms =
- TimeUnit.MILLISECONDS.convert(math.max(0, start + length - System.nanoTime), TimeUnit.NANOSECONDS)
+ val hack_time_remaining_ms = math.max(0, start + length - System.currentTimeMillis())
(true, p.Faction, hack_time_remaining_ms)
case _ =>
(false, PlanetSideEmpire.NEUTRAL, 0L)
diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/participation/FacilityHackParticipation.scala b/src/main/scala/net/psforever/objects/serverobject/structures/participation/FacilityHackParticipation.scala
index b643c36ab..c64c31156 100644
--- a/src/main/scala/net/psforever/objects/serverobject/structures/participation/FacilityHackParticipation.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/structures/participation/FacilityHackParticipation.scala
@@ -11,11 +11,12 @@ import scala.collection.mutable
trait FacilityHackParticipation extends ParticipationLogic {
protected var lastInfoRequest: Long = 0L
protected var infoRequestOverTime: Seq[Long] = Seq[Long]()
- /*
- key: unique player identifier
+ /**
+ key: unique player identifier
+ values: last player entry, number of times updated, time of last update (POSIX time)
*/
protected var playerContribution: mutable.LongMap[(Player, Int, Long)] = mutable.LongMap[(Player, Int, Long)]()
- protected var playerPopulationOverTime: Seq[Map[PlanetSideEmpire.Value, Int]] = Seq[ Map[PlanetSideEmpire.Value, Int]]()
+ protected var playerPopulationOverTime: Seq[Map[PlanetSideEmpire.Value, Int]] = Seq[Map[PlanetSideEmpire.Value, Int]]()
def PlayerContribution(timeDelay: Long): Map[Long, Float] = {
playerContribution
@@ -49,6 +50,13 @@ trait FacilityHackParticipation extends ParticipationLogic {
}
}
+ /**
+ * Eliminate participation for players who have no submitted updates within the time period.
+ * @param list current list of players
+ * @param now current time (ms)
+ * @param before how long before the current time beyond which players should be eliminated (ms)
+ * @see `timeSensitiveFilterAndAppend`
+ */
protected def updatePopulationOverTime(list: List[Player], now: Long, before: Long): Unit = {
var populationList = list
val layer = PlanetSideEmpire.values.map { faction =>
@@ -56,70 +64,82 @@ trait FacilityHackParticipation extends ParticipationLogic {
populationList = everyoneElse
(faction, isFaction.size)
}.toMap[PlanetSideEmpire.Value, Int]
- playerPopulationOverTime = timeSensitiveFilterAndAppend(playerPopulationOverTime, layer, now, before)
+ playerPopulationOverTime = timeSensitiveFilterAndAppend(playerPopulationOverTime, layer, now - before)
}
+ protected def updateTime(now: Long): Unit = {
+ infoRequestOverTime = timeSensitiveFilterAndAppend(infoRequestOverTime, now, now - 900000L)
+ }
+
+ /**
+ * Eliminate entries from the primary input list based on time entries in a secondary time record list.
+ * The time record list must be updated independently.
+ * @param list input list whose entries are edited against time and then is appended
+ * @param newEntry new entry of the appropriate type to append to the end of the output list
+ * @param beforeTime how long before the current time beyond which entries in the input list should be eliminated (ms)
+ * @tparam T it does not matter what the type is
+ * @return the modified list
+ */
protected def timeSensitiveFilterAndAppend[T](
list: Seq[T],
newEntry: T,
- now: Long = System.currentTimeMillis(),
- before: Long
+ beforeTime: Long
): Seq[T] = {
infoRequestOverTime match {
- case Nil =>
- Seq(newEntry)
+ case Nil => Seq(newEntry)
case _ =>
- val beforeTime = now - before
(infoRequestOverTime.indexWhere { _ >= beforeTime } match {
- case -1 =>
- list
- case cutOffIndex =>
- list.drop(cutOffIndex)
+ case -1 => list
+ case cutOffIndex => list.drop(cutOffIndex)
}) :+ newEntry
}
}
}
object FacilityHackParticipation {
- private[participation] def calculateExperienceFromKills(
+ private[participation] def allocateKillsByPlayers(
center: Vector3,
radius: Float,
hackStart: Long,
completionTime: Long,
opposingFaction: PlanetSideEmpire.Value,
contributionVictor: Iterable[(Player, Int, Long)],
- contributionOpposingSize: Int
- ): Long = {
+ ): Iterable[(UniquePlayer, Float, Seq[Kill])] = {
val killMapFunc: Iterable[(Player, Int, Long)] => Iterable[(UniquePlayer, Float, Seq[Kill])] = {
killsEarnedPerPlayerDuringHack(center.xy, radius * radius, hackStart, hackStart + completionTime, opposingFaction)
}
- val killMapValues = killMapFunc(contributionVictor)
- val totalExperienceFromKills = killMapValues.flatMap { _._3.map { _.experienceEarned } }.sum
- val experienceModifier = {
- if (contributionOpposingSize > 0 && contributionOpposingSize < 10) {
- contributionOpposingSize * 0.1f + math.random()
- } else {
- contributionOpposingSize * 0.1f
- }
- }
- (totalExperienceFromKills * experienceModifier).toLong
+ killMapFunc(contributionVictor)
}
- private def killsEarnedPerPlayerDuringHack(
- centerXY: Vector3,
- distanceSq: Float,
- start: Long,
- end: Long,
- faction: PlanetSideEmpire.Value
- )
- (
- list: Iterable[(Player, Int, Long)]
- ): Iterable[(UniquePlayer, Float, Seq[Kill])] = {
+ private[participation] def calculateExperienceFromKills(
+ killMapValues: Iterable[(UniquePlayer, Float, Seq[Kill])],
+ contributionOpposingSize: Int
+ ): Long = {
+ val totalExperienceFromKills = killMapValues
+ .flatMap { _._3.map { _.experienceEarned } }
+ .sum
+ .toFloat
+ (totalExperienceFromKills * contributionOpposingSize.toFloat * 0.1d).toLong
+ }
+
+ private[participation] def killsEarnedPerPlayerDuringHack(
+ centerXY: Vector3,
+ distanceSq: Float,
+ start: Long,
+ end: Long,
+ faction: PlanetSideEmpire.Value
+ )
+ (
+ list: Iterable[(Player, Int, Long)]
+ ): Iterable[(UniquePlayer, Float, Seq[Kill])] = {
val duration = end - start
list.map { case (p, d, _) =>
val killList = p.avatar.scorecard.Kills.filter { k =>
val killTime = k.info.interaction.hitTime
- k.victim.Faction == faction && start < killTime && killTime < end && Vector3.DistanceSquared(centerXY, k.info.interaction.hitPos.xy) < distanceSq
+ k.victim.Faction == faction &&
+ start < killTime &&
+ killTime <= end &&
+ Vector3.DistanceSquared(centerXY, k.info.interaction.hitPos.xy) < distanceSq
}
(PlayerSource(p).unique, math.min(d, duration).toFloat / duration.toFloat, killList)
}
@@ -220,7 +240,8 @@ object FacilityHackParticipation {
victorPop <- victorPopulationNumbers
opposePop <- opposingPopulationNumbers
out = if (
- (opposePop < victorPop && opposePop * healthyPercentage > victorPop) ||
+ (opposePop + victorPop < 8) ||
+ (opposePop < victorPop && opposePop * healthyPercentage > victorPop) ||
(opposePop > victorPop && victorPop * healthyPercentage > opposePop)
) {
1f //balanced enough population
@@ -241,7 +262,7 @@ object FacilityHackParticipation {
overwhelmingOddsBonus: Long
): Long = {
if (opposingSize * steamrollPercentage < victorSize.toFloat) {
- -steamrollBonus * (victorSize - opposingSize) //steamroll by the victor
+ 0L //steamroll by the victor
} else if (victorSize * overwhelmingOddsPercentage <= opposingSize.toFloat) {
overwhelmingOddsBonus + opposingSize + victorSize //victory against overwhelming odds
} else {
diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala b/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala
index 6800ebfbb..4f7639675 100644
--- a/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala
@@ -1,15 +1,17 @@
// Copyright (c) 2023 PSForever
package net.psforever.objects.serverobject.structures.participation
-import net.psforever.objects.serverobject.structures.Building
-import net.psforever.objects.sourcing.PlayerSource
+import net.psforever.objects.serverobject.structures.{Building, StructureType}
+import net.psforever.objects.sourcing.{PlayerSource, UniquePlayer}
import net.psforever.objects.zones.{HotSpotInfo, ZoneHotSpotProjector}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
-import net.psforever.types.{PlanetSideEmpire, Vector3}
+import net.psforever.types.{PlanetSideEmpire, PlanetSideGeneratorState, Vector3}
import net.psforever.util.Config
-
import akka.pattern.ask
import akka.util.Timeout
+import net.psforever.objects.avatar.scoring.Kill
+import net.psforever.objects.zones.exp.ToDatabase
+
import scala.collection.mutable
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
@@ -49,17 +51,13 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci
requestLayers.completeWith(request)
request.onComplete {
case Success(ZoneHotSpotProjector.ExposedHeat(_, _, activity)) =>
- hotSpotLayersOverTime = timeSensitiveFilterAndAppend(hotSpotLayersOverTime, activity, System.currentTimeMillis(), before = 900000L)
+ hotSpotLayersOverTime = timeSensitiveFilterAndAppend(hotSpotLayersOverTime, activity, System.currentTimeMillis() - 900000L)
case _ =>
requestLayers.completeWith(Future(ZoneHotSpotProjector.ExposedHeat(Vector3.Zero, 0, Nil)))
}
requestLayers.future
}
- private def updateTime(now: Long): Unit = {
- infoRequestOverTime = timeSensitiveFilterAndAppend(infoRequestOverTime, now, now, before = 900000L)
- }
-
def RewardFacilityCapture(
defenderFaction: PlanetSideEmpire.Value,
attackingFaction: PlanetSideEmpire.Value,
@@ -70,11 +68,12 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci
): Unit = {
val curr = System.currentTimeMillis()
val hackStart = curr - completionTime
- val (victorFaction, opposingFaction, flagCarrier) = if (!isResecured) {
- val carrier = building.GetFlagSocket.flatMap(_.previousFlag).flatMap(_.Carrier)
- (attackingFaction, defenderFaction, carrier)
+ val socketOpt = building.GetFlagSocket
+ val (victorFaction, opposingFaction, hasFlag, flagCarrier) = if (!isResecured) {
+ val carrier = socketOpt.flatMap(_.previousFlag).flatMap(_.Carrier)
+ (attackingFaction, defenderFaction, socketOpt.nonEmpty, carrier)
} else {
- (defenderFaction, attackingFaction, None)
+ (defenderFaction, attackingFaction, socketOpt.nonEmpty, None)
}
val (contributionVictor, contributionOpposing, _) = {
val (a, b1) = playerContribution.partition { case (_, (p, _, _)) => p.Faction == victorFaction }
@@ -83,17 +82,6 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci
}
val contributionVictorSize = contributionVictor.size
if (contributionVictorSize > 0) {
- val contributionOpposingSize = contributionOpposing.size
- //1) experience from killing opposingFaction across duration of hack
- val baseExperienceFromFacilityCapture: Long = FacilityHackParticipation.calculateExperienceFromKills(
- building.Position,
- building.Definition.SOIRadius - 50f,
- hackStart,
- completionTime,
- opposingFaction,
- contributionVictor,
- contributionOpposingSize
- )
//setup for ...
val populationIndices = playerPopulationOverTime.indices
val allFactions = PlanetSideEmpire.values.filterNot { _ == PlanetSideEmpire.NEUTRAL }.toSeq
@@ -103,32 +91,43 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci
}.toMap[PlanetSideEmpire.Value, Seq[Int]]
(individualPopulationByLayer(victorFaction), individualPopulationByLayer(opposingFaction))
}
+ val contributionOpposingSize = contributionOpposing.size
+ val killsByPlayersNotInTower = eliminateClosestTowerFromParticipating(
+ building,
+ FacilityHackParticipation.allocateKillsByPlayers(
+ building.Position,
+ building.Definition.SOIRadius.toFloat,
+ hackStart,
+ completionTime,
+ opposingFaction,
+ contributionOpposing
+ )
+ )
+ //1) experience from killing opposingFaction across duration of hack
+ //The kills that occurred in the facility's attached field tower's sphere of influence have been eliminated from consideration.
+ val baseExperienceFromFacilityCapture: Long = FacilityHackParticipation.calculateExperienceFromKills(
+ killsByPlayersNotInTower,
+ contributionOpposingSize
+ )
//2) peak population modifier
+ //Large facility battles should be well-rewarded.
val populationModifier = FacilityHackParticipation.populationProgressModifier(
opposingPopulationByLayer,
{ pop =>
- if (pop > 59) 0.5f
- else if (pop > 29) 0.4f
- else if (pop > 25) 0.3f
- else 0.25f
+ if (pop > 75) 0.9f
+ else if (pop > 59) 0.6f
+ else if (pop > 29) 0.55f
+ else if (pop > 25) 0.5f
+ else 0.45f
},
- 3
+ 4
)
- //3) competition bonus
- val competitionBonus: Long = FacilityHackParticipation.competitionBonus(
- contributionVictorSize,
- contributionOpposingSize,
- steamrollPercentage = 1.25f,
- steamrollBonus = 5L,
- overwhelmingOddsPercentage = 0.5f,
- overwhelmingOddsBonus = 100L
- )
- //4) competition multiplier
+ //3) competition multiplier
val competitionMultiplier: Float = {
val populationBalanceModifier: Float = FacilityHackParticipation.populationBalanceModifier(
victorPopulationByLayer,
opposingPopulationByLayer,
- healthyPercentage = 1.25f
+ healthyPercentage = 1.5f
)
//compensate for heat
val regionHeatMapProgression = {
@@ -140,7 +139,11 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci
((A-1, B-2, C-3), (D-1, E-2, F-3), (G-1, H-2, I-3)) ... (1->(A, D, G), 2->(B, E, H), 3->(C, F, I))
*/
val finalMap = mutable.HashMap[Vector3, Map[PlanetSideEmpire.Value, Seq[Long]]]()
- .addAll(hotSpotLayersOverTime.head.map { entry => (entry.DisplayLocation, Map.empty) })
+ .addAll(
+ hotSpotLayersOverTime.take(1).flatMap { entry =>
+ entry.map { f => (f.DisplayLocation, Map.empty[PlanetSideEmpire.Value, Seq[Long]]) }
+ }
+ )
//note: this pre-seeding of keys allows us to skip a getOrElse call in the foldLeft
hotSpotLayersOverTime.foldLeft(finalMap) { (map, list) =>
list.foreach { entry =>
@@ -153,48 +156,131 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci
}.toMap
finalMap //explicit for no good reason
}
- val heatVictorMap = FacilityHackParticipation.diffHeatForFactionMap(regionHeatMapProgression, victorFaction).values
- val heatAgainstMap = FacilityHackParticipation.diffHeatForFactionMap(regionHeatMapProgression, opposingFaction).values
- val heatMapModifier = FacilityHackParticipation.heatMapComparison(heatVictorMap, heatAgainstMap)
+ val heatMapModifier = FacilityHackParticipation.heatMapComparison(
+ FacilityHackParticipation.diffHeatForFactionMap(regionHeatMapProgression, victorFaction).values,
+ FacilityHackParticipation.diffHeatForFactionMap(regionHeatMapProgression, opposingFaction).values
+ )
heatMapModifier * populationBalanceModifier
}
- //5) hack time modifier
- val overallTimeMultiplier: Float = if (isResecured) {
- math.max(0.5f + (hackTime - completionTime) / hackTime, 1f)
- } else {
- 1f
+ //4) hack time modifier
+ //Captured major facilities without a lattice link unit and resecured major facilities with a lattice link unit
+ // incur the full hack time if the module is not transported to a friendly facility
+ //Captured major facilities with a lattice link unit and resecure major facilities without a lattice link uit
+ // will incur an abbreviated duration
+ val overallTimeMultiplier: Float = {
+ if (
+ building.Faction == PlanetSideEmpire.NEUTRAL ||
+ building.NtuLevel == 0 ||
+ building.Generator.map { _.Condition }.contains(PlanetSideGeneratorState.Destroyed)
+ ) { //the facility ran out of nanites or power during the hack or became neutral
+ 0f
+ } else if (hasFlag) {
+ if (completionTime >= hackTime) { //hack timed out without llu delivery
+ 0.25f
+ } else if (isResecured) {
+ 0.5f + (if (hackTime <= completionTime * 0.3f) {
+ completionTime.toFloat / hackTime.toFloat
+ } else if (hackTime >= completionTime * 0.6f) {
+ (hackTime - completionTime).toFloat / hackTime.toFloat
+ } else {
+ 0f
+ })
+ } else {
+ 0.5f + (hackTime - completionTime).toFloat / (2f * hackTime)
+ }
+ } else {
+ if (isResecured) {
+ 0.5f + (hackTime - completionTime).toFloat / (2f * hackTime)
+ } else {
+ 0.5f
+ }
+ }
}
- //calculate overall command experience points and the individual player multiplier
+ //5. individual contribution factors - by time
+ val contributionPerPlayerByTime = playerContribution.collect {
+ case (a, (_, d, t)) if d >= 600000 && math.abs(completionTime - t) < 5000 =>
+ (a, 0.45f)
+ case (a, (_, d, _)) if d >= 600000 =>
+ (a, 0.25f)
+ case (a, (_, d, t)) if math.abs(completionTime - t) < 5000 =>
+ (a, 0.25f * (0.5f + (d.toFloat / 600000f)))
+ case (a, (_, _, _)) =>
+ (a, 0.15f)
+ }
+ //6. competition bonus
+ //This value will probably suck, and that's fine.
+ val competitionBonus: Long = FacilityHackParticipation.competitionBonus(
+ contributionVictorSize,
+ contributionOpposingSize,
+ steamrollPercentage = 1.25f,
+ steamrollBonus = 5L,
+ overwhelmingOddsPercentage = 0.5f,
+ overwhelmingOddsBonus = 15L
+ )
+ //7. calculate overall command experience points
val finalCep: Long = math.ceil(
- math.max(1L, baseExperienceFromFacilityCapture + competitionBonus) *
+ math.max(0L, baseExperienceFromFacilityCapture) *
populationModifier *
competitionMultiplier *
overallTimeMultiplier *
- Config.app.game.cepRate
+ Config.app.game.experience.cep.rate + competitionBonus
).toLong
- val contributionPerPlayerByTime = playerContribution.collect {
- case (a, (_, d, t)) if d >= 600000 && math.abs(completionTime - t) < 5000 =>
- (a, 1f)
- case (a, (_, d, t)) if math.abs(completionTime - t) < 5000 =>
- (a, d.toFloat / 6000000f)
- case (a, (_, d, t)) =>
- (a, math.max(0, d - completionTime + t).toFloat / 6000000f)
- }
- //reward participant(s)
- // classically, only players in the SOI are rewarded
+ //8. reward participants
+ //Classically, only players in the SOI are rewarded, and the llu runner too
+ val hackerId = hacker.CharId
val events = building.Zone.AvatarEvents
- building.PlayersInSOI
- .filter { player =>
- player.Faction == victorFaction && player.CharId != hacker.CharId && !flagCarrier.contains(player)
- }
+ val playersInSoi = building.PlayersInSOI
+ if (playersInSoi.exists(_.CharId == hackerId) && flagCarrier.map(_.CharId).getOrElse(0L) != hackerId) {
+ events ! AvatarServiceMessage(hacker.Name, AvatarAction.AwardCep(hackerId, finalCep))
+ }
+ playersInSoi
+ .filter { player => player.Faction == victorFaction && player.CharId != hackerId }
.foreach { player =>
- val contributionMultiplier = contributionPerPlayerByTime.getOrElse(player.CharId, 1f)
- events ! AvatarServiceMessage(player.Name, AvatarAction.AwardCep(0, (finalCep * contributionMultiplier).toLong))
+ val charId = player.CharId
+ val contributionMultiplier = contributionPerPlayerByTime.getOrElse(charId, 1f)
+ val outputValue = (finalCep * contributionMultiplier).toLong
+ events ! AvatarServiceMessage(player.Name, AvatarAction.AwardCep(0, outputValue))
}
- events ! AvatarServiceMessage(hacker.Name, AvatarAction.AwardCep(hacker.CharId, finalCep))
flagCarrier.collect {
- player => events ! AvatarServiceMessage(player.Name, AvatarAction.AwardCep(player.CharId, (finalCep * 0.5f).toLong))
+ case player if !isResecured =>
+ val charId: Long = player.CharId
+ val finalModifiedCep: Long = math.min(
+ (hackTime - completionTime) / 1000L,
+ (finalCep * Config.app.game.experience.cep.lluCarrierModifier).toLong
+ )
+ ToDatabase.reportFacilityCapture(
+ charId,
+ building.Zone.Number,
+ building.GUID.guid,
+ finalModifiedCep,
+ expType="llu"
+ )
+ events ! AvatarServiceMessage(player.Name, AvatarAction.AwardCep(charId, finalModifiedCep))
}
}
}
+
+ private def eliminateClosestTowerFromParticipating(
+ building: Building,
+ list: Iterable[(UniquePlayer, Float, Seq[Kill])]
+ ): Iterable[(UniquePlayer, Float, Seq[Kill])] = {
+ val buildingPosition = building.Position.xy
+ building
+ .Zone
+ .Buildings
+ .values
+ .filter { building => building.BuildingType == StructureType.Tower }
+ .minByOption { tower => Vector3.DistanceSquared(buildingPosition, tower.Position.xy) }
+ .map { tower =>
+ val towerPosition = tower.Position.xy
+ val towerRadius = math.pow(tower.Definition.SOIRadius.toDouble * 0.7d, 2d).toFloat
+ list
+ .map { case (p, f, kills) =>
+ val filteredKills = kills.filter { kill => Vector3.DistanceSquared(kill.victim.Position.xy, towerPosition) <= towerRadius }
+ (p, f, filteredKills)
+ }
+ .filter { case (_, _, kills) => kills.nonEmpty }
+ }
+ .getOrElse(list)
+ }
}
diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/participation/TowerHackParticipation.scala b/src/main/scala/net/psforever/objects/serverobject/structures/participation/TowerHackParticipation.scala
index 410ddf4d4..2e2d459c2 100644
--- a/src/main/scala/net/psforever/objects/serverobject/structures/participation/TowerHackParticipation.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/structures/participation/TowerHackParticipation.scala
@@ -4,7 +4,7 @@ package net.psforever.objects.serverobject.structures.participation
import net.psforever.objects.serverobject.structures.Building
import net.psforever.objects.sourcing.PlayerSource
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
-import net.psforever.types.PlanetSideEmpire
+import net.psforever.types.{PlanetSideEmpire, Vector3}
import net.psforever.util.Config
final case class TowerHackParticipation(building: Building) extends FacilityHackParticipation {
@@ -25,8 +25,6 @@ final case class TowerHackParticipation(building: Building) extends FacilityHack
completionTime: Long,
isResecured: Boolean
): Unit = {
- val curr = System.currentTimeMillis()
- val hackStart = curr - completionTime
val (victorFaction, opposingFaction) = if (!isResecured) {
(attackingFaction, defenderFaction)
} else {
@@ -40,18 +38,10 @@ final case class TowerHackParticipation(building: Building) extends FacilityHack
}
val contributionVictorSize = contributionVictor.size
if (contributionVictorSize > 0) {
- val contributionOpposingSize = contributionOpposing.size
- //1) experience from killing opposingFaction across duration of hack
- val baseExperienceFromFacilityCapture: Long = FacilityHackParticipation.calculateExperienceFromKills(
- building.Position,
- building.Definition.SOIRadius.toFloat,
- hackStart,
- completionTime,
- opposingFaction,
- contributionVictor,
- contributionOpposingSize
- )
//setup for ...
+ import scala.concurrent.duration._
+ val curr = System.currentTimeMillis()
+ val contributionOpposingSize = contributionOpposing.size
val populationIndices = playerPopulationOverTime.indices
val allFactions = PlanetSideEmpire.values.filterNot {
_ == PlanetSideEmpire.NEUTRAL
@@ -62,18 +52,74 @@ final case class TowerHackParticipation(building: Building) extends FacilityHack
}.toMap[PlanetSideEmpire.Value, Seq[Int]]
(individualPopulationByLayer(victorFaction), individualPopulationByLayer(opposingFaction))
}
+ val soiPlayers = building.PlayersInSOI
+ //1) experience from killing opposingFaction
+ //Because the hack duration of towers is instantaneous, the prior period of five minutes is artificially selected.
+ val baseExperienceFromFacilityCapture: Long = FacilityHackParticipation.calculateExperienceFromKills(
+ FacilityHackParticipation.allocateKillsByPlayers(
+ building.Position,
+ building.Definition.SOIRadius.toFloat,
+ curr - 5.minutes.toMillis,
+ curr,
+ opposingFaction,
+ contributionVictor
+ ),
+ contributionOpposingSize
+ )
//2) peak population modifier
+ //Towers should not be regarded as major battles.
+ //As the population rises, the rewards decrease (dramatically).
val populationModifier = FacilityHackParticipation.populationProgressModifier(
- opposingPopulationByLayer,
+ victorPopulationByLayer,
{ pop =>
- if (pop > 59) 0.75f
- else if (pop > 29) 0.675f
- else if (pop > 25) 0.55f
- else 0.5f
+ if (pop > 80) 0f
+ else if (pop > 39) (80 - pop).toFloat * 0.01f
+ else if (pop > 25) 0.5f
+ else if (pop > 19) 0.55f
+ else if (pop > 9) 0.6f
+ else if (pop > 5) 0.75f
+ else 1f
},
2
)
- //3) competition bonus
+ //3) competition multiplier
+ val competitionMultiplier: Float = FacilityHackParticipation.populationBalanceModifier(
+ victorPopulationByLayer,
+ opposingPopulationByLayer,
+ healthyPercentage = 1.25f
+ )
+ //4a. individual contribution factors - by time
+ //Once again, an arbitrary five minute period.
+ val contributionPerPlayerByTime = playerContribution.collect {
+ case (a, (_, d, t)) if d >= 300000 && math.abs(completionTime - t) < 5000 =>
+ (a, 0.45f)
+ case (a, (_, d, _)) if d >= 300000 =>
+ (a, 0.25f)
+ case (a, (_, d, t)) if math.abs(completionTime - t) < 5000 =>
+ (a, 0.25f * (0.5f + (d.toFloat / 300000f)))
+ case (a, (_, _, _)) =>
+ (a, 0.15f)
+ }
+ //4b. individual contribution factors - by distance to goal (secondary_capture)
+ //Because the hack duration of towers is instantaneous, distance from terminal is a more important factor
+ val contributionPerPlayerByDistanceFromGoal = {
+ var minDistance: Float = Float.PositiveInfinity
+ val location = building
+ .CaptureTerminal
+ .map { terminal => terminal.Position }
+ .getOrElse { hacker.Position }
+ soiPlayers
+ .map { p =>
+ val distance = Vector3.Distance(p.Position, location)
+ minDistance = math.min(minDistance, distance)
+ (p.CharId, distance)
+ }
+ .map { case (id, distance) =>
+ (id, math.max(0.15f, minDistance / distance))
+ }
+ }.toMap[Long, Float]
+ //5) token competition bonus
+ //This value will probably suck, and that's fine.
val competitionBonus: Long = FacilityHackParticipation.competitionBonus(
contributionVictorSize,
contributionOpposingSize,
@@ -82,37 +128,29 @@ final case class TowerHackParticipation(building: Building) extends FacilityHack
overwhelmingOddsPercentage = 0.5f,
overwhelmingOddsBonus = 30L
)
- //4) competition multiplier
- val competitionMultiplier: Float = FacilityHackParticipation.populationBalanceModifier(
- victorPopulationByLayer,
- opposingPopulationByLayer,
- healthyPercentage = 1.25f
- )
- //calculate overall command experience points and the individual player multiplier
+ //6. calculate overall command experience points
val finalCep: Long = math.ceil(
- math.max(1L, baseExperienceFromFacilityCapture + competitionBonus) *
+ baseExperienceFromFacilityCapture *
populationModifier *
competitionMultiplier *
- Config.app.game.cepRate
+ Config.app.game.experience.cep.rate + competitionBonus
).toLong
- val contributionPerPlayerByTime = playerContribution.collect {
- case (a, (_, d, t)) if d >= 300000 && math.abs(completionTime - t) < 5000 =>
- (a, 0.5f)
- case (a, (_, d, t)) if math.abs(completionTime - t) < 5000 =>
- (a, 0.25f * (1f + (d.toFloat / 3000000f)))
- case (a, (_, d, t)) =>
- (a, 0.25f * (1f + (math.max(0, d - completionTime + t).toFloat / 3000000f)))
- }
- //reward participant(s)
- // classically, only players in the SOI are rewarded
+ //7. reward participants
+ //Classically, only players in the SOI are rewarded
val events = building.Zone.AvatarEvents
- building.PlayersInSOI
+ soiPlayers
.filter { player =>
player.Faction == victorFaction && player.CharId != hacker.CharId
}
.foreach { player =>
- val contributionMultiplier = contributionPerPlayerByTime.getOrElse(player.CharId, 1f)
- events ! AvatarServiceMessage(player.Name, AvatarAction.AwardCep(0, (finalCep * contributionMultiplier).toLong))
+ val charId = player.CharId
+ val contributionTimeMultiplier = contributionPerPlayerByTime.getOrElse(charId, 0.5f)
+ val contributionDistanceMultiplier = contributionPerPlayerByDistanceFromGoal.getOrElse(charId, 0.5f)
+ val outputValue = (finalCep * contributionTimeMultiplier * contributionDistanceMultiplier).toLong
+ events ! AvatarServiceMessage(
+ player.Name,
+ AvatarAction.AwardCep(0, outputValue)
+ )
}
events ! AvatarServiceMessage(hacker.Name, AvatarAction.AwardCep(hacker.CharId, finalCep))
}
diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala
index c3ab7e93f..ff61eee36 100644
--- a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala
@@ -101,7 +101,7 @@ class FacilityTurretControl(turret: FacilityTurret)
turret.ControlledWeapon(wepNumber = 1).foreach {
case weapon: Tool =>
// recharge when last shot fired 3s delay, +1, 200ms interval
- if (weapon.Magazine < weapon.MaxMagazine && System.nanoTime() - weapon.LastDischarge > 3000000000L) {
+ if (weapon.Magazine < weapon.MaxMagazine && System.currentTimeMillis() - weapon.LastDischarge > 3000L) {
weapon.Magazine += 1
val seat = turret.Seat(0).get
seat.occupant match {
diff --git a/src/main/scala/net/psforever/objects/vehicles/AntTransferBehavior.scala b/src/main/scala/net/psforever/objects/vehicles/AntTransferBehavior.scala
index b6e40a1b5..97d6b8576 100644
--- a/src/main/scala/net/psforever/objects/vehicles/AntTransferBehavior.scala
+++ b/src/main/scala/net/psforever/objects/vehicles/AntTransferBehavior.scala
@@ -187,8 +187,10 @@ trait AntTransferBehavior extends TransferBehavior with NtuStorageBehavior {
val chargeToDeposit = if (min == 0) {
transferTarget match {
case Some(silo: ResourceSilo) =>
- // Silos would charge from 0-100% in roughly 105s on live (~20%-100% https://youtu.be/veOWToR2nSk?t=1402)
- scala.math.min(scala.math.min(silo.MaxNtuCapacitor / 105, chargeable.NtuCapacitor), max)
+ scala.math.min(
+ scala.math.min(silo.MaxNtuCapacitor / silo.Definition.ChargeTime.toMillis.toFloat, chargeable.NtuCapacitor),
+ max
+ )
case _ =>
0
}
diff --git a/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala b/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala
index 52e8146e8..18a3d9797 100644
--- a/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala
+++ b/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala
@@ -893,7 +893,7 @@ object VehicleControl {
/**
* Determine if a given activity entry would invalidate the act of charging vehicle shields this tick.
- * @param now the current time (in nanoseconds)
+ * @param now the current time (in milliseconds)
* @param act a `VitalsActivity` entry to test
* @return `true`, if the shield charge would be blocked;
* `false`, otherwise
diff --git a/src/main/scala/net/psforever/objects/vital/InGameHistory.scala b/src/main/scala/net/psforever/objects/vital/InGameHistory.scala
index cd5177b2b..eebc44eca 100644
--- a/src/main/scala/net/psforever/objects/vital/InGameHistory.scala
+++ b/src/main/scala/net/psforever/objects/vital/InGameHistory.scala
@@ -10,6 +10,7 @@ import net.psforever.objects.vital.etc.{ExplodingEntityReason, PainboxReason, Su
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
import net.psforever.objects.vital.projectile.ProjectileReason
import net.psforever.types.{ExoSuitType, ImplantType, TransactionType}
+import net.psforever.util.Config
import scala.collection.mutable
@@ -327,7 +328,7 @@ trait InGameHistory {
}
def GetContribution(): Option[List[InGameActivity]] = {
- Option(GetContributionDuringPeriod(History, duration = 600000L))
+ Option(GetContributionDuringPeriod(History, duration = Config.app.game.experience.longContributionTime))
}
def GetContributionDuringPeriod(list: List[InGameActivity], duration: Long): List[InGameActivity] = {
diff --git a/src/main/scala/net/psforever/objects/zones/exp/KillAssists.scala b/src/main/scala/net/psforever/objects/zones/exp/KillAssists.scala
index ada240c1b..f966318ba 100644
--- a/src/main/scala/net/psforever/objects/zones/exp/KillAssists.scala
+++ b/src/main/scala/net/psforever/objects/zones/exp/KillAssists.scala
@@ -78,23 +78,23 @@ object KillAssists {
lastDamage: Option[DamageResult],
history: Iterable[InGameActivity],
): Seq[(PlayerSource, KDAStat)] = {
- val shortHistory = limitHistoryToThisLife(history.toList)
- determineKiller(lastDamage, shortHistory) match {
+ val truncatedHistory = limitHistoryToThisLife(history.toList)
+ determineKiller(lastDamage, truncatedHistory) match {
case Some((result, killer: PlayerSource)) =>
- val assists = collectKillAssistsForPlayer(victim, shortHistory, Some(killer))
- val fullBep = calculateExperience(killer, victim, shortHistory)
+ val assists = collectKillAssistsForPlayer(victim, truncatedHistory, Some(killer))
+ val fullBep = calculateExperience(killer, victim, truncatedHistory)
val hitSquad = (killer, Kill(victim, result, fullBep)) +: assists.map {
case ContributionStatsOutput(p, w, r) => (p, Assist(victim, w, r, (fullBep * r).toLong))
}.toSeq
- (victim, Death(hitSquad.map { _._1 }, shortHistory.last.time - shortHistory.head.time, fullBep)) +: hitSquad
+ (victim, Death(hitSquad.map { _._1 }, truncatedHistory.last.time - truncatedHistory.head.time, fullBep)) +: hitSquad
case _ =>
- val assists = collectKillAssistsForPlayer(victim, shortHistory, None)
- val fullBep = Support.baseExperience(victim, shortHistory)
+ val assists = collectKillAssistsForPlayer(victim, truncatedHistory, None)
+ val fullBep = Support.baseExperience(victim, truncatedHistory)
val hitSquad = assists.map {
case ContributionStatsOutput(p, w, r) => (p, Assist(victim, w, r, (fullBep * r).toLong))
}.toSeq
- (victim, Death(hitSquad.map { _._1 }, shortHistory.last.time - shortHistory.head.time, fullBep)) +: hitSquad
+ (victim, Death(hitSquad.map { _._1 }, truncatedHistory.last.time - truncatedHistory.head.time, fullBep)) +: hitSquad
}
}
diff --git a/src/main/scala/net/psforever/objects/zones/exp/KillContributions.scala b/src/main/scala/net/psforever/objects/zones/exp/KillContributions.scala
index c315429f6..93293aaa3 100644
--- a/src/main/scala/net/psforever/objects/zones/exp/KillContributions.scala
+++ b/src/main/scala/net/psforever/objects/zones/exp/KillContributions.scala
@@ -10,6 +10,7 @@ import net.psforever.objects.vital.projectile.ProjectileReason
import net.psforever.objects.zones.exp.rec.{CombinedHealthAndArmorContributionProcess, MachineRecoveryExperienceContributionProcess}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.{PlanetSideEmpire, Vector3}
+import net.psforever.util.Config
import scala.collection.mutable
@@ -49,7 +50,7 @@ object KillContributions {
multivehicle_rearm_terminal,
lodestar_repair_terminal
).collect { _.ObjectId }
- } //TODO currently includes things that are not typical items but are used for expressing contribution implements
+ } //TODO currently includes things that are not typical equipment but things that express contribution
/** cached for empty collection returns; please do not add anything to it */
private val emptyMap: mutable.LongMap[ContributionStats] = mutable.LongMap.empty[ContributionStats]
@@ -119,21 +120,20 @@ object KillContributions {
): Iterable[(Long, ContributionStatsOutput)] = {
val faction = target.Faction
/*
- divide into applicable time periods - long for 10 minutes and short 5 minutes;
+ divide into applicable time periods;
these two periods represent passes over the in-game history to evaluate statistic modification events;
the short time period should stand on its own, but should also be represented in the long time period;
more players should be rewarded if one qualifies for the longer time period's evaluation
*/
- //divide by applicable time periods (long=10minutes, short=5minutes)
- val (contributions, (shortHistory, longHistory)) = {
+ val (contributions, (longHistory, shortHistory)) = {
val killTime = kill.time.toDate.getTime
- val shortPeriod = killTime - 300000L
+ val shortPeriod = killTime - Config.app.game.experience.shortContributionTime
val (contrib, onlyHistory) = history.partition { _.isInstanceOf[Contribution] }
(
contrib
.collect { case Contribution(unique, entries) => (unique, entries) }
.toMap[SourceUniqueness, List[InGameActivity]],
- limitHistoryToThisLife(onlyHistory.toList, killTime).partition { _.time > shortPeriod }
+ limitHistoryToThisLife(onlyHistory.toList, killTime).partition { _.time < shortPeriod }
)
}
//events that are older than 5 minutes are enough to prove one has been alive that long
@@ -195,7 +195,7 @@ object KillContributions {
* @return the potentially truncated history
*/
private def limitHistoryToThisLife(history: List[InGameActivity], eventTime: Long): List[InGameActivity] = {
- limitHistoryToThisLife(history, eventTime, eventTime - 600000L)
+ limitHistoryToThisLife(history, eventTime, eventTime - Config.app.game.experience.longContributionTime)
}
/**
@@ -262,9 +262,9 @@ object KillContributions {
excludedTargets: mutable.ListBuffer[SourceUniqueness]
): mutable.LongMap[ContributionStats] = {
contributeWithRevivalActivity(history, existingParticipants)
- contributeWithTerminalActivity(faction, history, contributions, excludedTargets, existingParticipants)
- contributeWithVehicleTransportActivity(history, faction, contributions, excludedTargets, existingParticipants)
- contributeWithVehicleCargoTransportActivity(history, faction, contributions, excludedTargets, existingParticipants)
+ contributeWithTerminalActivity(history, faction, contributions, excludedTargets, existingParticipants)
+ contributeWithVehicleTransportActivity(kill, history, faction, contributions, excludedTargets, existingParticipants)
+ contributeWithVehicleCargoTransportActivity(kill, history, faction, contributions, excludedTargets, existingParticipants)
contributeWithKillWhileMountedActivity(kill, faction, contributions, excludedTargets, existingParticipants)
existingParticipants.remove(0)
existingParticipants
@@ -314,13 +314,17 @@ object KillContributions {
excludedTargets.addOne(owner)
excludedTargets.addOne(attacker.unique)
val time = kill.time.toDate.getTime
+ val weaponStat = Support.calculateSupportExperience(
+ event = "mounted-kill",
+ WeaponStats(DriverAssist(mount.Definition.ObjectId), 1, 1, time, 1f)
+ )
combineStatsInto(
out,
(
owner.charId,
ContributionStats(
PlayerSource(owner, mount.Position),
- Seq(WeaponStats(DriverAssist(mount.Definition.ObjectId), 1, 1, time, 10f)),
+ Seq(weaponStat),
1,
1,
1,
@@ -332,12 +336,12 @@ object KillContributions {
}
combineStatsInto(
out,
- extractContributionsForMachineByTarget(mount, faction, eventTime, contributions, excludedTargets)
+ extractContributionsForMachineByTarget(mount, faction, eventTime, contributions, excludedTargets, eventOutputType="support-repair")
)
case Some((mount: TurretSource, _: PlayerSource)) if !excludedTargets.contains(mount.unique) =>
combineStatsInto(
out,
- extractContributionsForMachineByTarget(mount, faction, eventTime, contributions, excludedTargets)
+ extractContributionsForMachineByTarget(mount, faction, eventTime, contributions, excludedTargets, eventOutputType="support-repair-turret")
)
}
}
@@ -345,6 +349,7 @@ object KillContributions {
/**
* Gather and reward specific in-game equipment use activity.
* na
+ * @param kill the in-game event that maintains information about the other player's death
* @param history chronology of activity the game considers noteworthy
* @param faction empire to target
* @param contributions mapping between external entities
@@ -356,6 +361,7 @@ object KillContributions {
* @see `extractContributionsForMachineByTarget`
*/
private def contributeWithVehicleTransportActivity(
+ kill: Kill,
history: List[InGameActivity],
faction: PlanetSideEmpire.Value,
contributions: Map[SourceUniqueness, List[InGameActivity]],
@@ -385,11 +391,19 @@ object KillContributions {
}
} || {
val sameZone = in.zoneNumber == out.zoneNumber
- val distanceMoved = Vector3.DistanceSquared(in.vehicle.Position.xy, out.vehicle.Position.xy)
+ val distanceTransported = Vector3.DistanceSquared(in.vehicle.Position.xy, out.vehicle.Position.xy)
+ val distanceMoved = {
+ val killLocation = kill.info.adversarial
+ .collect { adversarial => adversarial.attacker.Position.xy }
+ .getOrElse(Vector3.Zero)
+ Vector3.DistanceSquared(killLocation, out.player.Position.xy)
+ }
val timeSpent = out.time - in.time
- timeSpent >= 210000 /* 3:30 */ ||
- (sameZone && (distanceMoved > 160000f || distanceMoved > 10000f && timeSpent >= 60000)) |
- (!sameZone && (distanceMoved > 10000f || timeSpent >= 120000))
+ distanceMoved < 5625f /* 75m */ &&
+ (timeSpent >= 210000L /* 3:30 */ ||
+ (sameZone && (distanceTransported > 160000f /* 400m */ ||
+ distanceTransported > 10000f /* 100m */ && timeSpent >= 60000L /* 1:00m */)) ||
+ (!sameZone && (distanceTransported > 10000f /* 100m */ || timeSpent >= 120000L /* 2:00 */ )))
}) =>
out
}
@@ -398,18 +412,23 @@ object KillContributions {
.groupBy { _.vehicle }
.collect { case (mount, dismountsFromVehicle) if mount.owner.nonEmpty =>
val promotedOwner = PlayerSource(mount.owner.get, mount.Position)
- val equipmentUseContext = mount.Definition match {
+ val (equipmentUseContext, equipmentUseEvent) = mount.Definition match {
case v @ GlobalDefinitions.router =>
- RouterKillAssist(v.ObjectId)
+ (RouterKillAssist(v.ObjectId), "router")
case v =>
- HotDropKillAssist(v.ObjectId, 0)
+ (HotDropKillAssist(v.ObjectId, 0), "hotdrop")
}
- val statContext = Seq(WeaponStats(equipmentUseContext, 0, dismountsFromVehicle.size, dismountsFromVehicle.maxBy(_.time).time, 15f))
+ val size = dismountsFromVehicle.size
+ val time = dismountsFromVehicle.maxBy(_.time).time
+ val weaponStat = Support.calculateSupportExperience(
+ equipmentUseEvent,
+ WeaponStats(equipmentUseContext, size, size, time, 1f)
+ )
combineStatsInto(
out,
(
promotedOwner.CharId,
- ContributionStats(promotedOwner, statContext, 1, 1, 1, statContext.head.time)
+ ContributionStats(promotedOwner, Seq(weaponStat), size, size, size, time)
)
)
contributions.get(mount.unique).collect {
@@ -417,13 +436,13 @@ object KillContributions {
val mountHistory = dismountsFromVehicle
.flatMap { event =>
val eventTime = event.time
- val startTime = event.pairedEvent.get.time - 600000L
+ val startTime = event.pairedEvent.get.time - Config.app.game.experience.longContributionTime
limitHistoryToThisLife(list, eventTime, startTime)
}
.distinctBy(_.time)
combineStatsInto(
out,
- extractContributionsForMachineByTarget(mount, faction, mountHistory, contributions, excludedTargets)
+ extractContributionsForMachineByTarget(mount, faction, mountHistory, contributions, excludedTargets, eventOutputType="support-repair")
)
}
}
@@ -432,6 +451,7 @@ object KillContributions {
/**
* Gather and reward specific in-game equipment use activity.
* na
+ * @param kill the in-game event that maintains information about the other player's death
* @param faction empire to target
* @param contributions mapping between external entities
* the target has interacted with in the form of in-game activity
@@ -442,6 +462,7 @@ object KillContributions {
* @see `extractContributionsForMachineByTarget`
*/
private def contributeWithVehicleCargoTransportActivity(
+ kill: Kill,
history: List[InGameActivity],
faction: PlanetSideEmpire.Value,
contributions: Map[SourceUniqueness, List[InGameActivity]],
@@ -463,9 +484,16 @@ object KillContributions {
if in.vehicle.unique == out.vehicle.unique &&
out.vehicle.Faction == out.cargo.Faction &&
(in.vehicle.Definition == GlobalDefinitions.router || {
- val distanceMoved = Vector3.DistanceSquared(in.vehicle.Position.xy, out.vehicle.Position.xy)
+ val distanceTransported = Vector3.DistanceSquared(in.vehicle.Position.xy, out.vehicle.Position.xy)
+ val distanceMoved = {
+ val killLocation = kill.info.adversarial
+ .collect { adversarial => adversarial.attacker.Position.xy }
+ .getOrElse(Vector3.Zero)
+ Vector3.DistanceSquared(killLocation, out.cargo.Position.xy)
+ }
val timeSpent = out.time - in.time
- timeSpent >= 210000 /* 3:30 */ || distanceMoved > 640000f
+ distanceMoved < 5625f /* 75m */ &&
+ (timeSpent >= 210000 /* 3:30 */ || distanceTransported > 360000f /* 600m */)
}) =>
out
}
@@ -478,7 +506,13 @@ object KillContributions {
dismountsFromVehicle
.groupBy(_.vehicle)
.map { case (vehicle, events) =>
- (vehicle, vehicle.owner, Seq(WeaponStats(HotDropKillAssist(vehicle.Definition.ObjectId, mountId), 0, events.size, events.maxBy(_.time).time, 15f)))
+ val size = events.size
+ val time = events.maxBy(_.time).time
+ val weaponStat = Support.calculateSupportExperience(
+ event = "hotdrop",
+ WeaponStats(HotDropKillAssist(vehicle.Definition.ObjectId, mountId), size, size, time, 1f)
+ )
+ (vehicle, vehicle.owner, Seq(weaponStat))
}
.collect { case (vehicle, Some(owner), statContext) =>
combineStatsInto(
@@ -493,13 +527,13 @@ object KillContributions {
val mountHistory = dismountsFromVehicle
.flatMap { event =>
val eventTime = event.time
- val startTime = event.pairedEvent.get.time - 600000L
+ val startTime = event.pairedEvent.get.time - Config.app.game.experience.longContributionTime
limitHistoryToThisLife(list, eventTime, startTime)
}
.distinctBy(_.time)
combineStatsInto(
out,
- extractContributionsForMachineByTarget(mount, faction, mountHistory, contributions, excludedTargets)
+ extractContributionsForMachineByTarget(mount, faction, mountHistory, contributions, excludedTargets, eventOutputType="support-repair")
)
}
contributions.get(vehicle.unique).collect {
@@ -507,13 +541,13 @@ object KillContributions {
val carrierHistory = dismountsFromVehicle
.flatMap { event =>
val eventTime = event.time
- val startTime = event.pairedEvent.get.time - 600000L
+ val startTime = event.pairedEvent.get.time - Config.app.game.experience.longContributionTime
limitHistoryToThisLife(list, eventTime, startTime)
}
.distinctBy(_.time)
combineStatsInto(
out,
- extractContributionsForMachineByTarget(vehicle, faction, carrierHistory, contributions, excludedTargets)
+ extractContributionsForMachineByTarget(vehicle, faction, carrierHistory, contributions, excludedTargets, eventOutputType="support-repair")
)
}
}
@@ -542,104 +576,120 @@ object KillContributions {
* @see `TerminalUsedActivity`
*/
private def contributeWithTerminalActivity(
- faction: PlanetSideEmpire.Value,
history: List[InGameActivity],
+ faction: PlanetSideEmpire.Value,
contributions: Map[SourceUniqueness, List[InGameActivity]],
excludedTargets: mutable.ListBuffer[SourceUniqueness],
out: mutable.LongMap[ContributionStats]
): Unit = {
- val data = history
+ history
.collect {
- case h: HealFromTerminal => (h.term, (h, h.term.hacked))
- case r: RepairFromTerminal => (r.term, (r, r.term.hacked))
- case t: TerminalUsedActivity => (t.terminal, (t, t.terminal.hacked))
+ case h: HealFromTerminal => (h.term, h)
+ case r: RepairFromTerminal => (r.term, r)
+ case t: TerminalUsedActivity => (t.terminal, t)
}
.groupBy(_._1.unique)
- .map { case (_, list) => (list.head._1, list.map { _._2 }) }
- data.flatMap {
- case (terminal, events) =>
- val (activity, hackState) = events.unzip
- val terminalFaction = terminal.Faction
- if (terminalFaction != faction && hackState.exists { _.nonEmpty }) {
- /*
- if the terminal has been hacked,
- and the original terminal does not align with our own faction,
- then the support must be reported as a hack;
- if we are the same faction as the terminal, then the hacked condition is irrelevant
- */
- val hackContext = HackKillAssist(GlobalDefinitions.remote_electronics_kit.ObjectId, terminal.Definition.ObjectId)
- hackState
- .groupBy(_.get.player)
- .collect {
- case (player, _) if player.Faction == faction => //only reward allied hacking
- val time = activity.maxBy(_.time).time
+ .map {
+ case (_, events1) =>
+ val (termThings1, _) = events1.unzip
+ val hackContext = HackKillAssist(GlobalDefinitions.remote_electronics_kit.ObjectId, termThings1.head.Definition.ObjectId)
+ if (termThings1.exists(t => t.Faction != faction && t.hacked.nonEmpty)) {
+ /*
+ if the terminal has been hacked,
+ and the original terminal does not align with our own faction,
+ then the support must be reported as a hack;
+ if we are the same faction as the terminal, then the hacked condition is irrelevant
+ */
+ events1
+ .collect { case out @ (t, _) if t.hacked.nonEmpty => out }
+ .groupBy { case (t, _) => t.hacked.get.player.unique }
+ .foreach { case (_, events2) =>
+ val (termThings2, events3) = events2.unzip
+ val hacker = termThings2.head.hacked.get.player
+ val size = events3.size
+ val time = events3.maxBy(_.time).time
+ val weaponStats = Support.calculateSupportExperience(
+ event = "hack",
+ WeaponStats(hackContext, size, size, time, 1f)
+ )
combineStatsInto(
out,
(
- player.CharId,
+ hacker.CharId,
ContributionStats(
- player,
- Seq(WeaponStats(hackContext, 0, 1, time, 10f)),
- 0,
- 0,
- 1,
+ hacker,
+ Seq(weaponStats),
+ size,
+ size,
+ size,
time
)
)
)
- }
- activity
- } else if (terminalFaction == faction) {
- val eventTime = activity.maxBy(_.time).time
- val startTime = activity.minBy(_.time).time - 600000L
- val (equipmentUseContext, ownerOpt) = terminal.installation match {
- case v: VehicleSource =>
- terminal.Definition match {
- case GlobalDefinitions.order_terminala =>
- combineStatsInto(out, extractContributionsForMachineByTarget(v, faction, eventTime, startTime, contributions, excludedTargets))
- (AmsResupplyKillAssist(terminal.Definition.ObjectId), v.owner)
- case GlobalDefinitions.order_terminalb =>
- combineStatsInto(out, extractContributionsForMachineByTarget(v, faction, eventTime, startTime, contributions, excludedTargets))
- (AmsResupplyKillAssist(terminal.Definition.ObjectId), v.owner)
- case GlobalDefinitions.lodestar_repair_terminal =>
- combineStatsInto(out, extractContributionsForMachineByTarget(v, faction, eventTime, startTime, contributions, excludedTargets))
- (RepairKillAssist(terminal.Definition.ObjectId, v.Definition.ObjectId), v.owner)
- case GlobalDefinitions.bfr_rearm_terminal =>
- combineStatsInto(out, extractContributionsForMachineByTarget(v, faction, eventTime, startTime, contributions, excludedTargets))
- (LodestarRearmKillAssist(terminal.Definition.ObjectId), v.owner)
- case GlobalDefinitions.multivehicle_rearm_terminal =>
- combineStatsInto(out, extractContributionsForMachineByTarget(v, faction, eventTime, startTime, contributions, excludedTargets))
- (LodestarRearmKillAssist(terminal.Definition.ObjectId), v.owner)
- case _ =>
- (NoUse(), None)
}
- case _: BuildingSource =>
- combineStatsInto(out, extractContributionsForMachineByTarget(terminal, faction, eventTime, startTime, contributions, excludedTargets))
- (NoUse(), None) //general terminal use
- case _ =>
- (NoUse(), None)
- }
- ownerOpt.collect { owner =>
- val time = activity.maxBy(_.time).time
- combineStatsInto(
- out,
- (
- owner.charId,
- ContributionStats(
- PlayerSource(owner, terminal.installation.Position),
- Seq(WeaponStats(equipmentUseContext, 0, 1, time, 10f)),
- 0,
- 0,
- 1,
- time
+ } else if (termThings1.exists(_.Faction == faction)) {
+ //faction-aligned terminal
+ val (_, events2) = events1.unzip
+ val eventTime = events2.maxBy(_.time).time
+ val startTime = events2.minBy(_.time).time - Config.app.game.experience.longContributionTime
+ val termThingsHead = termThings1.head
+ val (equipmentUseContext, equipmentUseEvent, installationEvent, target) = termThingsHead.installation match {
+ case v: VehicleSource =>
+ termThingsHead.Definition match {
+ case GlobalDefinitions.order_terminala =>
+ (AmsResupplyKillAssist(GlobalDefinitions.order_terminala.ObjectId), "ams-resupply", "support-repair", Some(v))
+ case GlobalDefinitions.order_terminalb =>
+ (AmsResupplyKillAssist(GlobalDefinitions.order_terminalb.ObjectId), "ams-resupply", "support-repair", Some(v))
+ case GlobalDefinitions.lodestar_repair_terminal =>
+ (RepairKillAssist(GlobalDefinitions.lodestar_repair_terminal.ObjectId, v.Definition.ObjectId), "lodestar-repair", "support-repair", Some(v))
+ case GlobalDefinitions.bfr_rearm_terminal =>
+ (LodestarRearmKillAssist(GlobalDefinitions.bfr_rearm_terminal.ObjectId), "lodestar-rearm", "support-repair", Some(v))
+ case GlobalDefinitions.multivehicle_rearm_terminal =>
+ (LodestarRearmKillAssist(GlobalDefinitions.multivehicle_rearm_terminal.ObjectId), "lodestar-rearm", "support-repair", Some(v))
+ case _ =>
+ (NoUse(), "", "", None)
+ }
+ case _: BuildingSource =>
+ (NoUse(), "", "support-repair-terminal", Some(termThingsHead))
+ case _ =>
+ (NoUse(), "", "", None)
+ }
+ target.map { src =>
+ combineStatsInto(
+ out,
+ extractContributionsForMachineByTarget(src, faction, eventTime, startTime, contributions, excludedTargets, installationEvent)
+ )
+ }
+ events1
+ .map { case (a, b) => (a.installation, b) }
+ .collect { case (installation: VehicleSource, evt) if installation.owner.nonEmpty => (installation, evt) }
+ .groupBy(_._1.owner.get)
+ .collect { case (owner, list) =>
+ val (installations, events2) = list.unzip
+ val size = events2.size
+ val time = events2.maxBy(_.time).time
+ val weaponStats = Support.calculateSupportExperience(
+ equipmentUseEvent,
+ WeaponStats(equipmentUseContext, size, size, time, 1f)
)
- ))
+ combineStatsInto(
+ out,
+ (
+ owner.charId,
+ ContributionStats(
+ PlayerSource(owner, installations.head.Position),
+ Seq(weaponStats),
+ size,
+ size,
+ size,
+ time
+ )
+ )
+ )
+ }
}
- activity
- } else {
- Nil
- }
- }
+ None
+ }
}
/**
@@ -671,7 +721,12 @@ object KillContributions {
id,
ContributionStats(
user,
- Seq(WeaponStats(ReviveKillAssist(objectId), 100, 1, time, math.log(eventSize + 1).toFloat * 15f)),
+ Seq({
+ Support.calculateSupportExperience(
+ event = "revival",
+ WeaponStats(ReviveKillAssist(objectId), 1, eventSize, time, 1f)
+ )
+ }),
eventSize,
eventSize,
eventSize,
@@ -700,10 +755,11 @@ object KillContributions {
faction: PlanetSideEmpire.Value,
time: Long,
contributions: Map[SourceUniqueness, List[InGameActivity]],
- excludedTargets: mutable.ListBuffer[SourceUniqueness]
+ excludedTargets: mutable.ListBuffer[SourceUniqueness],
+ eventOutputType: String
): mutable.LongMap[ContributionStats] = {
- val start: Long = time - 600000L
- extractContributionsForMachineByTarget(target, faction, time, start, contributions, excludedTargets)
+ val start: Long = time - Config.app.game.experience.longContributionTime
+ extractContributionsForMachineByTarget(target, faction, time, start, contributions, excludedTargets, eventOutputType)
}
/**
@@ -726,11 +782,12 @@ object KillContributions {
eventTime: Long,
startTime: Long,
contributions: Map[SourceUniqueness, List[InGameActivity]],
- excludedTargets: mutable.ListBuffer[SourceUniqueness]
+ excludedTargets: mutable.ListBuffer[SourceUniqueness],
+ eventOutputType: String
): mutable.LongMap[ContributionStats] = {
val unique = target.unique
val history = limitHistoryToThisLife(contributions.getOrElse(unique, List()), eventTime, startTime)
- extractContributionsForMachineByTarget(target, faction, history, contributions, excludedTargets)
+ extractContributionsForMachineByTarget(target, faction, history, contributions, excludedTargets, eventOutputType)
}
/**
@@ -753,12 +810,13 @@ object KillContributions {
faction: PlanetSideEmpire.Value,
history: List[InGameActivity],
contributions: Map[SourceUniqueness, List[InGameActivity]],
- excludedTargets: mutable.ListBuffer[SourceUniqueness]
+ excludedTargets: mutable.ListBuffer[SourceUniqueness],
+ eventOutputType: String
): mutable.LongMap[ContributionStats] = {
val unique = target.unique
if (!excludedTargets.contains(unique) && history.nonEmpty) {
excludedTargets.addOne(unique)
- val process = new MachineRecoveryExperienceContributionProcess(faction, contributions, excludedTargets)
+ val process = new MachineRecoveryExperienceContributionProcess(faction, contributions, eventOutputType, excludedTargets)
process.submit(history)
cullContributorImplements(process.output())
} else {
diff --git a/src/main/scala/net/psforever/objects/zones/exp/Support.scala b/src/main/scala/net/psforever/objects/zones/exp/Support.scala
index 08fa1dbf4..98a2dfc11 100644
--- a/src/main/scala/net/psforever/objects/zones/exp/Support.scala
+++ b/src/main/scala/net/psforever/objects/zones/exp/Support.scala
@@ -4,6 +4,7 @@ package net.psforever.objects.zones.exp
import net.psforever.objects.sourcing.PlayerSource
import net.psforever.objects.vital.{InGameActivity, ReconstructionActivity, RepairFromExoSuitChange, SpawningActivity}
import net.psforever.types.{ExoSuitType, PlanetSideEmpire}
+import net.psforever.util.Config
import scala.collection.mutable
@@ -11,6 +12,8 @@ import scala.collection.mutable
* Functions to assist experience calculation and history manipulation and analysis.
*/
object Support {
+ private val sep = Config.app.game.experience.sep
+
/**
* Calculate a base experience value to consider additional reasons for points.
* @param victim player to which a final interaction has reduced health to zero
@@ -27,18 +30,19 @@ object Support {
case _ => 0L
}
val base = if (Support.wasEverAMax(victim, history)) {
- 250L
- } else if (victim.Seated || victim.progress.kills.nonEmpty) {
- 100L
+ Config.app.game.experience.bep.base.asMax
+ } else if (victim.progress.kills.nonEmpty) {
+ Config.app.game.experience.bep.base.withKills
+ } else if (victim.Seated) {
+ Config.app.game.experience.bep.base.asMounted
} else if (lifespan > 15000L) {
- 50L
+ Config.app.game.experience.bep.base.mature
} else {
1L
}
if (base > 1) {
//black ops modifier
- //TODO x10
- base
+ base * Config.app.game.experience.bep.base.bopsMultiplier
} else {
base
}
@@ -187,4 +191,49 @@ object Support {
case _ => false
}
}
+
+ /**
+ * Take a weapon statistics entry and calculate the support experience value resulting from this support event.
+ * The complete formula is:
+ * `base + shots-multplier * ln(shots^exp + 2) + amount-multiplier * amount`
+ * ... where the middle field can be truncated into:
+ * `shots-multplier * shots`
+ * ... without the natural logarithm exponent defined.
+ * Limits can be applied to the number of shots and/or to the amount,
+ * which will either zero the calculations or cap the results.
+ * @param event identification for the event calculation parameters
+ * @param weaponStat base weapon stat entry to be modified
+ * @param canNotFindEventDefaultValue custom default value
+ * @return weapon stat entry with a modified for the experience
+ */
+ private[exp] def calculateSupportExperience(
+ event: String,
+ weaponStat: WeaponStats,
+ canNotFindEventDefaultValue: Option[Float] = None
+ ): WeaponStats = {
+ val rewards: Float = sep.events
+ .find(evt => event.equals(evt.name))
+ .map { event =>
+ val shots = weaponStat.shots
+ val shotsMultiplier = event.shotsMultiplier
+ if (shotsMultiplier > 0f && shots < event.shotsCutoff) {
+ val modifiedShotsReward: Float = {
+ val partialShots = math.min(event.shotsLimit, shots).toFloat
+ shotsMultiplier * (if (event.shotsNatLog > 0f) {
+ math.log(math.pow(partialShots, event.shotsNatLog) + 2d).toFloat
+ } else {
+ partialShots
+ })
+ }
+ val modifiedAmountReward: Float = event.amountMultiplier * weaponStat.amount.toFloat
+ event.base + modifiedShotsReward + modifiedAmountReward
+ } else {
+ 0f
+ }
+ }
+ .getOrElse(
+ canNotFindEventDefaultValue.getOrElse(sep.canNotFindEventDefaultValue.toFloat)
+ )
+ weaponStat.copy(contributions = rewards)
+ }
}
diff --git a/src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala b/src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala
index 62b76a04e..fc9801c2b 100644
--- a/src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala
+++ b/src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala
@@ -2,7 +2,6 @@
package net.psforever.objects.zones.exp
import scala.concurrent.ExecutionContext.Implicits.global
-
import net.psforever.objects.avatar.scoring.EquipmentStat
import net.psforever.objects.serverobject.hackable.Hackable.HackInfo
import net.psforever.objects.sourcing.VehicleSource
@@ -11,8 +10,6 @@ import net.psforever.types.Vector3
import net.psforever.util.Database.ctx
import net.psforever.util.Database.ctx._
-import scala.util.Success
-
object ToDatabase {
/**
* Insert an entry into the database's `killactivity` table.
@@ -57,7 +54,7 @@ object ToDatabase {
position: Vector3,
exp: Long
): Unit = {
- ctx.run(query[persistence.Assistactivity]
+ ctx.run(query[persistence.Killactivity]
.insert(
_.killerId -> lift(avatarId),
_.victimId -> lift(victimId),
@@ -115,6 +112,10 @@ object ToDatabase {
_.assists -> lift(0),
_.sessionId -> lift(-1L)
)
+ .onConflictUpdate(_.avatarId, _.weaponId, _.sessionId)(
+ (t, e) => t.shotsFired -> (t.shotsFired + e.shotsFired),
+ (t, e) => t.shotsLanded -> (t.shotsLanded + e.shotsLanded)
+ )
)
}
@@ -151,4 +152,66 @@ object ToDatabase {
)
)
}
+
+ /**
+ * Insert an entry into the database's `ntuactivity` table.
+ * This table monitors experience earned through NTU silo operations and
+ * first time event entity interactions (zone and building set to 0).
+ */
+ def reportNtuActivity(
+ avatarId: Long,
+ zoneId: Int,
+ buildingId: Int,
+ experience: Long
+ ): Unit = {
+ ctx.run(query[persistence.Ntuactivity]
+ .insert(
+ _.avatarId -> lift(avatarId),
+ _.zoneId -> lift(zoneId),
+ _.buildingId -> lift(buildingId),
+ _.exp -> lift(experience)
+ )
+ .onConflictUpdate(_.avatarId, _.zoneId, _.buildingId)(
+ (t, e) => t.exp -> (t.exp + e.exp)
+ )
+ )
+ }
+
+ /**
+ * Insert an entry into the database's `kdasession` table
+ * to specifically update the revive counter for the current session.
+ */
+ def reportRespawns(
+ avatarId: Long,
+ reviveCount: Int
+ ): Unit = {
+ ctx.run(query[persistence.Kdasession]
+ .insert(
+ _.avatarId -> lift(avatarId),
+ _.revives -> lift(reviveCount),
+ _.sessionId -> lift(-1)
+ )
+ )
+ }
+
+ /**
+ * Insert an entry into the database's `buildingCapture` table.
+ */
+ def reportFacilityCapture(
+ avatarId: Long,
+ zoneId: Int,
+ buildingId: Int,
+ exp: Long,
+ expType: String
+ ): Unit = {
+ ctx.run(query[persistence.Buildingcapture]
+ .insert(
+ _.avatarId -> lift(avatarId),
+ _.zoneId -> lift(zoneId),
+ _.buildingId -> lift(buildingId),
+ _.exp -> lift(exp),
+ _.expType -> lift(expType)
+ )
+ )
+ }
}
diff --git a/src/main/scala/net/psforever/objects/zones/exp/rec/CombinedHealthAndArmorContributionProcess.scala b/src/main/scala/net/psforever/objects/zones/exp/rec/CombinedHealthAndArmorContributionProcess.scala
index 8e0685f06..59b9a2089 100644
--- a/src/main/scala/net/psforever/objects/zones/exp/rec/CombinedHealthAndArmorContributionProcess.scala
+++ b/src/main/scala/net/psforever/objects/zones/exp/rec/CombinedHealthAndArmorContributionProcess.scala
@@ -3,7 +3,7 @@ package net.psforever.objects.zones.exp.rec
import net.psforever.objects.sourcing.SourceUniqueness
import net.psforever.objects.vital.InGameActivity
-import net.psforever.objects.zones.exp.{ContributionStats, KillContributions, WeaponStats}
+import net.psforever.objects.zones.exp.{ContributionStats, KillContributions, Support, WeaponStats}
import net.psforever.types.PlanetSideEmpire
import scala.collection.mutable
@@ -58,23 +58,32 @@ class CombinedHealthAndArmorContributionProcess(
}
.map {
case (id, entry) =>
+ var totalShots: Int = 0
+ var totalAmount: Int = 0
+ var mostRecentTime: Long = 0
val groupedWeapons = entry.weapons
.groupBy(_.equipment)
.map {
- case (weaponId, weaponEntries) =>
- val specificEntries = weaponEntries.filter(_.equipment == weaponId)
+ case (weaponContext, weaponEntries) =>
+ val specificEntries = weaponEntries.filter(_.equipment == weaponContext)
val amount = specificEntries.foldLeft(0)(_ + _.amount)
+ totalAmount = totalAmount + amount
val shots = specificEntries.foldLeft(0)(_ + _.shots)
- WeaponStats(weaponId, amount, shots, specificEntries.maxBy(_.time).time, 1f)
+ totalShots = totalShots + shots
+ val time = specificEntries.maxBy(_.time).time
+ mostRecentTime = math.max(mostRecentTime, time)
+ Support.calculateSupportExperience(
+ event = "support-heal",
+ WeaponStats(weaponContext, amount, shots, time, 1f)
+ )
}
.toSeq
- (id, ContributionStats(
- player = entry.player,
+ (id, entry.copy(
weapons = groupedWeapons,
- amount = entry.amount + entry.amount,
- total = entry.total + entry.total,
- shots = groupedWeapons.foldLeft(0)(_ + _.shots),
- time = groupedWeapons.maxBy(_.time).time
+ amount = totalAmount,
+ total = math.max(entry.total, totalAmount),
+ shots = totalShots,
+ time = mostRecentTime
))
}
}
diff --git a/src/main/scala/net/psforever/objects/zones/exp/rec/MachineRecoveryExperienceContributionProcess.scala b/src/main/scala/net/psforever/objects/zones/exp/rec/MachineRecoveryExperienceContributionProcess.scala
index ce0404888..c1544fa46 100644
--- a/src/main/scala/net/psforever/objects/zones/exp/rec/MachineRecoveryExperienceContributionProcess.scala
+++ b/src/main/scala/net/psforever/objects/zones/exp/rec/MachineRecoveryExperienceContributionProcess.scala
@@ -3,15 +3,15 @@ package net.psforever.objects.zones.exp.rec
import net.psforever.objects.sourcing.SourceUniqueness
import net.psforever.objects.vital.{DamagingActivity, InGameActivity, RepairFromEquipment, RepairingActivity}
-import net.psforever.objects.zones.exp.ContributionStats
+import net.psforever.objects.zones.exp.{ContributionStats, Support, WeaponStats}
import net.psforever.types.PlanetSideEmpire
import scala.collection.mutable
-//noinspection ScalaUnusedSymbol
class MachineRecoveryExperienceContributionProcess(
private val faction : PlanetSideEmpire.Value,
private val contributions: Map[SourceUniqueness, List[InGameActivity]],
+ eventOutputType: String,
private val excludedTargets: mutable.ListBuffer[SourceUniqueness] = mutable.ListBuffer()
) extends RecoveryExperienceContributionProcess(faction, contributions) {
def submit(history: List[InGameActivity]): Unit = {
@@ -65,11 +65,9 @@ class MachineRecoveryExperienceContributionProcess(
.map { case (wrapper, entries) =>
val size = entries.size
val newTime = entries.maxBy(_.time).time
- entries.head.copy(
- shots = size,
- amount = entries.foldLeft(0)(_ + _.amount),
- contributions = (10 + size).toFloat,
- time = newTime
+ Support.calculateSupportExperience(
+ eventOutputType,
+ WeaponStats(wrapper, size, entries.foldLeft(0)(_ + _.amount), newTime, 1f)
)
}
.toSeq
diff --git a/src/main/scala/net/psforever/persistence/Assistactivity.scala b/src/main/scala/net/psforever/persistence/Assistactivity.scala
deleted file mode 100644
index 9d59ad53c..000000000
--- a/src/main/scala/net/psforever/persistence/Assistactivity.scala
+++ /dev/null
@@ -1,16 +0,0 @@
-package net.psforever.persistence
-
-import org.joda.time.LocalDateTime
-
-case class Assistactivity(
- index: Int,
- killerId: Long,
- victimId: Long,
- weaponId: Int,
- zoneId: Int,
- px: Int, //Position.x * 1000
- py: Int, //Position.y * 1000
- pz: Int, //Position.z * 1000
- exp: Long,
- timestamp: LocalDateTime = LocalDateTime.now()
- )
diff --git a/src/main/scala/net/psforever/persistence/KdaExp.scala b/src/main/scala/net/psforever/persistence/KdaExp.scala
new file mode 100644
index 000000000..66fcc0834
--- /dev/null
+++ b/src/main/scala/net/psforever/persistence/KdaExp.scala
@@ -0,0 +1,95 @@
+// Copyright (c) 2023 PSForever
+package net.psforever.persistence
+
+import org.joda.time.LocalDateTime
+
+case class Assistactivity(
+ index: Int,
+ killerId: Long,
+ victimId: Long,
+ weaponId: Int,
+ zoneId: Int,
+ px: Int, //Position.x * 1000
+ py: Int, //Position.y * 1000
+ pz: Int, //Position.z * 1000
+ exp: Long,
+ timestamp: LocalDateTime = LocalDateTime.now()
+ )
+
+case class Buildingcapture(
+ index: Int,
+ avatarId: Long,
+ zoneId: Int,
+ buildingId: Int,
+ exp: Long,
+ expType: String,
+ timestamp: LocalDateTime = LocalDateTime.now()
+ )
+
+case class Kdasession (
+ avatarId: Long,
+ sessionId: Int,
+ kills: Int,
+ deaths: Int,
+ assists: Int,
+ revives: Int
+ )
+
+case class Killactivity(
+ index: Int,
+ killerId: Long,
+ victimId: Long,
+ victimExosuit: Int,
+ victimMounted: Int, //object type id * 10 + seat type
+ weaponId: Int,
+ zoneId: Int,
+ px: Int, //Position.x * 1000
+ py: Int, //Position.y * 1000
+ pz: Int, //Position.z * 1000
+ exp: Long,
+ timestamp: LocalDateTime = LocalDateTime.now()
+ )
+
+case class Machinedestroyed(
+ index: Int,
+ avatarId: Long,
+ weaponId: Int,
+ machineType: Int,
+ machineFaction: Int,
+ hackedFaction: Int,
+ asCargo: Boolean,
+ zoneNum: Int,
+ px: Int, //Position.x * 1000
+ py: Int, //Position.y * 1000
+ pz: Int, //Position.z * 1000
+ timestamp: LocalDateTime = LocalDateTime.now()
+ )
+
+case class Ntuactivity (
+ avatarId: Long,
+ zoneId: Int,
+ buildingId: Int,
+ exp: Long
+ )
+
+case class Supportactivity(
+ index: Int,
+ userId: Long,
+ targetId: Long,
+ targetExosuit: Int,
+ interactionType: Int,
+ implementType: Int,
+ intermediateType: Int,
+ exp: Long,
+ timestamp: LocalDateTime = LocalDateTime.now()
+ )
+
+case class Weaponstatsession(
+ avatarId: Long,
+ weaponId: Int,
+ shotsFired: Int,
+ shotsLanded: Int,
+ kills: Int,
+ assists: Int,
+ sessionId: Long
+ )
diff --git a/src/main/scala/net/psforever/persistence/Killactivity.scala b/src/main/scala/net/psforever/persistence/Killactivity.scala
deleted file mode 100644
index 2eb6a1f7e..000000000
--- a/src/main/scala/net/psforever/persistence/Killactivity.scala
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright (c) 2022 PSForever
-package net.psforever.persistence
-
-import org.joda.time.LocalDateTime
-
-case class Killactivity(
- index: Int,
- killerId: Long,
- victimId: Long,
- victimExosuit: Int,
- victimMounted: Int, //object type id * 10 + seat type
- weaponId: Int,
- zoneId: Int,
- px: Int, //Position.x * 1000
- py: Int, //Position.y * 1000
- pz: Int, //Position.z * 1000
- exp: Long,
- timestamp: LocalDateTime = LocalDateTime.now()
- )
diff --git a/src/main/scala/net/psforever/persistence/Machinedestroyed.scala b/src/main/scala/net/psforever/persistence/Machinedestroyed.scala
deleted file mode 100644
index c25e4eac1..000000000
--- a/src/main/scala/net/psforever/persistence/Machinedestroyed.scala
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright (c) 2022 PSForever
-package net.psforever.persistence
-
-import org.joda.time.LocalDateTime
-
-case class Machinedestroyed(
- index: Int,
- avatarId: Long,
- weaponId: Int,
- machineType: Int,
- machineFaction: Int,
- hackedFaction: Int,
- asCargo: Boolean,
- zoneNum: Int,
- px: Int, //Position.x * 1000
- py: Int, //Position.y * 1000
- pz: Int, //Position.z * 1000
- timestamp: LocalDateTime = LocalDateTime.now()
- )
diff --git a/src/main/scala/net/psforever/persistence/Supportactivity.scala b/src/main/scala/net/psforever/persistence/Supportactivity.scala
deleted file mode 100644
index d131adaec..000000000
--- a/src/main/scala/net/psforever/persistence/Supportactivity.scala
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright (c) 2022 PSForever
-package net.psforever.persistence
-
-import org.joda.time.LocalDateTime
-
-case class Supportactivity(
- index: Int,
- userId: Long,
- targetId: Long,
- targetExosuit: Int,
- interactionType: Int,
- implementType: Int,
- intermediateType: Int,
- exp: Long,
- timestamp: LocalDateTime = LocalDateTime.now()
- )
diff --git a/src/main/scala/net/psforever/persistence/Weaponstatsession.scala b/src/main/scala/net/psforever/persistence/Weaponstatsession.scala
deleted file mode 100644
index 7aca2bf82..000000000
--- a/src/main/scala/net/psforever/persistence/Weaponstatsession.scala
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright (c) 2022 PSForever
-package net.psforever.persistence
-
-case class Weaponstatsession(
- avatarId: Long,
- weaponId: Int,
- shotsFired: Int,
- shotsLanded: Int,
- kills: Int,
- assists: Int,
- sessionId: Long
- )
diff --git a/src/main/scala/net/psforever/services/RemoverActor.scala b/src/main/scala/net/psforever/services/RemoverActor.scala
index 0348a3c1b..d6118dccc 100644
--- a/src/main/scala/net/psforever/services/RemoverActor.scala
+++ b/src/main/scala/net/psforever/services/RemoverActor.scala
@@ -82,7 +82,7 @@ abstract class RemoverActor() extends SupportActor[RemoverActor.Entry] {
entryManagementBehaviors
.orElse {
case RemoverActor.AddTask(obj, zone, duration) =>
- val entry = RemoverActor.Entry(obj, zone, duration.getOrElse(FirstStandardDuration).toNanos)
+ val entry = RemoverActor.Entry(obj, zone, duration.getOrElse(FirstStandardDuration).toMillis)
if (InclusionTest(entry) && !secondHeap.exists(test => sameEntryComparator.Test(test, entry))) {
InitialJob(entry)
if (entry.duration == 0) {
@@ -120,7 +120,7 @@ abstract class RemoverActor() extends SupportActor[RemoverActor.Entry] {
case RemoverActor.StartDelete() =>
firstTask.cancel()
secondTask.cancel()
- val now: Long = System.nanoTime
+ val now: Long = System.currentTimeMillis()
val (in, out) = firstHeap.partition(entry => {
now - entry.time >= entry.duration
})
@@ -229,19 +229,19 @@ abstract class RemoverActor() extends SupportActor[RemoverActor.Entry] {
* this new entry is always set to last for the duration of the second pool
*/
private def RepackageEntry(entry: RemoverActor.Entry): RemoverActor.Entry = {
- RemoverActor.Entry(entry.obj, entry.zone, SecondStandardDuration.toNanos)
+ RemoverActor.Entry(entry.obj, entry.zone, SecondStandardDuration.toMillis)
}
/**
* Common function to reset the first task's delayed execution.
* Cancels the scheduled timer and will only restart the timer if there is at least one entry in the first pool.
- * @param now the time (in nanoseconds);
- * defaults to the current time (in nanoseconds)
+ * @param now the time (in milliseconds);
+ * defaults to the current time (in milliseconds)
*/
- def RetimeFirstTask(now: Long = System.nanoTime): Unit = {
+ def RetimeFirstTask(now: Long = System.currentTimeMillis()): Unit = {
firstTask.cancel()
if (firstHeap.nonEmpty) {
- val short_timeout: FiniteDuration = math.max(1, firstHeap.head.duration - (now - firstHeap.head.time)) nanoseconds
+ val short_timeout: FiniteDuration = math.max(1, firstHeap.head.duration - (now - firstHeap.head.time)).milliseconds
import scala.concurrent.ExecutionContext.Implicits.global
firstTask = context.system.scheduler.scheduleOnce(short_timeout, self, RemoverActor.StartDelete())
}
@@ -274,14 +274,14 @@ abstract class RemoverActor() extends SupportActor[RemoverActor.Entry] {
/**
* Default time for entries waiting in the first list.
* Override.
- * @return the time as a `FiniteDuration` object (to be later transformed into nanoseconds)
+ * @return the time as a `FiniteDuration` object (to be later transformed into milliseconds)
*/
def FirstStandardDuration: FiniteDuration
/**
* Default time for entries waiting in the second list.
* Override.
- * @return the time as a `FiniteDuration` object (to be later transformed into nanoseconds)
+ * @return the time as a `FiniteDuration` object (to be later transformed into milliseconds)
*/
def SecondStandardDuration: FiniteDuration
@@ -322,7 +322,7 @@ object RemoverActor extends SupportActorCaseConversions {
* Internally, all entries have a "time created" field.
* @param _obj the target
* @param _zone the zone in which this target is registered
- * @param _duration how much longer the target will exist in its current state (in nanoseconds)
+ * @param _duration how much longer the target will exist in its current state (in milliseconds)
*/
case class Entry(_obj: PlanetSideGameObject, _zone: Zone, _duration: Long)
extends SupportActor.Entry(_obj, _zone, _duration)
@@ -332,7 +332,7 @@ object RemoverActor extends SupportActorCaseConversions {
* @see `FirstStandardDuration`
* @param obj the target
* @param zone the zone in which this target is registered
- * @param duration how much longer the target will exist in its current state (in nanoseconds);
+ * @param duration how much longer the target will exist in its current state (in milliseconds);
* a default time duration is provided by implementation
*/
case class AddTask(obj: PlanetSideGameObject, zone: Zone, duration: Option[FiniteDuration] = None)
diff --git a/src/main/scala/net/psforever/services/avatar/AvatarService.scala b/src/main/scala/net/psforever/services/avatar/AvatarService.scala
index 53c50321d..8ee7edb52 100644
--- a/src/main/scala/net/psforever/services/avatar/AvatarService.scala
+++ b/src/main/scala/net/psforever/services/avatar/AvatarService.scala
@@ -456,6 +456,15 @@ class AvatarService(zone: Zone) extends Actor {
)
)
+ case AvatarAction.FacilityCaptureRewards(building_id, zone_number, exp) =>
+ AvatarEvents.publish(
+ AvatarServiceResponse(
+ s"/$forChannel/Avatar",
+ Service.defaultPlayerGUID,
+ AvatarResponse.FacilityCaptureRewards(building_id, zone_number, exp)
+ )
+ )
+
case _ => ()
}
diff --git a/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala b/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala
index d66925062..33d5b894f 100644
--- a/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala
+++ b/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala
@@ -158,6 +158,7 @@ object AvatarAction {
final case class UpdateKillsDeathsAssists(charId: Long, kda: KDAStat) extends Action
final case class AwardBep(charId: Long, bep: Long, expType: ExperienceType) extends Action
final case class AwardCep(charId: Long, bep: Long) extends Action
+ final case class FacilityCaptureRewards(building_id: Int, zone_number: Int, exp: Long) extends Action
final case class TeardownConnection() extends Action
// final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action
diff --git a/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala b/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala
index af730abcd..05ba1a1d5 100644
--- a/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala
+++ b/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala
@@ -129,4 +129,5 @@ object AvatarResponse {
final case class UpdateKillsDeathsAssists(charId: Long, kda: KDAStat) extends Response
final case class AwardBep(charId: Long, bep: Long, expType: ExperienceType) extends Response
final case class AwardCep(charId: Long, bep: Long) extends Response
+ final case class FacilityCaptureRewards(building_id: Int, zone_number: Int, exp: Long) extends Response
}
diff --git a/src/main/scala/net/psforever/services/local/support/DoorCloseActor.scala b/src/main/scala/net/psforever/services/local/support/DoorCloseActor.scala
index 3d25dcb41..115924267 100644
--- a/src/main/scala/net/psforever/services/local/support/DoorCloseActor.scala
+++ b/src/main/scala/net/psforever/services/local/support/DoorCloseActor.scala
@@ -49,8 +49,7 @@ class DoorCloseActor() extends Actor {
})
if (openDoors.nonEmpty) {
- val short_timeout: FiniteDuration =
- math.max(1, DoorCloseActor.timeout_time - (now - openDoors.head.time)) nanoseconds
+ val short_timeout: FiniteDuration = math.max(1, DoorCloseActor.timeout_time - (now - openDoors.head.time)).milliseconds
import scala.concurrent.ExecutionContext.Implicits.global
doorCloserTrigger = context.system.scheduler.scheduleOnce(short_timeout, self, DoorCloseActor.TryCloseDoors())
}
@@ -67,7 +66,7 @@ class DoorCloseActor() extends Actor {
* and newer entries are always added to the end of the main `List`,
* processing in order is always correct.
* @param list the `List` of entries to divide
- * @param now the time right now (in nanoseconds)
+ * @param now the time right now (in milliseconds)
* @see `List.partition`
* @return a `Tuple` of two `Lists`, whose qualifications are explained above
*/
@@ -84,7 +83,7 @@ class DoorCloseActor() extends Actor {
* a `List` of elements that have exceeded the time limit,
* and a `List` of elements that still satisfy the time limit.
* @param iter the `Iterator` of entries to divide
- * @param now the time right now (in nanoseconds)
+ * @param now the time right now (in milliseconds)
* @param index a persistent record of the index where list division should occur;
* defaults to 0
* @return the index where division will occur
diff --git a/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala b/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala
index 500fc63da..5f259aee0 100644
--- a/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala
+++ b/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala
@@ -17,7 +17,6 @@ import net.psforever.services.local.support.HackCaptureActor.GetHackingFaction
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID}
-import java.util.concurrent.TimeUnit
import scala.concurrent.duration.{FiniteDuration, _}
import scala.util.Random
@@ -38,7 +37,7 @@ class HackCaptureActor extends Actor {
val duration = target.Definition.FacilityHackTime
target.HackedBy match {
case Some(hackInfo) =>
- target.HackedBy = hackInfo.Duration(duration.toNanos)
+ target.HackedBy = hackInfo.Duration(duration.toMillis)
case None =>
log.error(s"Initial $target hack information is missing")
}
@@ -57,8 +56,8 @@ class HackCaptureActor extends Actor {
case HackCaptureActor.ProcessCompleteHacks() =>
log.trace("Processing complete hacks")
clearTrigger.cancel()
- val now: Long = System.nanoTime
- val (stillHacked, finishedHacks) = hackedObjects.partition(x => now - x.hack_timestamp < x.duration.toNanos)
+ val now: Long = System.currentTimeMillis()
+ val (stillHacked, finishedHacks) = hackedObjects.partition(x => now - x.hack_timestamp < x.duration.toMillis)
hackedObjects = stillHacked
finishedHacks.foreach { entry =>
val terminal = entry.target
@@ -213,9 +212,8 @@ class HackCaptureActor extends Actor {
private def RestartTimer(): Unit = {
if (hackedObjects.nonEmpty) {
- val hackEntry = hackedObjects.reduceLeft(HackCaptureActor.minTimeLeft(System.nanoTime()))
- val short_timeout: FiniteDuration =
- math.max(1, hackEntry.duration.toNanos - (System.nanoTime - hackEntry.hack_timestamp)).nanoseconds
+ val hackEntry = hackedObjects.reduceLeft(HackCaptureActor.minTimeLeft(System.currentTimeMillis()))
+ val short_timeout: FiniteDuration = math.max(1, hackEntry.duration.toMillis - (System.currentTimeMillis() - hackEntry.hack_timestamp)).milliseconds
log.trace(s"RestartTimer: still items left in hacked objects list. Checking again in ${short_timeout.toSeconds} seconds")
import scala.concurrent.ExecutionContext.Implicits.global
clearTrigger = context.system.scheduler.scheduleOnce(short_timeout, self, HackCaptureActor.ProcessCompleteHacks())
@@ -229,7 +227,7 @@ object HackCaptureActor {
zone: Zone,
unk1: Long,
unk2: Long,
- startTime: Long = System.nanoTime()
+ startTime: Long = System.currentTimeMillis()
)
final case class ResecureCaptureTerminal(target: CaptureTerminal, zone: Zone, hacker: PlayerSource)
@@ -256,8 +254,7 @@ object HackCaptureActor {
17039360L
case Some(Hackable.HackInfo(p, _, start, length)) =>
// See PlanetSideAttributeMessage #20 documentation for an explanation of how the timer is calculated
- val hackTimeRemainingMS =
- TimeUnit.MILLISECONDS.convert(math.max(0, start + length - System.nanoTime), TimeUnit.NANOSECONDS)
+ val hackTimeRemainingMS = math.max(0, start + length - System.currentTimeMillis())
val startNum = p.Faction match {
case PlanetSideEmpire.TR => 0x10000
case PlanetSideEmpire.NC => 0x20000
@@ -273,8 +270,8 @@ object HackCaptureActor {
entry1: HackCaptureActor.HackEntry,
entry2: HackCaptureActor.HackEntry
): HackCaptureActor.HackEntry = {
- val entry1TimeLeft = entry1.duration.toNanos - (now - entry1.hack_timestamp)
- val entry2TimeLeft = entry2.duration.toNanos - (now - entry2.hack_timestamp)
+ val entry1TimeLeft = entry1.duration.toMillis - (now - entry1.hack_timestamp)
+ val entry2TimeLeft = entry2.duration.toMillis - (now - entry2.hack_timestamp)
if (entry1TimeLeft < entry2TimeLeft) {
entry1
} else {
diff --git a/src/main/scala/net/psforever/services/local/support/HackClearActor.scala b/src/main/scala/net/psforever/services/local/support/HackClearActor.scala
index 2e3599fec..c6f8e8bb4 100644
--- a/src/main/scala/net/psforever/services/local/support/HackClearActor.scala
+++ b/src/main/scala/net/psforever/services/local/support/HackClearActor.scala
@@ -29,15 +29,15 @@ class HackClearActor() extends Actor {
def receive: Receive = {
case HackClearActor.ObjectIsHacked(target, zone, unk1, unk2, duration, time) =>
- val durationNanos = TimeUnit.NANOSECONDS.convert(duration, TimeUnit.SECONDS)
- hackedObjects = hackedObjects :+ HackClearActor.HackEntry(target, zone, unk1, unk2, time, durationNanos)
+ val durationMillis = TimeUnit.MILLISECONDS.convert(duration, TimeUnit.SECONDS)
+ hackedObjects = hackedObjects :+ HackClearActor.HackEntry(target, zone, unk1, unk2, time, durationMillis)
// Restart the timer, in case this is the first object in the hacked objects list
RestartTimer()
case HackClearActor.TryClearHacks() =>
clearTrigger.cancel()
- val now: Long = System.nanoTime
+ val now: Long = System.currentTimeMillis()
//TODO we can just walk across the list of doors and extract only the first few entries
val (unhackObjects, stillHackedObjects) = PartitionEntries(hackedObjects, now)
hackedObjects = stillHackedObjects
@@ -75,12 +75,12 @@ class HackClearActor() extends Actor {
private def RestartTimer(): Unit = {
if (hackedObjects.nonEmpty) {
- val now = System.nanoTime()
+ val now = System.currentTimeMillis()
val (_/*unhackObjects*/, stillHackedObjects) = PartitionEntries(hackedObjects, now)
stillHackedObjects.headOption match {
case Some(hackEntry) =>
- val short_timeout: FiniteDuration = math.max(1, hackEntry.duration - (now - hackEntry.time)) nanoseconds
+ val short_timeout: FiniteDuration = math.max(1, hackEntry.duration - (now - hackEntry.time)).milliseconds
log.debug(
s"HackClearActor: still items left in hacked objects list. Checking again in ${short_timeout.toSeconds} seconds"
@@ -102,7 +102,7 @@ class HackClearActor() extends Actor {
* and newer entries are always added to the end of the main `List`,
* processing in order is always correct.
* @param list the `List` of entries to divide
- * @param now the time right now (in nanoseconds)
+ * @param now the time right now (in milliseconds)
* @see `List.partition`
* @return a `Tuple` of two `Lists`, whose qualifications are explained above
*/
@@ -119,7 +119,7 @@ class HackClearActor() extends Actor {
* a `List` of elements that have exceeded the time limit,
* and a `List` of elements that still satisfy the time limit.
* @param iter the `Iterator` of entries to divide
- * @param now the time right now (in nanoseconds)
+ * @param now the time right now (in milliseconds)
* @param index a persistent record of the index where list division should occur;
* defaults to 0
* @return the index where division will occur
@@ -158,7 +158,7 @@ object HackClearActor {
unk1: Long,
unk2: Long,
duration: Int,
- time: Long = System.nanoTime()
+ time: Long = System.currentTimeMillis()
)
/**
@@ -185,7 +185,7 @@ object HackClearActor {
* @param target the server object
* @param zone the zone in which the object resides
* @param time when the object was hacked
- * @param duration The hack duration in nanoseconds
+ * @param duration The hack duration in milliseconds
* @see `ObjectIsHacked`
*/
private final case class HackEntry(
diff --git a/src/main/scala/net/psforever/services/support/SupportActor.scala b/src/main/scala/net/psforever/services/support/SupportActor.scala
index dd3076e4f..bb52e1f0b 100644
--- a/src/main/scala/net/psforever/services/support/SupportActor.scala
+++ b/src/main/scala/net/psforever/services/support/SupportActor.scala
@@ -96,7 +96,7 @@ abstract class SupportActor[A <: SupportActor.Entry] extends Actor {
object SupportActor {
class Entry(val obj: PlanetSideGameObject, val zone: Zone, val duration: Long) {
- val time: Long = System.nanoTime
+ val time: Long = System.currentTimeMillis()
}
/**
diff --git a/src/main/scala/net/psforever/services/vehicle/support/TurretUpgrader.scala b/src/main/scala/net/psforever/services/vehicle/support/TurretUpgrader.scala
index fa93a1292..eb064d72b 100644
--- a/src/main/scala/net/psforever/services/vehicle/support/TurretUpgrader.scala
+++ b/src/main/scala/net/psforever/services/vehicle/support/TurretUpgrader.scala
@@ -50,8 +50,8 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
entryManagementBehaviors
.orElse {
case TurretUpgrader.AddTask(turret, zone, upgrade, duration) =>
- val lengthOfTime = duration.getOrElse(TurretUpgrader.StandardUpgradeLifetime).toNanos
- if (lengthOfTime > (1 second).toNanos) { //don't even bother if it's too short; it'll revert near instantly
+ val lengthOfTime = duration.getOrElse(TurretUpgrader.StandardUpgradeLifetime).toMillis
+ if (lengthOfTime > (1 second).toMillis) { //don't even bother if it's too short; it'll revert near instantly
val entry = CreateEntry(turret, zone, TurretUpgrade.None, lengthOfTime)
UpgradeTurretAmmo(CreateEntry(turret, zone, upgrade, lengthOfTime))
if (list.isEmpty) {
@@ -73,7 +73,7 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
case TurretUpgrader.Downgrade() =>
task.cancel()
- val now: Long = System.nanoTime
+ val now: Long = System.currentTimeMillis()
val (in, out) =
list.partition(entry => {
now - entry.time >= entry.duration
@@ -85,10 +85,10 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
case _ => ;
}
- def RetimeFirstTask(now: Long = System.nanoTime): Unit = {
+ def RetimeFirstTask(now: Long = System.currentTimeMillis()): Unit = {
task.cancel()
if (list.nonEmpty) {
- val short_timeout: FiniteDuration = math.max(1, list.head.duration - (now - list.head.time)) nanoseconds
+ val short_timeout: FiniteDuration = math.max(1, list.head.duration - (now - list.head.time)).milliseconds
import scala.concurrent.ExecutionContext.Implicits.global
task = context.system.scheduler.scheduleOnce(short_timeout, self, TurretUpgrader.Downgrade())
}
@@ -249,7 +249,7 @@ object TurretUpgrader extends SupportActorCaseConversions {
* @param _obj the target
* @param _zone the zone in which this target is registered
* @param upgrade the next upgrade state for this turret
- * @param _duration how much longer the target will exist in its current state (in nanoseconds)
+ * @param _duration how much longer the target will exist in its current state (in milliseconds)
*/
case class Entry(_obj: PlanetSideGameObject, _zone: Zone, upgrade: TurretUpgrade.Value, _duration: Long)
extends SupportActor.Entry(_obj, _zone, _duration)
diff --git a/src/main/scala/net/psforever/util/Config.scala b/src/main/scala/net/psforever/util/Config.scala
index 6c3b8e9de..cdde9a011 100644
--- a/src/main/scala/net/psforever/util/Config.scala
+++ b/src/main/scala/net/psforever/util/Config.scala
@@ -150,9 +150,6 @@ case class GameConfig(
instantAction: InstantActionConfig,
amenityAutorepairRate: Float,
amenityAutorepairDrainRate: Float,
- bepRate: Double,
- cepRate: Double,
- maximumCepPerSquadSize: Seq[Int],
newAvatar: NewAvatar,
hart: HartConfig,
sharedMaxCooldown: Boolean,
@@ -162,7 +159,8 @@ case class GameConfig(
cavernRotation: CavernRotationConfig,
savedMsg: SavedMessageEvents,
playerDraw: PlayerStateDrawSettings,
- doorsCanBeOpenedByMedAppFromThisDistance: Float
+ doorsCanBeOpenedByMedAppFromThisDistance: Float,
+ experience: Experience
)
case class InstantActionConfig(
@@ -238,3 +236,51 @@ case class PlayerStateDrawSettings(
assert(ranges.nonEmpty)
assert(ranges.size == delays.size)
}
+
+case class Experience(
+ shortContributionTime: Long,
+ longContributionTime: Long,
+ bep: BattleExperiencePoints,
+ sep: SupportExperiencePoints,
+ cep: CommandExperiencePoints
+) {
+ assert(shortContributionTime < longContributionTime)
+}
+
+case class BattleExperiencePoints(
+ base: BattleExperiencePointsBase,
+ rate: Float
+)
+
+case class BattleExperiencePointsBase(
+ bopsMultiplier: Long,
+ asMax: Long,
+ withKills: Long,
+ asMounted: Long,
+ mature: Long
+)
+
+case class SupportExperiencePoints(
+ rate: Float,
+ ntuSiloDepositReward: Long,
+ canNotFindEventDefaultValue: Long,
+ events: Seq[SupportExperienceEvent]
+)
+
+case class SupportExperienceEvent(
+ name: String,
+ base: Long,
+ shotsMultiplier: Float = 0f,
+ shotsNatLog: Double = 0f,
+ shotsLimit: Int = 50,
+ shotsCutoff: Int = 50,
+ amountMultiplier: Float = 0f
+)
+
+case class CommandExperiencePoints(
+ rate: Float,
+ lluCarrierModifier: Float,
+ maximumPerSquadSize: Seq[Int],
+ squadSizeLimitOverflow: Int,
+ squadSizeLimitOverflowMultiplier: Float
+)
diff --git a/src/test/scala/objects/PlayerControlTest.scala b/src/test/scala/objects/PlayerControlTest.scala
index 9266d0c79..df5833690 100644
--- a/src/test/scala/objects/PlayerControlTest.scala
+++ b/src/test/scala/objects/PlayerControlTest.scala
@@ -624,7 +624,7 @@ class PlayerControlDeathStandingTest extends ActorTest {
// override def AvatarEvents = avatarProbe.ref
// override def Activity = activityProbe.ref
// }
-// zone.actor = system.spawn(ZoneActor(zone), s"test-zone-${System.nanoTime()}")
+// zone.actor = system.spawn(ZoneActor(zone), s"test-zone-${System.currentTimeMillis()}")
//
// player1.Zone = zone
// player1.Spawn()