diff --git a/sources/Commands/ACommandGod.uc b/sources/Commands/ACommandGod.uc
new file mode 100644
index 0000000..60c7dbf
--- /dev/null
+++ b/sources/Commands/ACommandGod.uc
@@ -0,0 +1,205 @@
+/**
+ * Command for making player immortal.
+ * Copyright 2022 Anton Tarasenko
+ *------------------------------------------------------------------------------
+ * This file is part of Acedia.
+ *
+ * Acedia is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Acedia is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Acedia. If not, see .
+ */
+class ACommandGod extends Command;
+
+struct GodStatus
+{
+ // Player to whom we grant godhood
+ var EPlayer target;
+ // Is `target` only a demigod (can get damaged, but not die)?
+ var bool demigod;
+ // Should `target` be unaffected by attacks momentum?
+ var bool unmovable;
+};
+
+var private array godhoodList;
+
+var private ACommandGod_Announcer announcer;
+
+var private const int TDAMAGE, TMOMENTUM;
+
+protected function Finalizer()
+{
+ _.memory.Free(announcer);
+ _.kf.health.OnDamage(self).Disconnect();
+ super.Finalizer();
+}
+
+protected function BuildData(CommandDataBuilder builder)
+{
+ builder.Name(P("god")).Group(P("gameplay"))
+ .Summary(P("Command for making player immortal."));
+ builder.RequireTarget()
+ .Describe(P("Gives targeted players god status, making them"
+ @ "invincible."));
+ builder.SubCommand(P("list"))
+ .Describe(P("Reports godhood status of targeted players."));
+ builder.SubCommand(P("strip"))
+ .Describe(P("Strips targeted players from the godhood status."));
+ builder.Option(P("demi"))
+ .Describe(P("This flag makes targeted players \"demigods\" instead -"
+ @ "they still cannot die, but they can take any non-lethal"
+ @ "damage."));
+ builder.Option(P("unmovable"))
+ .Describe(P("This flag also prevents targeted players from being"
+ @ "affected by the momentum trasnferred from damaging attacks."));
+ announcer = ACommandGod_Announcer(
+ _.memory.Allocate(class'ACommandGod_Announcer'));
+ _.kf.health.OnDamage(self).connect = ProtectDivines;
+}
+
+protected function ExecutedFor(
+ EPlayer target,
+ CallData arguments,
+ EPlayer instigator)
+{
+ local GodStatus newGodStatus;
+
+ announcer.Setup(target, instigator, othersConsole);
+ if (arguments.subCommandName.IsEmpty())
+ {
+ newGodStatus.target = target;
+ newGodStatus.demigod = arguments.options.HasKey(P("demi"));
+ newGodStatus.unmovable = arguments.options.HasKey(P("unmovable"));
+ MakeGod(target, newGodStatus);
+ }
+ else if (arguments.subCommandName.Compare(P("list"))) {
+ announcer.AnnounceGodStatus(BorrowGodStatus(target));
+ }
+ else if (arguments.subCommandName.Compare(P("strip"))) {
+ RemoveGod(target);
+ }
+}
+
+private function ProtectDivines(
+ EPawn target,
+ EPawn instigator,
+ HashTable damageData)
+{
+ local int damage;
+ local EPlayer targetedPlayer;
+ local GodStatus targetDivinity;
+
+ targetedPlayer = target.GetPlayer();
+ targetDivinity = BorrowGodStatus(targetedPlayer);
+ _.memory.Free(targetedPlayer);
+ if (targetDivinity.target == none) {
+ return;
+ }
+ if (targetDivinity.unmovable) {
+ damageData.SetVector(T(TMOMENTUM), Vect(0.0f, 0.0f, 0.0f));
+ }
+ if (targetDivinity.demiGod)
+ {
+ damage = damageData.GetInt(T(TDAMAGE));
+ damage = Min(damage, target.GetHealth() - 1);
+ damageData.SetInt(T(TDAMAGE), damage);
+ }
+ else {
+ damageData.SetInt(T(TDAMAGE), 0);
+ }
+}
+
+private final function MakeGod(
+ EPlayer target,
+ GodStatus newGodStatus)
+{
+ local int godIndex;
+ local bool wasGod;
+ local GodStatus oldGodStatus;
+
+ if (target == none) {
+ return;
+ }
+ for (godIndex = 0; godIndex < godhoodList.length; godIndex += 1)
+ {
+ if (target.SameAs(godhoodList[godIndex].target))
+ {
+ wasGod = true;
+ oldGodStatus = godhoodList[godIndex];
+ break;
+ }
+ }
+ if (wasGod)
+ {
+ if ( newGodStatus.demiGod == oldGodStatus.demiGod
+ && newGodStatus.unmovable == oldGodStatus.unmovable)
+ {
+ announcer.AnnounceSameGod(newGodStatus);
+ }
+ else
+ {
+ announcer.AnnounceChangedGod(oldGodStatus, newGodStatus);
+ godhoodList[godIndex].target.FreeSelf();
+ newGodStatus.target.NewRef();
+ godhoodList[godIndex] = newGodStatus;
+ }
+ }
+ else {
+ announcer.AnnounceNewGod(newGodStatus);
+ newGodStatus.target.NewRef();
+ godhoodList[godhoodList.length] = newGodStatus;
+ }
+}
+
+private final function RemoveGod(EPlayer target)
+{
+ local int i;
+
+ if (target == none) {
+ return;
+ }
+ for (i = 0; i < godhoodList.length; i += 1)
+ {
+ if (target.SameAs(godhoodList[i].target))
+ {
+ announcer.AnnounceRemoveGod(godhoodList[i]);
+ godhoodList[i].target.FreeSelf();
+ godhoodList.Remove(i, 1);
+ return;
+ }
+ }
+ announcer.AnnounceWasNotGod();
+}
+
+private final function GodStatus BorrowGodStatus(EPlayer target)
+{
+ local int i;
+ local GodStatus emptyStatus;
+
+ if (target == none) {
+ return emptyStatus;
+ }
+ for (i = 0; i < godhoodList.length; i += 1)
+ {
+ if (target.SameAs(godhoodList[i].target)) {
+ return godhoodList[i];
+ }
+ }
+ return emptyStatus;
+}
+
+defaultproperties
+{
+ TDAMAGE = 0
+ stringConstants(0) = "damage"
+ TMOMENTUM = 1
+ stringConstants(1) = "momentum"
+}
\ No newline at end of file
diff --git a/sources/Commands/ACommandGod_Announcer.uc b/sources/Commands/ACommandGod_Announcer.uc
new file mode 100644
index 0000000..ab686ce
--- /dev/null
+++ b/sources/Commands/ACommandGod_Announcer.uc
@@ -0,0 +1,225 @@
+/**
+ * Announcer for `ACommandGod`.
+ * Copyright 2022 Anton Tarasenko
+ *------------------------------------------------------------------------------
+ * This file is part of Acedia.
+ *
+ * Acedia is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Acedia is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Acedia. If not, see .
+ */
+class ACommandGod_Announcer extends CommandAnnouncer
+ dependson(ACommandGod);
+
+var private AnnouncementVariations godStatus, newGod, removeGod, sameGod;
+var private AnnouncementVariations changedGod, wasNotGod;
+
+protected function Finalizer()
+{
+ FreeVariations(godStatus);
+ FreeVariations(newGod);
+ FreeVariations(removeGod);
+ FreeVariations(sameGod);
+ FreeVariations(changedGod);
+ FreeVariations(wasNotGod);
+ super.Finalizer();
+}
+
+public final function AnnounceGodStatus(ACommandGod.GodStatus status)
+{
+ local int i;
+ local MutableText statusAsText;
+ local array templates;
+
+ if (!godStatus.initialized)
+ {
+ godStatus.initialized = true;
+ godStatus.toSelfReport = _.text.MakeTemplate_S(
+ "You're %1");
+ godStatus.toOtherReport = _.text.MakeTemplate_S(
+ "%%target%% is %1");
+ }
+ statusAsText = DisplayStatus(status);
+ templates = MakeArray(godStatus);
+ for (i = 0; i < templates.length; i += 1) {
+ templates[i].Reset().Arg(statusAsText);
+ }
+ _.memory.Free(statusAsText);
+ MakeAnnouncement(godStatus);
+}
+
+public final function AnnounceNewGod(ACommandGod.GodStatus status)
+{
+ local int i;
+ local MutableText statusAsText;
+ local array templates;
+
+ if (!newGod.initialized)
+ {
+ newGod.initialized = true;
+ newGod.toSelfReport = _.text.MakeTemplate_S(
+ "You {$TextPositive made} yourself %1");
+ newGod.toSelfPublic = _.text.MakeTemplate_S(
+ "%%instigator%% {$TextPositive made} themselves %1");
+ newGod.toOtherReport = _.text.MakeTemplate_S(
+ "You {$TextPositive made} %%target%% %1");
+ newGod.toOtherPrivate = _.text.MakeTemplate_S(
+ "%%instigator%% {$TextPositive made} you %1");
+ newGod.toOtherPublic = _.text.MakeTemplate_S(
+ "%%instigator%% {$TextPositive made} %%target%% %1");
+ }
+ statusAsText = DisplayStatus(status);
+ templates = MakeArray(newGod);
+ for (i = 0; i < templates.length; i += 1) {
+ templates[i].Reset().Arg(statusAsText);
+ }
+ _.memory.Free(statusAsText);
+ MakeAnnouncement(newGod);
+}
+
+public final function AnnounceRemoveGod(ACommandGod.GodStatus status)
+{
+ local int i;
+ local MutableText statusAsText;
+ local array templates;
+
+ if (!removeGod.initialized)
+ {
+ removeGod.initialized = true;
+ removeGod.toSelfReport = _.text.MakeTemplate_S(
+ "You, %1, {$TextNegative became} a mere {$TextNegative mortal}");
+ removeGod.toSelfPublic = _.text.MakeTemplate_S(
+ "%1 %%instigator%% {$TextNegative made} themselves a mere"
+ @ "{$TextNegative mortal}");
+ removeGod.toOtherReport = _.text.MakeTemplate_S(
+ "%1 %%target%% was {$TextNegative made} a mere"
+ @ "{$TextNegative mortal} by you");
+ removeGod.toOtherPrivate = _.text.MakeTemplate_S(
+ "You, %1, was {$TextNegative made} a mere {$TextNegative mortal}"
+ @ "by %%instigator%%");
+ removeGod.toOtherPublic = _.text.MakeTemplate_S(
+ "%1 %%target%% was {$TextNegative made} a mere"
+ @ "{$TextNegative mortal} by %%instigator%%");
+ }
+ statusAsText = DisplayStatus(status);
+ templates = MakeArray(removeGod);
+ for (i = 0; i < templates.length; i += 1) {
+ templates[i].Reset().Arg(statusAsText);
+ }
+ _.memory.Free(statusAsText);
+ MakeAnnouncement(removeGod);
+}
+
+public final function AnnounceSameGod(ACommandGod.GodStatus status)
+{
+ local int i;
+ local MutableText statusAsText;
+ local array templates;
+
+ if (!sameGod.initialized)
+ {
+ sameGod.initialized = true;
+ sameGod.toSelfReport = _.text.MakeTemplate_S(
+ "You are already %1");
+ sameGod.toOtherReport = _.text.MakeTemplate_S(
+ "%%target%% is already %1");
+ }
+ statusAsText = DisplayStatus(status);
+ templates = MakeArray(sameGod);
+ for (i = 0; i < templates.length; i += 1) {
+ templates[i].Reset().Arg(statusAsText);
+ }
+ _.memory.Free(statusAsText);
+ MakeAnnouncement(sameGod);
+}
+
+public final function AnnounceChangedGod(
+ ACommandGod.GodStatus oldStatus,
+ ACommandGod.GodStatus newStatus)
+{
+ local int i;
+ local MutableText oldStatusAsText, newStatusAsText;
+ local array templates;
+
+ if (!changedGod.initialized)
+ {
+ changedGod.initialized = true;
+ changedGod.toSelfReport = _.text.MakeTemplate_S(
+ "You, %1, {$TextPositive made} yourself %2");
+ changedGod.toSelfPublic = _.text.MakeTemplate_S(
+ "%1 %%instigator%% {$TextPositive made} themselves %2");
+ changedGod.toOtherReport = _.text.MakeTemplate_S(
+ "You {$TextPositive made} %1 %%target%% into %1");
+ changedGod.toOtherPrivate = _.text.MakeTemplate_S(
+ "%%instigator%% {$TextPositive made} you, %1, into %2");
+ changedGod.toOtherPublic = _.text.MakeTemplate_S(
+ "%%instigator%% {$TextPositive made} %1 %%target%% into %2");
+ }
+ oldStatusAsText = DisplayStatus(oldStatus);
+ newStatusAsText = DisplayStatus(newStatus);
+ templates = MakeArray(changedGod);
+ for (i = 0; i < templates.length; i += 1) {
+ templates[i].Reset().Arg(oldStatusAsText).Arg(newStatusAsText);
+ }
+ _.memory.Free(oldStatusAsText);
+ _.memory.Free(newStatusAsText);
+ MakeAnnouncement(changedGod);
+}
+
+public final function AnnounceWasNotGod()
+{
+ local int i;
+ local array templates;
+
+ if (!sameGod.initialized)
+ {
+ sameGod.initialized = true;
+ sameGod.toSelfReport = _.text.MakeTemplate_S(
+ "You are already a mere {$TextNegative mortal}");
+ sameGod.toOtherReport = _.text.MakeTemplate_S(
+ "%%target%% is already a mere {$TextNegative mortal}");
+ }
+ templates = MakeArray(sameGod);
+ for (i = 0; i < templates.length; i += 1) {
+ templates[i].Reset();
+ }
+ MakeAnnouncement(sameGod);
+}
+
+private final function MutableText DisplayStatus(ACommandGod.GodStatus status)
+{
+ local MutableText builder;
+
+ builder = _.text.Empty();
+ if (status.target == none)
+ {
+ builder.Append(F("a mere {$TextNegative mortal}"));
+ return builder;
+ }
+ if (status.unmovable) {
+ builder.Append(F("an {$TextPositive unmovable}, "));
+ }
+ else {
+ builder.Append(F("a {$TextNeutral simple}, "));
+ }
+ if (status.demigod) {
+ builder.Append(F("immortal {$TextNeutral demigod}"));
+ }
+ else {
+ builder.Append(F("invincible {$TextPositive god}"));
+ }
+ return builder;
+}
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Futility_Feature.uc b/sources/Futility_Feature.uc
index 3d51d9e..958f56f 100644
--- a/sources/Futility_Feature.uc
+++ b/sources/Futility_Feature.uc
@@ -78,5 +78,6 @@ defaultproperties
allCommandClasses(3) = class'ACommandDB'
allCommandClasses(4) = class'ACommandInventory'
allCommandClasses(5) = class'ACommandFeature'
+ allCommandClasses(6) = class'ACommandGod'
errNoCommandsFeature = (l=LOG_Error,m="`Commands_Feature` is not detected, \"Futility\" will not be able to provide its functionality.")
}
\ No newline at end of file