civ7-modding-tools
Version:
Mod generation tool for Civilization 7.
466 lines (433 loc) • 19.9 kB
text/typescript
import * as lodash from "lodash";
import { TClassProperties, TObjectValues, TPartialRequired } from "../types";
import {
CityNameNode,
CivilizationItemNode,
CivilizationNode,
CivilizationTagNode,
CivilizationTraitNode, CivilizationUnlockNode,
DatabaseNode,
GameCivilizationNodeSlice,
GameEffectNode,
IconDefinitionNode, LeaderCivilizationBiasNode, LeaderUnlockNode,
LegacyCivilizationNode,
LegacyCivilizationTraitNode,
ModifierNode,
ProgressionTreeNodeUnlockNode,
ShellCivilizationNodeSlice,
StartBiasAdjacentToCoastNode,
StartBiasBiomeNode,
StartBiasFeatureClassNode,
StartBiasResourceNode,
StartBiasRiverNode,
StartBiasTerrainNode,
TCivilizationItemNode,
TCivilizationNode,
TIconDefinitionNode,
TLegacyCivilizationNode,
TraitModifierNode,
TraitNode,
TStartBiasBiomeNode,
TStartBiasFeatureClassNode,
TStartBiasResourceNode,
TStartBiasTerrainNode,
TTraitNode,
TypeNode,
VisArtCivilizationBuildingCultureNode,
VisArtCivilizationUnitCultureNode
} from "../nodes";
import { ACTION_GROUP_ACTION, AGE, BUILDING_CULTURES, CIVILIZATION_DOMAIN, EFFECT, KIND, REQUIREMENT, TAG_TRAIT, TRAIT, UNIT_CULTURE } from "../constants";
import { locale, trim } from "../utils";
import { XmlFile } from "../files";
import { CivilizationLocalization, TCivilizationLocalization } from "../localizations";
import { BaseBuilder } from "./BaseBuilder";
import { UnitBuilder } from "./UnitBuilder";
import { ConstructibleBuilder } from "./ConstructibleBuilder";
import { ProgressionTreeBuilder } from "./ProgressionTreeBuilder";
import { ModifierBuilder } from "./ModifierBuilder";
import { UniqueQuarterBuilder } from "./UniqueQuarterBuilder";
import { CivilizationUnlockBuilder } from "./CivilizationUnlockBuilder";
import { LeaderUnlockBuilder } from "./LeaderUnlockBuilder";
type TCivilizationBuilder = TClassProperties<CivilizationBuilder>
export class CivilizationBuilder extends BaseBuilder<TCivilizationBuilder> {
_current: DatabaseNode = new DatabaseNode();
_shell: DatabaseNode = new DatabaseNode();
_legacy: DatabaseNode = new DatabaseNode();
_localizations: DatabaseNode = new DatabaseNode();
_icons: DatabaseNode = new DatabaseNode();
_gameEffects: GameEffectNode = new GameEffectNode();
civilizationTraits: (TObjectValues<typeof TRAIT> | string)[] = [];
civilizationTags: TObjectValues<typeof TAG_TRAIT>[] = [];
trait: TPartialRequired<TTraitNode, 'traitType'> = { traitType: 'TRAIT_' };
traitAbility: TPartialRequired<TraitNode, 'traitType'> = { traitType: 'TRAIT_ABILITY_' };
civilization: TPartialRequired<TCivilizationNode, 'civilizationType' | 'domain'> = {
civilizationType: 'CIVILIZATION_CUSTOM',
domain: 'AntiquityAgeCivilizations'
}
civilizationLegacy: TPartialRequired<TLegacyCivilizationNode, 'age'> = { age: AGE.ANTIQUITY }
localizations: Partial<TCivilizationLocalization>[] = [];
icon: TPartialRequired<TIconDefinitionNode, 'path'> = { path: 'fs://game/civ_sym_han' }
civilizationItems: TPartialRequired<TCivilizationItemNode, "type" | "kind">[] = [];
startBiasBiomes: TPartialRequired<TStartBiasBiomeNode, 'biomeType'>[] = [];
startBiasResources: TPartialRequired<TStartBiasResourceNode, 'resourceType'>[] = [];
startBiasTerrains: TPartialRequired<TStartBiasTerrainNode, 'terrainType'>[] = [];
startBiasRiver: number | null = null;
startBiasFeatureClasses: TPartialRequired<TStartBiasFeatureClassNode, 'featureClassType'>[] = [];
startBiasAdjacentToCoast: number | null = null;
visArtCivilizationBuildingCultures: TObjectValues<typeof BUILDING_CULTURES>[] = [];
visArtCivilizationUnitCulture: TObjectValues<typeof UNIT_CULTURE> | null = null;
constructor(payload: Partial<TCivilizationBuilder> = {}) {
super();
this.fill(payload);
}
migrate() {
if (this.trait.traitType === 'TRAIT_') {
this.trait = {
traitType: this.civilization.civilizationType.replace('CIVILIZATION_', 'TRAIT_'),
}
}
if (this.traitAbility.traitType === 'TRAIT_ABILITY_') {
this.traitAbility = {
traitType: this.trait.traitType + '_ABILITY',
}
}
const civilization = new CivilizationNode({
adjective: locale(this.civilization.civilizationType, 'adjective'),
capitalName: locale(this.civilization.civilizationType, 'cityNames_1'),
description: locale(this.civilization.civilizationType, 'description'),
fullName: locale(this.civilization.civilizationType, 'fullName'),
name: locale(this.civilization.civilizationType, 'name'),
...this.civilization,
});
const trait = new TraitNode({
internalOnly: true,
...this.trait
});
const traitAbility = new TraitNode({
name: locale(this.civilization.civilizationType + '_ABILITY', 'name'),
description: locale(this.civilization.civilizationType + '_ABILITY', 'description'),
internalOnly: true,
...this.traitAbility
});
this._current.fill({
types: [
new TypeNode({
kind: KIND.TRAIT,
type: this.trait.traitType
}),
new TypeNode({
kind: KIND.TRAIT,
type: this.traitAbility.traitType
}),
],
traits: [trait, traitAbility],
civilizations: [GameCivilizationNodeSlice.from(civilization)],
civilizationTraits: [
new CivilizationTraitNode({
...civilization,
...this.trait
}),
new CivilizationTraitNode({
...civilization,
...this.traitAbility
}),
...this.civilizationTraits.map(item => {
return new CivilizationTraitNode({
...civilization,
traitType: item,
})
})
],
startBiasBiomes: this.startBiasBiomes.map(item => {
return new StartBiasBiomeNode({
...civilization,
...item
})
}),
startBiasTerrains: this.startBiasTerrains.map(item => {
return new StartBiasTerrainNode({
...civilization,
...item
})
}),
startBiasFeatureClasses: this.startBiasFeatureClasses.map(item => {
return new StartBiasFeatureClassNode({
...civilization,
...item
})
}),
startBiasResources: this.startBiasResources.map(item => {
return new StartBiasResourceNode({
...civilization,
...item
})
}),
startBiasRivers: this.startBiasRiver !== null ? [
new StartBiasRiverNode({
...civilization,
score: this.startBiasRiver
})
] : [],
startBiasAdjacentToCoasts: this.startBiasAdjacentToCoast !== null ? [
new StartBiasAdjacentToCoastNode({
...civilization,
score: this.startBiasAdjacentToCoast
})
] : [],
visArtCivilizationBuildingCultures: this.visArtCivilizationBuildingCultures.map(item => {
return new VisArtCivilizationBuildingCultureNode({
...civilization,
buildingCulture: item
})
}),
visArtCivilizationUnitCultures: this.visArtCivilizationUnitCulture !== null ? [
new VisArtCivilizationUnitCultureNode({
...civilization,
unitCulture: this.visArtCivilizationUnitCulture
})
] : [],
});
this._shell.fill({
civilizations: [ShellCivilizationNodeSlice.from(civilization)],
civilizationTags: this.civilizationTags.map(item => {
return new CivilizationTagNode({
civilizationDomain: this.civilization.domain,
tagType: item,
...civilization,
})
}),
civilizationItems: [
new CivilizationItemNode({
...civilization,
...traitAbility,
civilizationDomain: this.civilization.domain,
type: this.traitAbility.traitType,
kind: KIND.TRAIT,
}),
...this.civilizationItems.map(item => {
return new CivilizationItemNode({
civilizationDomain: this.civilization.domain,
...civilization,
...item
})
})
],
});
this._legacy.fill({
types: [
new TypeNode({ type: this.civilization.civilizationType, kind: KIND.CIVILIZATION }).insertOrIgnore(),
new TypeNode({ kind: KIND.TRAIT, type: this.trait.traitType }).insertOrIgnore(),
new TypeNode({ kind: KIND.TRAIT, type: this.traitAbility.traitType }).insertOrIgnore(),
],
traits: [new TraitNode(trait).insertOrIgnore()],
legacyCivilizations: [
new LegacyCivilizationNode({
...civilization,
...this.civilizationLegacy
})
],
legacyCivilizationTraits: [
new LegacyCivilizationTraitNode({
...civilization,
...this.trait
})
]
});
this._icons.fill({
iconDefinitions: [new IconDefinitionNode({
id: this.civilization.civilizationType,
...this.icon,
})]
});
this._localizations.fill({
englishText: this.localizations.map(item => {
return new CivilizationLocalization({
prefix: this.civilization.civilizationType,
...item
});
}).flatMap(item => item.getNodes())
});
const cityNamesCount = lodash.maxBy(this.localizations, loc => loc.cityNames?.length || 0)?.cityNames?.length || 0;
for (let i = 1; i <= cityNamesCount; i++) {
this._current.cityNames.push(new CityNameNode({
civilizationType: this.civilization.civilizationType,
cityName: locale(this.civilization.civilizationType, `cityNames_${i}`)
}));
}
return this;
}
/**
* @description Bind entity as unique to this civilization
* @param items
*/
bind(items: (UnitBuilder | ConstructibleBuilder | ProgressionTreeBuilder | ModifierBuilder | UniqueQuarterBuilder | CivilizationUnlockBuilder | LeaderUnlockBuilder)[] = []) {
items.forEach(item => {
if (item instanceof UnitBuilder) {
item._current.units.forEach(unit => {
unit.traitType = this.trait.traitType;
this._shell.civilizationItems.push(
new CivilizationItemNode({
civilizationDomain: this.civilization.domain,
civilizationType: this.civilization.civilizationType,
type: unit.unitType,
kind: KIND.UNIT,
icon: unit.unitType,
...unit,
})
)
});
}
if (item instanceof ModifierBuilder) {
item._gameEffects.modifiers.forEach(modifier => {
this._gameEffects.modifiers.push(modifier);
if (!item.isDetached) {
this._current.traitModifiers.push(new TraitModifierNode({
traitType: this.traitAbility.traitType,
modifierId: modifier.id
}));
}
})
}
if (item instanceof UniqueQuarterBuilder) {
item._always.uniqueQuarters.forEach(uniqueQuarter => {
uniqueQuarter.traitType = this.trait.traitType;
this._shell.civilizationItems.push(
new CivilizationItemNode({
civilizationDomain: this.civilization.domain,
civilizationType: this.civilization.civilizationType,
type: uniqueQuarter.uniqueQuarterType,
kind: KIND.QUARTER,
icon: uniqueQuarter.uniqueQuarterType,
...uniqueQuarter,
})
)
});
}
if (item instanceof ConstructibleBuilder) {
item._always.buildings.forEach(item => {
item.traitType = this.trait.traitType;
});
item._always.improvements.forEach(item => {
item.traitType = this.trait.traitType;
});
item._always.constructibles.forEach(constructible => {
this._shell.civilizationItems.push(
new CivilizationItemNode({
civilizationDomain: this.civilization.domain,
civilizationType: this.civilization.civilizationType,
type: constructible.constructibleType,
kind: KIND.IMPROVEMENT,
icon: '',
...constructible,
})
)
});
}
if (item instanceof ProgressionTreeBuilder) {
item._current.progressionTrees.forEach(progressionTree => {
this._gameEffects.modifiers.push(new ModifierNode({
id: `MOD_${progressionTree.progressionTreeType}_REVEAL`,
effect: EFFECT.PLAYER_REVEAL_CULTURE_TREE,
requirements: [{
type: REQUIREMENT.PLAYER_HAS_CIVILIZATION_OR_LEADER_TRAIT,
arguments: [{
name: 'TraitType',
value: this.trait.traitType
}]
}],
arguments: [{
name: 'ProgressionTreeType',
value: progressionTree.progressionTreeType
}]
}));
item._current.progressionTreeNodeUnlocks.push(new ProgressionTreeNodeUnlockNode({
progressionTreeNodeType: 'NODE_CIVIC_AQ_MAIN_CHIEFDOM',
targetKind: KIND.MODIFIER,
targetType: `MOD_${progressionTree.progressionTreeType}_REVEAL`,
unlockDepth: 1,
hidden: true
}));
})
}
if (item instanceof CivilizationUnlockBuilder) {
this._shell.civilizationUnlocks.push(new CivilizationUnlockNode({
ageDomain: 'StandardAges',
civilizationDomain: CIVILIZATION_DOMAIN.from(item.from.ageType),
civilizationType: item.from.civilizationType,
type: item.to.civilizationType,
ageType: item.to.ageType,
kind: KIND.CIVILIZATION,
name: locale(item.to.civilizationType, 'NAME'),
description: locale(item.to.civilizationType, 'DESCRIPTION'),
icon: item.to.civilizationType
}))
}
if (item instanceof LeaderUnlockBuilder) {
this._shell.leaderUnlocks.push(new LeaderUnlockNode({
leaderDomain: 'StandardLeaders',
ageDomain: 'StandardAges',
kind: KIND.CIVILIZATION,
name: locale(item.leaderUnlock.type, 'name'),
description: locale(item.leaderUnlock.type, 'description'),
icon: item.leaderUnlock.type,
...item.leaderUnlock
}))
this._shell.leaderCivilizationBias.push(new LeaderCivilizationBiasNode({
civilizationDomain: CIVILIZATION_DOMAIN.from(item.leaderUnlock.ageType),
civilizationType: item.leaderUnlock.type,
reasonType: locale(`PLAY_AS_${trim(item.leaderUnlock.leaderType)}_${trim(item.leaderUnlock.type)}`, 'TOOLTIP'),
...item.leaderUnlock,
...item.leaderCivilizationBias
}))
}
});
return this;
}
build() {
const path = `/civilizations/${lodash.kebabCase(trim(this.civilization.civilizationType))}/`;
return [
new XmlFile({
path,
name: 'current.xml',
content: this._current.toXmlElement(),
actionGroups: [this.actionGroupBundle.current],
actionGroupActions: [ACTION_GROUP_ACTION.UPDATE_DATABASE]
}),
new XmlFile({
path,
name: 'legacy.xml',
content: this._legacy.toXmlElement(),
actionGroups: [this.actionGroupBundle.always],
actionGroupActions: [ACTION_GROUP_ACTION.UPDATE_DATABASE]
}),
new XmlFile({
path,
name: 'shell.xml',
content: this._shell.toXmlElement(),
actionGroups: [this.actionGroupBundle.shell],
actionGroupActions: [ACTION_GROUP_ACTION.UPDATE_DATABASE]
}),
new XmlFile({
path,
name: 'icons.xml',
content: this._icons.toXmlElement(),
actionGroups: [this.actionGroupBundle.shell, this.actionGroupBundle.always],
actionGroupActions: [ACTION_GROUP_ACTION.UPDATE_ICONS]
}),
new XmlFile({
path,
name: 'localization.xml',
content: this._localizations.toXmlElement(),
actionGroups: [this.actionGroupBundle.shell, this.actionGroupBundle.always],
actionGroupActions: [ACTION_GROUP_ACTION.UPDATE_TEXT]
}),
new XmlFile({
path,
name: 'game-effects.xml',
content: this._gameEffects?.toXmlElement(),
actionGroups: [this.actionGroupBundle.current],
actionGroupActions: [ACTION_GROUP_ACTION.UPDATE_DATABASE]
}),
]
}
}